Zurück zur Startseite

Kafka Idempotenz: Zustellgarantien in der Praxis

Der Artikel behandelt die praktische Einrichtung der Idempotenz in Kafka für Producer und Consumer. Er diskutiert ProducerId, ProducerEpoch, kritische Konfigurationsparameter und Schutz vor doppelter Verarbeitung via Geschäftslogik in PostgreSQL.

Idempotentes Kafka: Producer und Consumer ohne Duplikate
Advertisement 728x90

Kafka-Zustellgarantien: Producer- und Consumer-Idempotenz in der Praxis

Der inventory-service wechselt vom In-Memory-Speicher zu PostgreSQL, um das Outbox-Pattern und Zustellgarantien zu unterstützen. Fügen Sie einen postgres-inventory-Container zu docker-compose.yaml hinzu mit image postgres:17, ports 5432:5432 und Umgebungsvariablen POSTGRES_USER=postgres, POSTGRES_PASSWORD=postgres, POSTGRES_DB=inventory_db.

Im pom.xml des inventory-service fügen Sie die folgenden Abhängigkeiten hinzu:

<dependency>
  <groupId>org.postgresql</groupId>
  <artifactId>postgresql</artifactId>
  <scope>runtime</scope>
</dependency>
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>

In application.properties legen Sie die Verbindung fest:

Google AdInline article slot
spring.datasource.url=jdbc:postgresql://localhost:5432/inventory_db
spring.datasource.username=postgres
spring.datasource.password=postgres

Erstellen Sie die inventory-Tabelle:

CREATE TABLE inventory (
    id BIGSERIAL PRIMARY KEY,
    product_name VARCHAR(256) NOT NULL UNIQUE,
    quantity INT NOT NULL CHECK (quantity >= 0)
);

INSERT INTO inventory (product_name, quantity) VALUES
('Smartphone', 5),
('Tablet', 10),
('Desktop', 6);

Inventory-Entity:

@Entity
@Table(name = "inventory")
public class Inventory {
    @Id
    @Column(name = "id")
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(name = "product_name", nullable = false, unique = true, length = 256)
    private String productName;

    @Column(name = "quantity", nullable = false)
    private Integer quantity;

    public Inventory(String productName, Integer quantity) {
        this.productName = productName;
        this.quantity = quantity;
    }
    // getters, setters
}

Repository mit deductStock:

Google AdInline article slot
public interface InventoryRepository extends JpaRepository<Inventory, Long> {
    @Modifying
    @Query("UPDATE Inventory i SET i.quantity = i.quantity - :quantity WHERE i.productName = :productName AND i.quantity >= :quantity")
    int deductStock(@Param("productName") String productName, @Param("quantity") Integer quantity);
}

Der KafkaListener-Handler im Service prüft die Verfügbarkeit des Lagerbestands und sendet InventoryReservedEvent oder InventoryRejectedEvent.

Einrichtung von acks und Verständnis der exactly-once-Semantik

Der Parameter spring.kafka.producer.acks steuert die Bestätigungen: 0 für at-most-once, 1 für teilweise, all für at-least-once mit Replikation auf allen Followern.

Exactly-once-Zustellung in verteilten Systemen ist aufgrund des Zwei-Generäle-Problems unmöglich. Sie wird als exactly-once-Semantik umgesetzt: at-least-once-Zustellung + Producer- und Consumer-Idempotenz.

Google AdInline article slot

Producer-Idempotenz: ProducerId, Epoch und Sequenznummer

Idempotenz verhindert Duplikate bei Wiederholungen durch verlorene acks. Sie wird aktiviert durch:

spring.kafka.producer.properties.enable.idempotence=true

Der Broker weist der Verbindung ein (ProducerId, ProducerEpoch)-Paar zu:

  • ProducerId: eindeutige Producer-Kennung
  • ProducerEpoch: inkrementiert sich bei Neustart, Schutz vor Zombie-Producern

Jede Nachricht erhält eine Sequenznummer. Der Broker lehnt Duplikate innerhalb von ProducerId/Epoch/Sequenz ab.

Zombie-Producer-Szenario:

  • Producer stürzt ab, "wird aber" nach Neustart eines neuen wiederbelebt
  • Der alte fährt mit hoher Sequenznummer fort
  • Der neue beginnt bei 1 – ProducerEpoch unterscheidet sie

Transactional.id sorgt für die Erhaltung der ProducerId bei Neustart (Spring ruft initTransactions() auf). Ohne sie – neue zufällige ProducerId.

Kritische Idempotenz-Parameter

spring.kafka.producer.acks=all
spring.kafka.producer.retries=2147483647  # >0
spring.kafka.producer.properties.max.in.flight.requests.per.connection=5  # ≤5
spring.kafka.producer.properties.enable.idempotence=true

Verstöße deaktivieren die Idempotenz. Wenden Sie sie auf order-service und inventory-service an.

Consumer-Idempotenz gegen doppelte Verarbeitung

Der Consumer kann eine Nachricht doppelt verarbeiten, wenn er nach dem Commit des Offsets abstürzt, aber vor Abschluss der Geschäftslogik. Für inventory-service:

  • Liest OrderPlacedEvent
  • deductStock() in PostgreSQL (atomar)
  • Sendet InventoryReservedEvent
  • Absturz
  • Liest Offset erneut, wiederholt deductStock() – aber quantity bereits reduziert, UPDATE greift nicht

Das gewährleistet Idempotenz auf Geschäftslogik-Ebene durch bedingtes UPDATE.

Vorteile des Ansatzes:

  • Kein zusätzlicher Offset-Speicher
  • Nutzt DB-ACID-Transaktionen
  • Einfache Umsetzung über WHERE quantity >= :quantity

Wichtige Punkte

  • Producer-Idempotenz: enable.idempotence=true + acks=all + retries>0 + max.in.flight≤5
  • ProducerEpoch: Schutz vor Zombie-Producern bei Neustart
  • Transactional.id: erhält ProducerId, erfordert initTransactions()
  • Consumer: Geschäftslogik mit bedingtem DB-UPDATE gewährleistet Idempotenz
  • Exactly-once-Semantik: at-least-once + Idempotenz, keine wörtliche exactly-once

— Editorial Team

Advertisement 728x90

Weiterlesen