Exactly-once vs At-least-once
1) ¿Por qué discutir semánticas en general
La semántica de entrega determina con qué frecuencia el destinatario verá el mensaje cuando hay fallos y retrases:- At-most-once - sin repeticiones, pero la pérdida es posible (raramente aceptable).
- At-least-once - no perdemos, pero es posible duplicar (default de la mayoría de los corredores/colas).
- Exactly-once: cada mensaje se procesa exactamente una vez en términos del efecto observado.
La verdad clave: en un mundo distribuido sin transacciones globales y consistencia sincrónica, es inalcanzable el «puro» fin a fin exacto-once. Construimos efectivamente exactly-once: permitimos repeticiones en el transporte, pero hacemos el tratamiento idempotente para que el efecto observado sea «como una sola vez».
2) Modelo de fallas y donde se producen duplicados
Las repeticiones aparecen debido a:- Pérdida de ack/commit (productor/bróker/consumer «no escuchado» confirmación).
- Reelecciones de líderes/réplicas, recuperaciones tras rupturas en la red.
- Taimouts/retraídas en cualquier sitio (kliyent→broker→konsyumer→sink).
Consecuencia: no se puede confiar en la «singularidad de la entrega» del transporte. Controlamos los efectos: escribir en el DB, cargar dinero, enviar una carta, etc.
3) Exactly-once en los proveedores y lo que es realmente
3. 1 Kafka
Da ladrillos:- Idempotent Producer (`enable. idempotence = true ') - evita las tomas en el lado del productor cuando se retraen.
- Transacciones - atomicamente publican mensajes en varios lotes y commiten offsets de consumo (patrón de lectura-proceso-escritura sin «pases»).
- Compaction: almacena el último valor por clave.
Pero el «final de la cadena» (xink: DB/pago/correo) todavía requiere idempotencia. De lo contrario, la toma del manejador causará un efecto de toma.
3. 2 NATS / Rabbit / SQS
El valor predeterminado es at-least-once con ack/redelivery. Exactly-once se logra a nivel de aplicación: claves, dedust stor, upsert.
Conclusión: Exactly-once por transporte ≠ efecto exactly-once. Este último se hace en el manejador.
4) Cómo construir efectivamente exactly-once encima de at-least-once
4. 1 Clave idempotente (clave idempotency)
Cada comando/evento lleva una clave natural: 'payment _ id', 'order _ id # step', 'saga _ id # n'. Controlador:- Comprueba «¿ya ha visto?» - dedust-stor (Redis/BD) con TTL/retoque.
- Si ha visto - repite el resultado calculado previamente o hace un no-op.
lua
-- SET key if not exists; expires in 24h local ok = redis.call("SET", KEYS[1], ARGV[1], "NX", "EX", 86400)
if ok then return "PROCESS" else return "SKIP" end
4. 2 Upsert en base (azul idempotente)
Los registros se realizan a través de UPSERT/ON CONFLICT con verificación de versión/cantidad.
PostgreSQL:sql
INSERT INTO payments(id, status, amount, updated_at)
VALUES ($1, $2, $3, now())
ON CONFLICT (id) DO UPDATE
SET status = EXCLUDED.status,
updated_at = now()
WHERE payments.status <> EXCLUDED.status;
4. 3 Outbox/Inbox Transaccional
Outbox: una transacción comercial y la entrada de «eventos a publicar» ocurre en una sola transacción de DB. El editor de fondo lee outbox y envía al corredor → no hay discrepancias entre el estado y el evento.
Inbox: para los comandos entrantes, guardamos 'message _ id' y el resultado antes de la ejecución; el tratamiento repetido ve la grabación y no repite los efectos secundarios.
4. 4 Procesamiento de cadena consistente (read→process→write)
Kafka: la transacción «leyó el offset → registró los resultados → commit» en un solo bloque atómico.
Sin transacciones: «primero anota el resultado/Inbox, luego ack»; cuando el crash duplicado verá Inbox y terminará no-op.
4. 5 SAGA/compensación
Cuando la idempotencia no es posible (el proveedor externo ha cargado el dinero), se utilizan las operaciones compensatorias (refund/void) y las API externas idempotentes (el 'POST' repetido con el mismo 'Idempotency-Key' da el mismo resultado).
5) Cuando es suficiente en-least-once
Actualizaciones de caché/vistas materializadas con un acuerdo de clave.
Contadores/métricas donde la re-incrementación es aceptable (o almacenando delta con la versión).
Notificaciones donde la escritura secundaria no es crítica (es mejor poner la clave de todos modos).
Regla: a menos que la toma cambie el sentido del negocio o detecte fácilmente → al-least-once + protección parcial.
6) Rendimiento y costo
Exactly-once (incluso «eficientemente») cuesta más: entradas adicionales (Inbox/Outbox), almacenamiento de claves, transacciones, más difícil de diagnosticar.
At-least-once es más barato/más fácil, mejor por throughput/p99.
Estime: el precio de la toma × la probabilidad de la toma vs el costo de la protección.
7) Ejemplos de configuraciones y código
7. 1 productor de Kafka (idempotencia + transacciones)
properties enable.idempotence=true acks=all retries=INT_MAX max.in.flight.requests.per.connection=5 transactional.id=orders-writer-1
java producer.initTransactions();
producer.beginTransaction();
producer.send(recordA);
producer.send(recordB);
// также можно atomically commit consumer offsets producer.commitTransaction();
7. 2 Consumer con Inbox (pseudocódigo)
pseudo if (inbox.exists(msg.id)) return inbox.result(msg.id)
begin tx if!inbox.insert(msg.id) then return inbox.result(msg.id)
result = handle(msg)
sink.upsert(result) # идемпотентный синк inbox.set_result(msg.id, result)
commit ack(msg)
7. 3 HTTP Idempotency-Key (API externas)
POST /payments
Idempotency-Key: 7f1c-42-...
Body: { "payment_id": "p-123", "amount": 10.00 }
El nuevo POST con la misma clave → el mismo resultado/estado.
8) Observabilidad y métricas
'duplicate _ attempts _ total' - cuántas veces se capturó la toma (por Inbox/Redis).
'idempotency _ hit _ rate' es la proporción de repeticiones «rescatadas» por la idempotencia.
'txn _ abort _ rate' (Kafka/BD) es la proporción de retrocesos.
'outbox _ backlog' - Retraso en la publicación.
'exactly _ once _ path _ latency {p95, p99}' vs 'at _ least _ once _ path _ latency' - gastos generales.
Auditoría de registros: el conjunto 'message _ id', 'idempotency _ key', 'saga _ id', 'attempt'.
9) Pruebas de reproducción (Días de juego)
Repetición del envío: retraídas del productor bajo temporizadores artificiales.
Crash entre «azul y ack»: asegúrese de que Inbox/Upsert prevenga la toma.
Pere-entrega: aumentar la redelivery en el corredor; Comprobar el dedoup.
Idempotencia de las API externas: volver a publicar con la misma clave es la misma respuesta.
Cambio de líder/ruptura de red: compruebe las transacciones de Kafka/el comportamiento de los cónsumers.
10) Anti-patrones
Confiar en el transporte: «tenemos Kafka con exactly-once, significa que se puede sin llaves» - no.
No-op ack antes de la grabación: acked, pero el azul cayó → pérdida.
Ausencia de DLQ/retraídas con jitter: repeticiones interminables y tormenta.
UUID aleatorios en lugar de claves naturales: nada que deduplicar.
Mezclar Inbox/Outbox con tablas prod sin índices: bloqueos en caliente y colas p99.
Operaciones comerciales sin API idempotente en proveedores externos.
11) Lista de verificación de selección
1. Precio de toma (dinero/derecho/UX) vs precio de protección (latencia/dificultad/costo).
2. ¿Existe una clave natural de evento/operación? Si no, invente uno estable.
3. ¿El cinc admite Upsert/versioning? De lo contrario - Inbox + compensación.
4. ¿Se necesitan transacciones globales? Si no, segmente en SAGA.
5. ¿Necesitas una réplica o un retoque largo? Kafka + Outbox. ¿Necesita RPC rápido/baja latencia? NATS + Idempotency-Key.
6. Multi-tenencia y cuotas: aislamiento de claves/espacios.
7. Observabilidad: se incluyen las métricas idempotency y backlog.
12) FAQ
P: ¿Es posible llegar a un final «matemático» exacto-once?
R: Sólo en escenarios estrechos con un almacenamiento de información y transacciones consistentes en todo el camino. En general, no; use efectivamente exactly-once a través de la idempotencia.
P: ¿Qué es más rápido?
A: At-least-once. Exactly-once agrega transacciones/almacenamiento de claves → por encima de p99 y costo.
P: ¿Dónde almacenar las llaves de la idempotencia?
R: Un emisor rápido (Redis) con TTL o una tabla de Inbox (PK = message _ id). Para pagos - más tiempo (días/semanas).
P: ¿Cómo elegir las llaves de dedoup TTL?
R: Mínimo = tiempo máximo de reabastecimiento + stock operativo (generalmente 24-72 h). Para las finanzas, más.
P: ¿Necesito una clave si tengo compactación por clave en Kafka?
R: Sí. Compaction reducirá el almacenamiento, pero no hará que su cinc sea idempotente.
13) Resultados
At-least-once es una semántica de transporte básica y confiable.
Exactly-once como efecto de negocio se logra a nivel de manejador: Idempotency-Key, Inbox/Outbox, Upsert/version, SAGA/compensación.
La elección es un compromiso costo ↔ riesgo de toma ↔ facilidad de operación. Diseñe las llaves naturales, haga que los azules sean idempotentes, agregue la observabilidad y lleve a cabo los días de juego regularmente - entonces sus paipelines serán predecibles y seguros incluso con una tormenta de retraídas y fallas.