Desduplicación de eventos
1) Por qué se necesita deduplicación
Los duplicados aparecen debido a retraídas, temporizadores de red, recuperaciones de feiles y un repliegue de datos históricos. Si no se controlan:- se violan las invariantes (doble cargo, correo electrónico/SMS repetido, orden «dos veces creada»);
- aumento de los costos (repetición de registros/procesamiento);
- el analista se distorsiona.
El objetivo de la deduplicación es proporcionar un único efecto observado en repeticiones de transporte permitidas, a menudo junto con la idempotencia.
2) Dónde colocar la deduplicación (niveles)
1. Edge/API-gateway - Cortar tomas explícitas por 'Idempotency-Keu '/cuerpo + firma.
2. Bróker/stream - deduplicación lógica por clave/secuenciación, coalescing en caso de error (menos común - debido al costo).
3. El receptor de eventos (consumer) es el lugar principal: Inbox/Key Table/caché.
4. Cink (BD/caché) - llaves únicas/UPSERT/versiones/compases.
5. ETL/análisis - dedoup por ventana de tiempo y clave en las columnas.
Regla: tan pronto como sea posible, pero teniendo en cuenta el costo de los falsos positivos y la necesidad de replicar.
3) Claves de deduplicación
3. 1 Natural (preferiblemente)
`payment_id`, `order_id`, `saga_id#step`, `aggregate_id#seq`.
Garantizar la estabilidad y el sentido.
3. 2 Compuestos
`(tenant_id, type, external_id, version)` или `(user_id, event_ts_truncated, payload_hash)`.
3. 3 Impresión (fingerprint)
Hash de un subconjunto determinista de campos (normalizar orden/mayúsculas), opcional 'HMAC (secret, payload)'.
3. 4 Secuencias/Versiones
Monótono 'seq' per aggregate (bloqueo/versionamiento optimista).
Anti-patrón: «UUID aleatorio» sin conexión con la entidad empresarial - el dedoup no es posible.
4) Ventanas temporales y orden
La ventana de deduplicación es el período durante el cual un evento puede volver a aparecer (generalmente 24-72h; para las finanzas - más tiempo).
Out-of-order: permitimos la tardanza (lateness). En los frameworks de streaming, event time + watermarks.
Sliding/Fix-window dedoop: "¿han visto la clave en los últimos minutos N? ».
Sequence-aware: si 'seq' ≤ último procesado es una toma/repetición.
5) Estructuras de datos e implementación
5. 1 Contabilización exacta (exacto)
Redis SET/STRING + TTL: 'SETNX key 1 EX 86400' → «por primera vez - manejamos, de lo contrario - SKIP».
Caché LRU/LFU (in-proc): rápido pero volátil → mejor solo como primera barrera.
Índices únicos SQL + UPSERT: «insertar o actualizar» (efecto idempotente).
5. 2 Estructuras aproximadas (probabilistic)
Filtro de Bloom/Cuckoo: memoria barata, falsos positivos posibles (false positive). Adecuado para un drop «ruidoso» explícito (por ejemplo, telemetría), no para finanzas/pedidos.
Count-Min Sketch: evaluación de frecuencias para protección contra tomas «calientes».
5. 3 Estados de transmisión
Kafka Streams/Flink: keyed state store c TTL, dedoup por clave en la ventana; checkpoint/restore.
Watermark + allowed lateness: controla la ventana de eventos atrasados.
6) Patrones de transacción
6. 1 Inbox (tabla entrante)
Guardamos 'message _ id '/clave y resultado antes de efectos secundarios:pseudo
BEGIN;
ins = INSERT INTO inbox(id, received_at) ON CONFLICT DO NOTHING;
IF ins_not_inserted THEN RETURN cached_result;
result = handle(event);
UPSERT sink with result; -- idempotent sync
UPDATE inbox SET status='done', result_hash=... WHERE id=...;
COMMIT;
La repetición verá la grabación y no repetirá el efecto.
6. 2 Outbox
Un registro de negocios y un evento en una transacción → el publicador da a un corredor. No elimina la toma del consumidor, pero sí los «agujeros».
6. 3 Índices únicos/UPSERT
sql
INSERT INTO payments(id, status, amount)
VALUES ($1, $2, $3)
ON CONFLICT (id) DO NOTHING; -- "create once"
o actualización controlada de la versión:
sql
UPDATE orders
SET status = $new, version = version + 1
WHERE id=$id AND version = $expected; -- optimistic blocking
6. 4 Versificación de agregados
El evento es aplicable si 'evento. version = aggregate. version + 1`. De lo contrario - toma/repetición/conflicto.
7) Dedoop y corredores/streams
7. 1 Kafka
Idempotent Producer reduce las tomas en la entrada.
Las transacciones permiten combinar atomicamente los offsets + los registros de salida.
Compaction: almacena el último valor por clave - post-factum dedup/coalesing (no para pagos).
Lado del consumidor: state store/Redis/DB para las claves de la ventana.
7. 2 NATS / JetStream
Ack/rareza → at-least-once. Dedoup en el consumidor (Inbox/Redis).
La secuencia JetStream/durabout del consumidor facilita la identificación de repeticiones.
7. 3 Colas (Rabbit/SQS)
Visibility timeout + re-envíos → necesita clave + dedust-stor.
SQS FIFO con 'MessageGroupId '/' DeduplicationId' ayuda, pero las ventanas TTL están limitadas al proveedor - almacena las claves durante más tiempo si el negocio lo requiere.
8) Repositorios y analíticas
8. 1 ClickHouse/BigQuery
Dedoup por ventana: 'ORDER BY key, ts' y' argMax '/' anyLast 'con condición.
ClickHouse:sql
SELECT key,
anyLast(value) AS v
FROM t
WHERE ts >= now() - INTERVAL 1 DAY
GROUP BY key;
O una capa materializada de eventos «únicos» (merge por clave/versión).
8. 2 Registros/telemetría
Permitamos el dedoup approximate (Bloom) en ingest → ahorramos red/disco.
9) Re-tratamiento, retrés y backfill
Las llaves de dedoup deben revivir la réplica (TTL ≥ ventana de la réplica).
Para el backfill, utilice el espacio de claves con la versión ('key # source = batch2025') o las «ciruelas» individuales para no interferir con la ventana en línea.
Almacenar los artefactos de resultado (hash/version) - esto acelera el «fast-skip» en las repeticiones.
10) Métricas y observabilidad
'dedup _ hit _ total '/' dedup _ hit _ rate' es la proporción de tomas capturadas.
'dedup _ fp _ rate' para filtros de probabilidad.
'window _ size _ seconds' real (por telemetría late arrivals).
`inbox_conflict_total`, `upsert_conflict_total`.
`replayed_events_total`, `skipped_by_inbox_total`.
Perfiles por tenant/key/type: donde más tomas hay y por qué.
Логи: `message_id`, `idempotency_key`, `seq`, `window_id`, `action=process|skip`.
11) Seguridad y privacidad
No ponga PII en la llave; use hashes/alias.
Para la firma de impresión - HMAC (secret, canonical_payload) para evitar colisiones/falsificaciones.
Los plazos de retención de las llaves están de acuerdo con el cumplimiento (GDPR retoque).
12) Rendimiento y costo
In-proc LRU ≪ Redis ≪ SQL por latencia/costo por operación.
Redis: barato y rápido, pero tenga en cuenta el volumen de claves y TTL; chardee por 'tenant/hash'.
SQL: caro en p99, pero ofrece fuertes garantías y auditorías.
Filtros probabilísticos: muy baratos, pero posibles FP - aplicar donde el «extra SKIP» no es crítico.
13) Anti-patrones
«Tenemos Kafka exactly-once - la clave no es necesaria». Necesito - en la capa azul/de negocios.
Un TTL demasiado corto para las llaves → las réplicas/el retraso entregará la toma.
Un solo deductor global → hotspot y SPOF; no chardeado por tenant/clave.
Dedoup sólo en memoria - pérdida de proceso = onda de tomas.
Bloom para dinero/pedidos - false positivo privará a la operación legítima.
Canonización de payload inconsistente - diferentes hashes en idéntico en el sentido del mensaje.
Ignorar fuera de orden: los eventos posteriores son marcados erróneamente por las tomas.
14) Lista de verificación de implementación
- Identificar la clave natural (o compuesto/impresión).
- Establezca la ventana de dedoup y la política 'lateness'.
- Seleccione el nivel (I): edge, consumer, sink; proporcione el charding.
- Implemente Inbox/UPSERT; para subprocesos - estado clave + TTL.
- Si necesita una barrera approximate - Bloom/Cuckoo (sólo para dominios no críticos).
- Configure la compatibilidad con réplicas (TTL ≥ ventana de réplica/backfill).
- Métricas 'dedup _ hit _ rate', conflictos y lagunas de ventanas; dashboards per-tenant.
- Game Day: Timeouts/Retrays, Replay, out-of-order, Cache Falls.
- Documente la canonización de payload y la versificación de claves.
- Realice pruebas de carga en «llaves calientes» y ventanas largas.
15) Ejemplos de configuraciones/código
15. 1 Redis SETNX + TTL (barrera)
lua
-- KEYS[1] = "dedup:{tenant}:{key}"
-- ARGV[1] = ttl_seconds local ok = redis. call("SET", KEYS[1], "1", "NX", "EX", ARGV[1])
if ok then return "PROCESS"
else return "SKIP"
end
15. 2 PostgreSQL Inbox
sql
CREATE TABLE inbox (
id text PRIMARY KEY,
received_at timestamptz default now(),
status text default 'received',
result_hash text
);
-- In the handler: INSERT... ON CONFLICT DO NOTHING -> check, then UPSERT in blue.
15. 3 Kafka Streams (dedoop en ventana)
java var deduped = input
.selectKey((k,v) -> v.idempotencyKey())
.groupByKey()
.windowedBy(TimeWindows. ofSizeWithNoGrace(Duration. ofHours(24)))
.reduce((oldV,newV) -> oldV) // first wins
.toStream()
.map((wKey,val) -> KeyValue. pair(wKey. key(), val));
15. 4 Flink (keyed state + TTL, pseudo)
java
ValueState<Boolean> seen;
env. enableCheckpointing(10000);
onEvent(e):
if (!seen.value()) { process(e); seen. update(true); }
15. 5 NGINX/API gateway (Idempotency-Key en edge)
nginx map $http_idempotency_key $idkey { default ""; }
Proxy the key to the backend; backend solves deadup (Inbox/Redis).
16) FAQ
P: ¿Qué elegir: dedoup o pura idempotencia?
R: Normalmente ambos: el dedoop es un «filtro» rápido (ahorro), la idempotencia es la garantía del efecto correcto.
P: ¿Qué TTL poner?
A: ≥ el tiempo máximo posible de reintroducción + stock. Típicamente 24-72h; para las finanzas y las tareas aplazadas - días/semanas.
P: ¿Cómo procesar eventos posteriores?
R: Configure 'lateness allowed' y la señalización 'late _ event'; tardía - a través de una rama separada (recompute/skip).
P: ¿Se puede deduplicar todo el flujo de telemetría?
R: Sí, con filtros approximate (Bloom) en edge, pero tenga en cuenta FP y no aplique a los efectos críticos del negocio.
P: ¿Dedoup está interfiriendo con el backfill?
R: Separe los espacios de claves ('key # batch2025') o desactive la barrera durante el tiempo de backfill; El TTL de claves sólo debe cubrir ventanas en línea.
17) Resultados
La deduplicación es una composición: la clave correcta, la ventana y la estructura de estado + patrones transaccionales (Inbox/Outbox/UPSERT) y el trabajo consciente con el orden y los eventos atrasados. Coloque las barreras donde sea más barato, proporcione idempotencia en los azules, mida 'dedup _ hit _ rate' y pruebe las réplicas/feiles, de modo que obtendrá 'efectivamente exactly-once' sin demasiadas colas de latencia y costo.