Idempotência e Deduplicação

Event ID e banco de controle

Criando tabela de deduplicacao com garantia transacional.

Avançado 35 min 30 pontos Leitura 0%

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-duplicadas concluida.
  • Banco do payment-service acessivel (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.sql
  • services/payment-service/internal/idempotency/store.go
  • services/payment-service/internal/consumers/order_created_consumer.go
  • docs/architecture/idempotencia.md

Passo a passo com codigo

  1. 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);
  1. 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
}
  1. 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
}
  1. Criar rotina de limpeza para eventos antigos:
DELETE FROM processed_events
WHERE processed_at < NOW() - INTERVAL '7 days';

Como testar

  1. Execute migration do payment-service.
  2. Publique o mesmo order.created duas vezes.
  3. Verifique logs: primeira mensagem processa, segunda cai em duplicate ignored.
  4. Consulte a tabela: apenas um registro para o event_id.
  5. 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_name para diferenciar handlers.

Erros comuns

  • Inserir event_id depois 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.