Retour à l'accueil

Idempotence Kafka : Garanties de livraison en pratique

L'article couvre la configuration pratique de l'idempotence dans Kafka pour le producteur et le consommateur. Il discute de ProducerId, ProducerEpoch, paramètres de configuration critiques et protection contre le double traitement via la logique métier dans PostgreSQL.

Kafka idempotent : producteur et consommateur sans doublons
Advertisement 728x90

## Garanties de livraison Kafka : Idempotence du producteur et du consommateur en pratique

Le service inventory-service passe d'un stockage en mémoire à PostgreSQL pour supporter le pattern Outbox et les garanties de livraison. Ajoutez un conteneur postgres-inventory à docker-compose.yaml avec l'image postgres:17, les ports 5432:5432, et les variables d'environnement POSTGRES_USER=postgres, POSTGRES_PASSWORD=postgres, POSTGRES_DB=inventory_db.

Dans le pom.xml du service inventory-service, ajoutez les dépendances suivantes :

<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>

Dans application.properties, spécifiez la connexion :

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

Créez la table inventory :

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);

Entité Inventory :

@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 avec 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);
}

Le gestionnaire KafkaListener dans le service vérifie la disponibilité du stock et envoie InventoryReservedEvent ou InventoryRejectedEvent.

Configuration des acks et compréhension des sémantiques exactly-once

Le paramètre spring.kafka.producer.acks contrôle les accusés de réception : 0 pour au plus une fois, 1 pour partiel, all pour au moins une fois avec réplication sur tous les followers.

La livraison exactement une fois dans les systèmes distribués est impossible en raison du problème des deux généraux. Elle est implémentée comme des sémantiques exactly-once : livraison au moins une fois + idempotence du producteur et du consommateur.

Google AdInline article slot

Idempotence du producteur : ProducerId, Epoch et numéro de séquence

L'idempotence évite les doublons lors d'une nouvelle tentative due à la perte d'un ack. Elle est activée par :

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

Le courtier assigne une paire (ProducerId, ProducerEpoch) à la connexion :

  • ProducerId : identifiant unique du producteur
  • ProducerEpoch : s'incrémente au redémarrage, protection contre les producteurs zombies

Chaque message est numéroté avec un numéro de séquence. Le courtier rejette les doublons au sein de ProducerId/Epoch/séquence.

Scénario de producteur zombie :

  • Le producteur plante, mais « ressuscite » après qu'un nouveau redémarre
  • L'ancien continue avec un numéro de séquence élevé
  • Le nouveau commence à partir de 1 — ProducerEpoch les distingue

Transactional.id assure la préservation de ProducerId au redémarrage (Spring appelle initTransactions()). Sans cela — nouveau ProducerId aléatoire.

Paramètres critiques d'idempotence

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

Toute violation désactive l'idempotence. Appliquez à order-service et inventory-service.

Idempotence du consommateur contre le double traitement

Le consommateur peut traiter un message deux fois s'il plante après avoir commis l'offset mais avant d'avoir terminé la logique métier. Pour inventory-service :

  • Lit OrderPlacedEvent
  • deductStock() dans PostgreSQL (atomiquement)
  • Envoie InventoryReservedEvent
  • Crash
  • Relit l'offset, répète deductStock() — mais quantity déjà réduit, l'UPDATE ne fonctionnera pas

Ceci assure l'idempotence au niveau de la logique métier via un UPDATE conditionnel.

Avantages de l'approche :

  • Pas de stockage supplémentaire d'offsets
  • Utilise les transactions ACID de la base de données
  • Implémentation simple via WHERE quantity >= :quantity

Points clés

  • Idempotence du producteur : enable.idempotence=true + acks=all + retries>0 + max.in.flight≤5
  • ProducerEpoch : protection contre les producteurs zombies au redémarrage
  • Transactional.id : préserve ProducerId, nécessite initTransactions()
  • Consommateur : logique métier avec UPDATE conditionnel en base de données assure l'idempotence
  • Sémantiques exactly-once : au moins une fois + idempotence, pas littéralement exactement une fois

— Editorial Team

Advertisement 728x90

Lire ensuite