## 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 :
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 :
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.
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
Aucun commentaire pour le moment.