Exactly-once vs At-least-once
1) Porquê discutir semânticos
A semântica de entrega determina a frequência com que o destinatário verá a mensagem em casos de falhas e retalhos:- At-se-once - sem repetição, mas pode ser uma perda (raramente aceitável).
- At-least-once - não perdemos, mas pode ser duplicado (default da maioria dos corretores/filas).
- Exactly-once - Cada mensagem é processada exatamente uma vez em termos de efeitos observados.
Verdade chave: em um mundo distribuído, sem transações globais e coerência sincronizada «puro» end-to-end exactly-once não é possível. Construímos efetivamente exactly-once, permitimos repetições nos transportes, mas fazemos o processamento de forma que o efeito observado seja «como se fosse uma vez».
2) Modelo de falha e onde as duplicações surgem
As repetições aparecem por causa de:- Perdas ack/commit (produtor/corretor/consumer «não ouviu» confirmação).
- Reapresentações de líderes/réplicas, restaurações após quebras de rede.
- Temporizações/retrações em qualquer área (kliyent→broker→konsyumer→sink).
Consequência: não se pode confiar na «exclusividade do transporte». Gerenciamos os efeitos de gravar no banco de dados, cancelar dinheiro, enviar e-mails, etc.
3) Exactly-once em fornecedores e o que é realmente
3. 1 Kafka
Dá tijolos:- Idempotent Producer (`enable. idempotence = true ') - Impede as duplicações no lado do produtor em retais.
- Transações - Atômico publicam mensagens em várias partituras e comensais de consumo (pattern read-processo-write sem «omissões»).
- Competition - armazena o último valor da chave.
Mas «fim da cadeia» (BB/pagamento/correio) ainda requer idempotidade. Caso contrário, a tomada do processador vai causar efeitos.
3. 2 NATS / Rabbit / SQS
O padrão é at-least-once com ack/redelivery. Exactly-once é alcançado ao nível da aplicação: chaves, deadup stor, upsert.
Conclusão: exactly-once transporte ≠ efeito exactly-once. Este último é feito no processador.
4) Como construir efetivamente exactly-once sobre at-least-once
4. 1 Chave Idempotente (idempotency key)
Cada comando/evento traz a chave natural: 'payment _ id', 'order _ id # step', 'saga _ id # n'. Processador:- A verificar «já viste?» - Redis/BD com TTL/Retensor.
- Se viu, repete o resultado calculado anteriormente ou faz 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 na base de dados (sink idimpotente)
As gravações são feitas através do UPSERT/ON CONFLICT, verificando a versão/valor.
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 transacionado/Inbox
Outbox: transação de negócios e gravação de eventos para publicação ocorrem em uma transação de banco de dados. O publisher de fundo lê o outbox e envia para o corretor → não há discrepância entre o estado e o evento.
Inbox: Para os comandos de entrada, salvamos 'mensagem _ id' e o resultado até a execução; reaproveitamento vê gravação e não repete efeitos colaterais.
4. 4 Processamento de cadeia consistente (read→process→write)
Kafka: A transação «leu o ofset → gravou os resultados → Comit» em um único bloco atômico.
Sem transações: «Primeiro anote o resultado/Inbox, depois ack»; com o crash, a duplicação verá o Inbox e termina no-op.
4. 5 SAGA/compensação
Quando a idimpotência não é possível (o provedor externo descontou o dinheiro), usamos operações de compensação (refund/void) e APIs externas idempotentes («POST» com o mesmo «Idempotency-Key» dá o mesmo resultado).
5) Quando é suficiente at-least-once
Atualizações em dinheiro/visualizações materializadas com a chave.
Contadores/métricas onde a incorporação é aceitável (ou armazenamos delta com versão).
Notificações onde a carta secundária não é crítica (é melhor colocar a chave na mesma).
Regra: Se a tiragem não alterar o significado do negócio ou facilmente descobrir at-least-once + proteção parcial.
6) Desempenho e custo
Exactly-once (mesmo «eficiente») é mais caro: gravações extras (Inbox/Outbox), armazenamento de chaves, transações, mais difícil de diagnosticar.
At-least-once é mais barato/fácil, melhor por throughput/p99.
Avalie o preço da dupla x probabilidade de duplicação vs custo de proteção.
7) Exemplos de configuração e código
7. 1 Produtor Kafka (Idempotidade + transações)
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 Consoante com Inbox (pseudocode)
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 }
O POST repetido com a mesma chave → o mesmo resultado/status.
8) Observabilidade e métricas
'duplicate _ attempts _ total' - quantas vezes apanharam a tomada (Inbox/Redis).
'idempotency _ hit _ rate' é a proporção de repetições «resgatadas» pela idimpotência.
'txn _ abort _ rate' (Kafka/BD) é a proporção de reembolsos.
'outbox _ backlog' é um atraso na publicação.
'exactly _ once _ path _ latency fnp95, p99' vs 'at _ least _ once _ path _ latency' - despesas gerais.
Auditar logs: vinculação 'mensagem _ id', 'idempotency _ key', 'saga _ id', 'attempt'.
9) Playbooks de teste (Game Days)
Retrai da produtora em temporais artificiais.
Crash entre «sink e ack»: certifique-se de que o Inbox/Upsert impede a tomada.
Entrega pere: aumentar redelivery no corretor; Verificar o Dedup.
Idempotidade das APIs externas: POST repetido com a mesma chave - a mesma resposta.
Mudança de líder/quebra de rede: verificar transações Kafka/comportamento dos consórcios.
10) Anti-pattern
«Temos um Kafka com exactly-once, então podemos sem as chaves».
No-op ack antes de gravar: Ackar, mas o sink caiu → perda.
Falta de DLQ/retrações com jitter, repetições sem fim e tempestade.
UUID aleatório em vez de chaves naturais, não há como deduzir.
Mistura Inbox/Outbox com tabelas de prod sem índices: bloqueios quentes e colas p99.
Transações de negócios sem API idumpotente em provedores externos.
11) Folha de cheque de seleção
1. Preço da dupla (dinheiro/legal/UX) vs preço de proteção (latência/complexidade/custo).
2. Há uma chave natural de evento/operação? Se não, inventa um estável.
3. O Sink suporta Upsert/versioning? Senão, Inbox + compensações.
4. São necessárias transações globais? Se não, segmente para SAGA.
5. Precisa de réplicas/retenções de longa duração? Kafka + Outbox. Precisa de RPC rápido/atraso baixo? NATS + Idempotency-Key.
6. Multi-tenência e quotas: isolamento de chaves/espaços.
7. Observabilidade: métricas de idempotency e backlog incluídas.
12) FAQ
Q: É possível alcançar um exactly-once «matemático» end-to-end?
A: Apenas em cenários estreitos com um único armazenamento e transações consistentes em todo o caminho. Em geral, não; use efetivamente o exactly-once através da idempotidade.
O que é mais rápido?
A: At-least-once. Exactly-once adiciona transações/armazenamento de chaves → acima de p99 e custo.
Onde guardar as chaves de idempotação?
A: Store rápido (Redis) com TTL ou tabela Inbox (PK = mensagem _ id). Para pagamentos - mais (dias/semanas).
Q: Como escolher as chaves TTL?
A: Mínimo = tempo máximo de reaproveitamento + reserva operacional (normalmente 24-72 h). Para as finanças, mais.
Preciso de uma chave se eu tiver a minha chave Kafka?
A: Sim. A competência reduzirá o armazenamento, mas não tornará o seu sink idumpotente.
13) Resultado
At-least-once é uma semântica básica e confiável de transporte.
Exactly-once como um efeito de negócio é alcançado ao nível do processador: Idempotency-Key, Inbox/Outbox, Upsert/versões, SAGA/compensações.
A escolha é um compromisso custo ↔ risco de duplicação ↔ facilidade de operação. Projete as chaves naturais, torne os sinks idempotantes, adicione a observação e realize regularmente o game days - então os seus piplines serão previsíveis e seguros, mesmo com a tempestade de retalhos e falhas.