Garantías de entrega de Kafka: Idempotencia del productor y del consumidor en la práctica
El servicio inventory-service está cambiando de almacenamiento en memoria a PostgreSQL para soportar el patrón Outbox y las garantías de entrega. Agrega un contenedor postgres-inventory a docker-compose.yaml con la imagen postgres:17, puertos 5432:5432 y variables de entorno POSTGRES_USER=postgres, POSTGRES_PASSWORD=postgres, POSTGRES_DB=inventory_db.
En el pom.xml del inventory-service, agrega las siguientes dependencias:
<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>
En application.properties, especifica la conexión:
spring.datasource.url=jdbc:postgresql://localhost:5432/inventory_db
spring.datasource.username=postgres
spring.datasource.password=postgres
Crea la tabla 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);
Entidad 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
}
Repositorio con 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);
}
El manejador KafkaListener en el servicio verifica la disponibilidad de stock y envía InventoryReservedEvent o InventoryRejectedEvent.
Configuración de acks y comprensión de la semántica exactly-once
El parámetro spring.kafka.producer.acks controla las confirmaciones: 0 para at-most-once, 1 para parcial, all para at-least-once con replicación en todos los seguidores.
La entrega exactly-once en sistemas distribuidos es imposible debido al Problema de los dos generales. Se implementa como semántica exactly-once: entrega at-least-once + idempotencia del productor y del consumidor.
Idempotencia del productor: ProducerId, Epoch y número de secuencia
La idempotencia evita duplicados en reintentos por pérdida de ack. Se habilita con:
spring.kafka.producer.properties.enable.idempotence=true
El broker asigna un par (ProducerId, ProducerEpoch) a la conexión:
- ProducerId: identificador único del productor
- ProducerEpoch: se incrementa en reinicios, protección contra productores zombis
Cada mensaje se numera con un número de secuencia. El broker rechaza duplicados dentro de ProducerId/Epoch/secuencia.
Escenario de productor zombi:
- El productor falla, pero "revive" después de que uno nuevo se reinicia
- El viejo continúa con un número de secuencia alto
- El nuevo comienza desde 1 — ProducerEpoch los distingue
Transactional.id asegura la preservación de ProducerId en reinicios (Spring llama a initTransactions()). Sin él — nuevo ProducerId aleatorio.
Parámetros críticos de idempotencia
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
Una violación deshabilita la idempotencia. Aplícalo a order-service y inventory-service.
Idempotencia del consumidor contra procesamiento doble
El consumidor puede procesar un mensaje dos veces si falla después de confirmar el offset pero antes de completar la lógica de negocio. Para inventory-service:
- Lee OrderPlacedEvent
- deductStock() en PostgreSQL (atómicamente)
- Envía InventoryReservedEvent
- Falla
- Relee el offset, repite deductStock() — pero quantity ya se redujo, el UPDATE no funcionará
Esto asegura idempotencia a nivel de lógica de negocio mediante UPDATE condicional.
Ventajas del enfoque:
- Sin almacenamiento adicional de offsets
- Usa transacciones ACID de la BD
- Implementación simple mediante WHERE quantity >= :quantity
Puntos clave
- Idempotencia del productor: enable.idempotence=true + acks=all + retries>0 + max.in.flight≤5
- ProducerEpoch: protección contra productores zombis en reinicios
- Transactional.id: preserva ProducerId, requiere initTransactions()
- Consumidor: lógica de negocio con UPDATE condicional en BD asegura idempotencia
- Semántica exactly-once: at-least-once + idempotencia, no exactly-once literal
— Editorial Team
Aún no hay comentarios.