Software · Arquitetura & Engenharia de Dados
Change Data Capture (CDC) e Outbox Pattern: Sincronização em Tempo Real sem Falhas
Como sincronizar microsserviços, réplicas analíticas e motores de busca através da leitura de logs transacionais de banco de dados e do padrão Transactional Outbox.
Neste artigo
Em sistemas modernos baseados em microsserviços e arquiteturas orientadas a eventos, o banco de dados relacional primário raramente é o único consumidor da informação. Quase toda operação de escrita em uma entidade de negócio (como a confirmação de um pedido ou a atualização de um cadastro de cliente) exige que outros sistemas downstream sejam notificados em tempo real: o motor de busca (Elasticsearch) precisa atualizar seus índices, o cache em memória (Redis) precisa invalidar chaves antigas, o Data Warehouse analítico necessita ingerir a nova linha e os microsserviços dependentes (faturamento, estoque e logística) devem reagir imediatamente ao evento.
No entanto, como garantir que essas mutações de dados sejam propagadas para o ecossistema com confiabilidade absoluta, sem sobrecarregar o banco de produção com consultas repetitivas e sem gerar inconsistências silenciosas de dados?
A resposta da engenharia moderna combina duas abordagens complementares: o Change Data Capture (CDC) e o Transactional Outbox Pattern.
O Pesadelo das Escritas Duplas (Dual Writes)
A abordagem mais intuitiva — e mais perigosa — adotada por equipes inexperientes é a chamada escrita dupla na aplicação:
// Anti-pattern da Escrita Dupla (Falta de Atomicidade Distribuída)
await _database.SaveOrderAsync(order); // 1. Grava no banco de dados local
await _messageBus.PublishAsync(order); // 2. Publica evento no Kafka / RabbitMQ
Essa construção parece inofensiva em testes locais, mas em produção ela é uma fonte inesgotável de corrupção de dados:
- E se o banco de dados gravar o pedido com sucesso, mas a rede oscilar antes da publicação no broker de mensageria? O pedido existirá no banco, mas nenhum evento será emitido: o estoque não será reservado e o faturamento jamais ocorrerá.
- Se você inverter a ordem e publicar a mensagem antes, o broker recebe o evento, mas uma restrição de chave única no banco pode abortar a transação local logo em seguida. Os microsserviços downstream processarão um pedido fantasma que nunca existiu.
Como não é viável nem recomendado utilizar transações distribuídas lentas de duas fases (Two-Phase Commit - 2PC) na nuvem, a aplicação nunca deve tentar escrever em dois sistemas independentes no mesmo fluxo síncrono.
O Padrão Transactional Outbox: Atomicidade Local
Para garantir consistência sem transações distribuídas, a engenharia adota o Transactional Outbox Pattern:
graph TD
App["<b>Aplicação / Serviço de Pedidos</b>"]
subgraph Tx["Mesma Transação ACID Local (PostgreSQL)"]
direction TB
Op1["1. INSERT INTO orders (id, total, status...)"]
Op2["2. INSERT INTO outbox_events (id, event_type, payload...)"]
Op1 --- Op2
Commit["<b>COMMIT ATÔMICO</b><br/>Ou ambos são gravados, ou nenhum é gravado."]
Op2 --> Commit
end
App --> Tx
classDef app fill:#0f172a,stroke:#64748b,stroke-width:1px,color:#f8fafc;
classDef tx fill:#0d1526,stroke:#38bdf8,stroke-width:2px,color:#f8fafc;
classDef commit fill:#0f2724,stroke:#34d399,stroke-width:2px,color:#f8fafc;
class App app;
class Tx tx;
class Commit commit;
A aplicação não publica mensagens na rede diretamente. Ela grava a alteração da entidade de negócio e o evento correspondente em uma tabela auxiliar dedicada (outbox_events) dentro da exata mesma transação ACID do banco de dados relacional. A gravação do evento no banco é garantidamente atômica e instantânea.
Change Data Capture (CDC): Lendo o Log Transacional
Agora que o evento reside com segurança na tabela de Outbox do banco de dados, como ele é transmitido para os tópicos do Apache Kafka ou RabbitMQ?
Historicamente, as empresas utilizavam Polling: um worker agendado executava a cada 5 segundos uma consulta do tipo SELECT * FROM outbox_events WHERE processed = false LIMIT 100. Essa abordagem impõe alto consumo contínuo de I/O nas tabelas do banco e introduz latência perceptível.
O Change Data Capture (CDC) revoluciona essa extração: em vez de executar consultas SQL contra as tabelas, o motor de CDC conecta-se diretamente ao log de transações bruto do banco de dados (o Write-Ahead Logging - WAL no PostgreSQL ou o Binlog no MySQL):
graph TD
DB["<b>PostgreSQL / MySQL</b><br/>(Mutações gravadas no disco)"] --> WAL["<b>Log de Transações (WAL / Binlog)</b>"]
WAL -->|Leitura de baixo nível sem locks nem queries| CDC["<b>Conector CDC (Debezium / Kafka Connect)</b>"]
CDC --> KAFKA["<b>Apache Kafka (Tópicos de Streaming)</b>"]
KAFKA --> ES["Elasticsearch"]
KAFKA --> Cache["Cache Redis"]
KAFKA --> DW["Data Warehouse"]
KAFKA --> MS["Microsserviços Downstream"]
classDef db fill:#0f172a,stroke:#38bdf8,stroke-width:2px,color:#f8fafc;
classDef cdc fill:#132338,stroke:#4ade80,stroke-width:2px,color:#f8fafc;
classDef dest fill:#1e293b,stroke:#64748b,stroke-width:1px,color:#f8fafc;
class DB,WAL db;
class CDC,KAFKA cdc;
class ES,Cache,DW,MS dest;
Por Que o CDC com Debezium é Tão Poderoso?
- Sobrecarga Quase Nula no Banco: Como o conector lê apenas o fluxo sequencial de bytes do arquivo de log do WAL, as tabelas de produção não sofrem contenção de leitura, travamentos de linha ou picos de CPU.
- Latência de Milissegundos: Assim que a transação efetua o commit no disco, o evento de mudança é detectado pelo conector de CDC e enviado para o Kafka em tempo quase real.
- Captura Fiel de Mudanças: O CDC registra não apenas o estado final, mas cada mutação atômica (
CREATE,UPDATE,DELETE), incluindo os valores das colunas antes e depois da alteração.
Garantia de Entrega e Idempotência nos Consumidores
A combinação de Transactional Outbox e CDC com Debezium e Kafka oferece uma garantia matemática de entrega do tipo Pelo Menos Uma Vez (At-Least-Once Delivery):
- Sob nenhuma circunstância uma mensagem confirmada no banco será esquecida ou perdida.
- No entanto, em cenários de reinicialização de conectores ou oscilações de rede, uma mensagem pode ocasionalmente ser retransmitida em duplicidade.
Por isso, a ponta consumidora deve ser obrigatoriamente idempotente:
- O consumidor armazena o identificador único do evento (
eventId) em uma tabela de controle local. - Se um evento já processado for recebido novamente, o consumidor o descarta imediatamente, preservando a integridade dos dados downstream.
Boas Práticas para CDC em Alta Escala
Em arquiteturas distribuídas com CDC, a sincronização de dados costuma seguir diretrizes consolidadas de engenharia:
- Transactional Outbox com Debezium: Abolimos completamente as escritas duplas manuais em código. Toda sincronização de estado inter-serviços transita pela tabela de Outbox capturada de forma não invasiva pelo Debezium no PostgreSQL.
- Compactação de Tópicos no Kafka: Para eventos que representam o estado completo de entidades, configuramos os tópicos do Kafka com a política de compactação de logs (Log Compaction). O Kafka mantém sempre a versão mais recente de cada chave de registro, permitindo que novos microsserviços reconstruam seu estado local a qualquer momento.
- Limpeza Automatizada da Tabela de Outbox: As linhas já capturadas e confirmadas no Kafka são purgadas da tabela de outbox de maneira incremental e em lotes controlados, evitando o inchaço (bloat) do banco de dados relacional.