Um pedido é gravado em um banco de dados e o pagamento em outro. As duas coisas precisam acontecer, ou nenhuma. Cada banco consegue confirmar a sua parte de forma atômica, mas nada local impede que um confirme enquanto o outro cai. O commit em duas fases (2PC) é o protocolo clássico para esse problema: vem embutido na maioria dos bancos relacionais por meio do padrão XA e é usado hoje dentro de bancos SQL distribuídos.
Ele também tem uma fraqueza famosa: se o coordenador falhar na hora errada, os participantes podem ficar presos segurando locks, sem como decidir sozinhos. Este post percorre o protocolo com um diagrama de sequência, todos os casos de falha, como os sistemas reais o implementam e quando uma saga ou um outbox é a ferramenta melhor.
Os papéis
- Coordenador, ou gerenciador de transações: conduz o protocolo e decide o resultado. Pode ser uma biblioteca dentro do seu servidor de aplicação, um serviço separado ou um componente de um banco de dados distribuído.
- Participantes, ou gerenciadores de recursos: os bancos de dados ou filas que guardam, cada um, uma parte da transação.
Cada parte mantém um log durável. As gravações no log não são um detalhe de implementação; são elas que fazem o protocolo sobreviver a quedas.
O protocolo
Fase 1: prepare.
- A aplicação faz o seu trabalho em cada participante dentro de uma transação global e depois pede ao coordenador que confirme.
- O coordenador envia PREPARE a cada participante.
- Cada participante garante que vai conseguir confirmar aconteça o que acontecer depois: grava as alterações da transação e um registro de preparação no log durável, mantém os locks e vota YES. Se não puder confirmar, por exemplo porque uma restrição falhou, vota NO e reverte.
Fase 2: commit ou abort.
- Se todos os votos forem YES, o coordenador grava uma decisão COMMIT no próprio log. Essa gravação é o ponto de commit: daí em diante a transação está confirmada, mesmo que todas as máquinas caiam um milissegundo depois.
- O coordenador envia COMMIT a cada participante. Cada um confirma, libera os locks e responde com uma confirmação de recebimento.
- Se algum participante votar NO ou não responder a tempo, o coordenador decide ABORT e envia ROLLBACK no lugar.
Um voto YES é uma promessa. Depois de votar YES, o participante abriu mão do direito de decidir sozinho: precisa confirmar se receber a ordem, mesmo depois de reiniciar, e não pode reverter por conta própria, porque o coordenador talvez já tenha decidido confirmar.
O custo
Com N participantes, um commit bem-sucedido exige duas idas e voltas a partir do coordenador e quatro mensagens por participante: PREPARE, voto, COMMIT e confirmação. Cada participante força a gravação do log em disco duas vezes, no prepare e no commit, e o coordenador força a sua decisão uma vez. Os locks ficam presos o tempo todo, então o participante mais lento decide por quanto tempo todos os outros bloqueiam trabalho conflitante.
O que acontece quando algo falha
| Falha | Quando | Resultado |
|---|---|---|
| Participante cai ou estoura o timeout | Antes de votar | O coordenador aborta a transação |
| Participante vota NO | Fase 1 | O coordenador aborta a transação |
| Participante cai | Depois de votar YES | Ao reiniciar, encontra o registro de preparação e pergunta o resultado ao coordenador, segurando os locks enquanto isso |
| Coordenador cai | Antes de gravar a decisão | Os participantes preparados esperam. Na recuperação, sem decisão no log, o coordenador aborta |
| Coordenador cai | Depois de gravar COMMIT | Na recuperação, lê o log e reenvia COMMIT |
| Partição de rede | Depois que um participante votou YES | Esse participante fica bloqueado até conseguir falar com o coordenador |
As linhas que importam são aquelas em que um participante votou YES e não consegue ouvir a decisão. Ele não pode confirmar, porque outro participante pode ter votado NO. Não pode abortar, porque o coordenador pode ter confirmado. Ele espera, segurando os locks, enquanto o coordenador estiver inacessível. Todas as outras transações que mexem nessas linhas também esperam. É isso que as pessoas querem dizer quando chamam o 2PC de protocolo bloqueante.
Transações preparadas presas na prática
O PostgreSQL implementa o lado do participante com PREPARE TRANSACTION, COMMIT PREPARED e ROLLBACK PREPARED. O recurso vem desativado por padrão (max_prepared_transactions é 0), e a documentação avisa que ele foi feito para gerenciadores de transações externos, não para aplicações. Uma transação preparada esquecida mantém os locks e impede o VACUUM de recuperar espaço.
Encontrar e resolver uma fica assim:
-- Prepared transactions waiting for a decision
SELECT gid, prepared, owner, database
FROM pg_prepared_xacts
ORDER BY prepared;
-- Only after checking the coordinator's log for the real outcome:
COMMIT PREPARED 'order-7f3a';
-- or
ROLLBACK PREPARED 'order-7f3a';
Nunca resolva uma transação preparada no chute. Se o coordenador confirmou e você reverte um participante, criou exatamente a inconsistência que o 2PC existe para evitar. O MySQL oferece o mesmo mecanismo com XA PREPARE, XA COMMIT e XA RECOVER.
Variações que aliviam a dor
- Presumed abort. Se o coordenador não tem registro de uma transação, a resposta é abortar. Isso economiza gravações de log nas transações abortadas e simplifica a recuperação; a maioria das implementações usa isso.
- Participantes somente leitura. Um participante que não alterou nada pode votar “read-only” e pular a fase 2.
- Commit em três fases. Acrescenta uma rodada de pre-commit para que os participantes consigam terminar sem o coordenador, mas só se os atrasos das mensagens forem limitados e a rede nunca se particionar. Essas premissas raramente valem, então o 3PC é sobretudo assunto de livro-texto.
- Replique o coordenador e os participantes. A solução real para o bloqueio é transformar cada papel em um grupo replicado que sobreviva à perda de uma máquina. O Spanner, do Google, roda o commit em duas fases sobre grupos Paxos exatamente por esse motivo, e outros bancos SQL distribuídos usam a mesma ideia com Raft. O commit em duas fases continua decidindo o resultado; o consenso mantém cada parte disponível.
Quando usar 2PC, e quando não usar
O 2PC serve quando todos os participantes são bancos de dados ou brokers que suportam uma etapa de preparação, você controla todos eles, as transações são curtas e você precisa de atomicidade e isolamento entre eles. Funciona mal entre serviços de outros times, entre APIs HTTP sem etapa de preparação (um provedor de pagamentos não vai manter a sua cobrança em estado preparado) e em processos de negócio que duram minutos ou dias.
| Commit em duas fases | Saga | Transactional outbox | |
|---|---|---|---|
| Consistência | Atômica; os locks garantem isolamento | Eventual, com etapas de compensação | Eventual entre um banco de dados e um broker |
| Bloqueio | Sim, enquanto uma decisão está pendente | Não | Não |
| Exige | Participantes que suportem prepare | Lógica de compensação para cada etapa | Um banco de dados mais um relay |
| Uso típico | Bancos ou shards que você opera | Fluxos de negócio entre serviços | Publicar eventos de forma confiável após uma gravação |
O diagrama do padrão saga mostra a alternativa com compensações, e o padrão transactional outbox cobre o caso mais comum em que os times recorrem ao 2PC: gravar em um banco de dados e publicar uma mensagem juntos. Muitas vezes a melhor solução é estrutural: coloque os dados que precisam mudar juntos em um único banco, ou em um único shard, como discute o diagrama de sharding de banco de dados.
O diagrama do commit em duas fases
Conteúdo do diagrama
- Application
- Coordinator
- Orders DB
- Payments DB
- phase 1: prepare
- phase 2: all votes YES
- commit()
- PREPARE
- YES (prepare record on disk)
- COMMIT
- ACK, locks released
- ACK
- ROLLBACK
- committed or aborted
Como ler:
- Quatro linhas de vida. A aplicação, o coordenador, o banco de pedidos e o banco de pagamentos. O tempo corre para baixo.
- O fragmento par é a fase 1. PREPARE vai ao mesmo tempo para os dois participantes, e cada um devolve um voto depois de gravar o registro de preparação.
- O fragmento alt é a fase 2. O ramo de cima é o caso normal: todos os votos são YES, o coordenador registra COMMIT e os dois participantes confirmam e respondem. O ramo de baixo é o caminho do abort.
- A nota marca a janela de dúvida entre um voto YES e a decisão, quando os participantes seguram locks e não podem decidir sozinhos.
- O bloco Markdown lista os casos de falha da tabela acima.
Desenhar transações distribuídas com honestidade
- Mostre as gravações no log. Um diagrama de 2PC sem “gravar o registro de preparação” e “gravar a decisão” deixa de fora a parte que o torna correto.
- Marque o ponto de commit. Uma única mensagem ou gravação de log decide o resultado; aponte para ela.
- Desenhe a janela em que tudo bloqueia. Ela é o motivo de os times evitarem o 2PC, então um diagrama que a esconde engana.
- Coloque os caminhos de falha em diagramas próprios. Uma queda do coordenador depois do prepare merece a sua própria sequência com as etapas de recuperação.
Para a notação em si, o exemplo de diagrama de sequência cobre linhas de vida, fragmentos e nomenclatura.
Monte no Archboard
- Entre com Google ou GitHub e crie um diagrama.
- Adicione quatro nós Linha de vida do pack Sequência: Application, Coordinator, Orders DB e Payments DB.
- Desenhe as mensagens como arestas, de cima para baixo: o pedido de commit, PREPARE para cada banco, os votos YES, COMMIT para cada banco e as confirmações.
- Adicione um Fragmento combinado do pack Sequência com o operador
parem volta das mensagens de preparação e um segundo Fragmento combinado com o operadoraltem volta dos ramos de commit e rollback. - Adicione uma Nota para a janela de dúvida e um bloco Markdown do pack Básico com a tabela de falhas.
O exemplo usa 7 nós de diagrama e fica dentro do plano Free.
Perguntas frequentes
Por que o commit em duas fases é chamado de protocolo bloqueante? Um participante que votou YES não pode confirmar nem abortar sozinho. Se o coordenador ficar inacessível antes de enviar a decisão, o participante espera segurando os locks, assim como toda transação que precisa dessas linhas.
Commit em duas fases é o mesmo que transação distribuída? É a forma mais comum de confirmar uma. Uma transação distribuída abrange vários recursos; o commit em duas fases é o protocolo que torna a sua confirmação atômica.
O commit em três fases resolve o problema do bloqueio? Só sob premissas que raramente valem: atrasos de mensagem limitados e nenhuma partição de rede. A solução prática é replicar o coordenador e os participantes com consenso, como faz o Spanner.
Posso usar commit em duas fases entre microsserviços? Raramente dá certo. Os serviços precisariam expor uma etapa de preparação e segurar locks uns para os outros, o que acopla a disponibilidade deles. Entre serviços, as escolhas usuais são sagas e o padrão outbox.
O commit em duas fases garante isolamento? Garante atomicidade. O isolamento vem dos locks dos participantes, que o 2PC mantém até a decisão chegar, então transações concorrentes não veem resultados parciais.