Idempotência e Deduplicação
Event ID e banco de controle
Criando tabela de deduplicacao com garantia transacional.
Nesta aula você vai
- Persistir Event ID para bloquear reprocessamento duplicado
- Garantir atomicidade entre deduplicacao e efeito de negocio
- Implementar expiracao segura da tabela de controle
Event ID e banco de controle
Objetivos
- Persistir Event ID para bloquear reprocessamento duplicado
- Garantir atomicidade entre deduplicação e efeito de negócio
- Definir retenção da tabela de controle
Pré-requisitos
- Aula
problema-mensagens-duplicadasconcluida. - Banco do
payment-serviceacessivel (PostgreSQL ou MySQL do projeto). - Permissao para executar migration no servico.
Conceito
Deduplicacao confiavel exige armazenamento duravel. Se a aplicacao reinicia, memoria local e perdida e o mesmo evento pode ser aceito novamente.
O padrao classico e registrar o event_id em tabela com chave unica dentro da mesma transacao do efeito de negocio.
Estrutura de arquivos
services/payment-service/migrations/20260703_create_processed_events.sqlservices/payment-service/internal/idempotency/store.goservices/payment-service/internal/consumers/order_created_consumer.godocs/architecture/idempotencia.md
Passo a passo com codigo
- Criar migration para tabela de controle:
CREATE TABLE processed_events (
event_id VARCHAR(64) PRIMARY KEY,
consumer_name VARCHAR(80) NOT NULL,
processed_at TIMESTAMP NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_processed_events_processed_at
ON processed_events(processed_at);
- Implementar tentativa de reserva do evento:
func (s *Store) TryReserve(ctx context.Context, tx *sql.Tx, eventID, consumer string) (bool, error) {
query := `INSERT INTO processed_events (event_id, consumer_name) VALUES ($1, $2) ON CONFLICT DO NOTHING`
res, err := tx.ExecContext(ctx, query, eventID, consumer)
if err != nil {
return false, err
}
rows, _ := res.RowsAffected()
return rows == 1, nil
}
- Usar no consumidor antes do efeito de negocio:
ok, err := idempotencyStore.TryReserve(ctx, tx, event.EventID, "payment.order-created")
if err != nil {
return err
}
if !ok {
logger.Infow("duplicate ignored", "event_id", event.EventID)
return nil
}
- Criar rotina de limpeza para eventos antigos:
DELETE FROM processed_events
WHERE processed_at < NOW() - INTERVAL '7 days';
Como testar
- Execute migration do
payment-service. - Publique o mesmo
order.createdduas vezes. - Verifique logs: primeira mensagem processa, segunda cai em
duplicate ignored. - Consulte a tabela: apenas um registro para o
event_id. - Rode teste automatizado de concorrencia para validar chave unica.
Dicas
- Garanta indice unico no banco, nao so no codigo.
- Use transacao unica para idempotencia e regra de negocio.
- Defina janela de retencao alinhada a replay esperado.
- Nomeie
consumer_namepara diferenciar handlers.
Erros comuns
- Inserir
event_iddepois de cobrar pagamento. - Fazer deduplicacao em cache local apenas.
- Apagar registros cedo demais e perder protecao.
- Nao testar corridas entre replicas.
Resumo
Com tabela de controle e chave unica, o eventId vira barreira duravel contra reprocessamento, inclusive em ambiente com replicas e reinicios.