Volver al inicio

Idempotencia de Kafka: Garantías de Entrega en la Práctica

El artículo cubre la configuración práctica de idempotencia en Kafka para productor y consumidor. Discute ProducerId, ProducerEpoch, parámetros de configuración críticos y protección contra procesamiento doble vía lógica de negocio en PostgreSQL.

Kafka Idempotente: productor y consumidor sin duplicados
Advertisement 728x90

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:

Google AdInline article slot
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:

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

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.

Google AdInline article slot

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

Advertisement 728x90

Leer después