← Java

Java · 18 / 18

Idempotenz, Outbox und verlässliche Wiederholung

Du erkennst doppelte Requests und verbindest einen Datenbank-Commit mit späterer Ereigniszustellung.

  • Groß
  • Anspruchsvoll
  • 120 min

Voraussetzungen: Spring-Boot-Batchworkflow mit Upload und Docker

Ein Timeout sagt nicht, ob eine Änderung ausgeführt wurde. Ein wiederholter Request darf deshalb nicht automatisch erneut wirken. Idempotenz hält einen stabilen Schlüssel mit Request-Fingerprint und Ergebnis. Eine Outbox löst eine andere Grenze: Datenbankänderung und Ereignisauftrag werden gemeinsam gespeichert, bevor ein Relay später an Kafka sendet. Beide Muster benötigen explizite Regeln für Parallelität, Abbruch und Wiederholung. Java Idempotency Kit und Transactional Outbox Relay zeigen diese Regeln mit PostgreSQL und Integrationstests.

01Die zwei Grenzen auseinanderhalten

Der kleine Simulator hält denselben Request-Key mit Ergebnis und Fingerprint. Er zeigt Wiederholung und Ablehnung eines geänderten Requests. Er ist nur im Speicher und kein konkurrierender PostgreSQL-Adapter. Das echte Kit ergänzt atomare Leases und sichere Besitzerprüfung.

Idempotency.java · javaDatei öffnen
import java.util.HashMap;
import java.util.Map;
public class Idempotency {
  record Saved(String fingerprint, int result) {}
  final Map<String, Saved> results = new HashMap<>();
  int effects = 0;
  int execute(String key, String fingerprint) {
    Saved previous = results.get(key);
    if (previous != null) {
      if (!previous.fingerprint().equals(fingerprint)) throw new IllegalArgumentException("Key conflict");
      return previous.result();
    }
    int result = ++effects;
    results.put(key, new Saved(fingerprint, result)); return result;
  }
  public static void main(String[] args) {
    var service = new Idempotency();
    System.out.println(service.execute("command-1", "same-input"));
    System.out.println(service.execute("command-1", "same-input"));
    if (service.effects != 1) throw new AssertionError("Duplicate effect");
    try { service.execute("command-1", "different-input"); throw new AssertionError("Conflict accepted"); }
    catch (IllegalArgumentException expected) { System.out.println(expected.getMessage()); }
  }
}

Starten / prüfen

java Idempotency.java

02Die Referenzprojekte prüfen

Klone die zwei festen Codeversionen in getrennte Lernordner. ./mvnw verify prüft die Module. Für die PostgreSQL-Integration muss Docker tatsächlich verfügbar sein. Lies die Testausgabe und unterscheide bestanden von übersprungen.

setup.sh · bashDatei öffnen
git clone https://github.com/wasiliy-strecker/java-idempotency-kit.git java-idempotency-kit-learning
cd java-idempotency-kit-learning
git checkout 24dd30d1b5c50bc33ed5fde3df7ca4af48a31b90
./mvnw verify

03Atomare Request-Verarbeitung verfolgen

Lies PostgresIdempotencyStore. Ein Key wird atomar erworben. Ein weiterer Request mit anderem Fingerprint darf den ersten nicht als passende Wiederholung behandeln. Nach Ende einer Lease darf ein alter Besitzer nicht ungeprüft das Ergebnis eines neuen Besitzers überschreiben.

04Daten und Outbox zusammen committen

Im SQL-Lernbeispiel stehen Order und Ereignis im selben BEGIN/COMMIT. Scheitert ein INSERT, werden beide zurückgerollt. Ein direkter Kafka-Aufruf innerhalb eines ansonsten unabhängigen DB-Ablaufs bietet diese Garantie nicht. Der echte JdbcOutboxWriter erhält den passenden Transaktionskontext.

outbox.sql · sqlDatei öffnen
CREATE TABLE learning_orders(id UUID PRIMARY KEY, amount_cents BIGINT NOT NULL);
CREATE TABLE learning_outbox(id UUID PRIMARY KEY, order_id UUID NOT NULL REFERENCES learning_orders(id), payload JSONB NOT NULL, published BOOLEAN NOT NULL DEFAULT false);
BEGIN;
INSERT INTO learning_orders VALUES ('00000000-0000-0000-0000-000000000010', 12990);
INSERT INTO learning_outbox(id,order_id,payload) VALUES (
 '00000000-0000-0000-0000-000000000011',
 '00000000-0000-0000-0000-000000000010',
 '{"type":"OrderCreated"}');
COMMIT;
SELECT o.amount_cents,e.payload FROM learning_orders o JOIN learning_outbox e ON e.order_id=o.id;

05Relay, Retry und Duplikate verstehen

Das Relay beansprucht Arbeit mit einer Lease. Nach erfolgreicher Veröffentlichung markiert es den Auftrag. Stirbt es zwischen Veröffentlichung und Markierung, kann dieselbe Nachricht erneut erscheinen. Empfänger brauchen deshalb eine stabile Ereignis-ID und einen passenden Duplikatvertrag. At-least-once ist keine globale Exactly-once-Garantie.

06Die entscheidenden Fehlerfälle testen

Prüfe denselben Request-Key gleichzeitig, geänderten Fingerprint, abgelaufene Lease und verlorene Antwort. Für die Outbox prüfe DB-Rollback, Publisherfehler und Absturz nach erfolgreichem Senden. Ein Test mit echten PostgreSQL-Transaktionen muss zeigen, welche Wirkung tatsächlich einmal oder erneut auftreten kann.

Erwartetes Ergebnis

Der Simulator erhöht seine fachliche Wirkung bei gleichem Key nur einmal. Das SQL-Beispiel schreibt Auftrag und Ereignis gemeinsam. Die echten Integrationstests prüfen Parallelitäts- und Leasegrenzen.

Datenfluss
  1. stabiler Request-Key
  2. atomare Lease / Fingerprint
  3. Fachänderung + Outbox-Commit
  4. Relay / Retry
  5. duplikattoleranter Empfänger

Deine Übung

Starte zwei Threads gegen den Memory-Simulator mit demselben Key. Warum ist seine HashMap kein Beweis für sichere Idempotenz?

Lösung mit Erklärung anzeigen

Lesen, Wirkung und Schreiben sind dort keine atomare gemeinsame Operation. Beide Threads können den fehlenden Key sehen und die Wirkung ausführen. Ein synchronisierter Lernblock schützt nur diesen einen Prozess. Das echte PostgreSQL-Kit benötigt eine atomare Datenbankentscheidung, Fingerprintprüfung und Besitzerregeln über mehrere Prozesse hinweg.

Projektbezug und Quellen

Das Beispiel ist zum Lernen vereinfacht. Die verlinkte Codeversion zeigt die Anwendung im Projekt und die dortigen Prüfungen.