DadosAvançado9 min de leitura6 seções1978, Data aproximada

Two-Phase Commit (2PC)

Como fazer vários bancos commitarem uma transação juntos — ou nenhum. Um coordenador pergunta 'estão prontos?' (prepare), e só manda 'commit' se todos prometeram. Dá atomicidade entre sistemas, ao preço de recursos travados durante todo o protocolo e de um coordenador que, se cair na hora errada, deixa todo mundo travado sem saber o que fazer.

Em 10 segundos

2PC faz vários bancos commitarem como um só: um coordenador pergunta 'prontos?' (prepare), cada participante trava e promete, e só então vem o 'commit'. Entrega atomicidade real entre sistemas — e cobra caro por ela: recursos ficam travados o protocolo inteiro, e um coordenador que cai entre as fases deixa participantes travados 'in-doubt'. É por causa desses custos que sistemas distribuídos modernos preferem a saga, que troca atomicidade por compensação.

Antes de ter nome

O problema do acordo atômico entre nós já rondava os sistemas de transação distribuída do início dos anos 1970; Gray deu a ele o nome e a forma que ficaram.

  • #distribuido
  • #transacao
  • #atomicidade
  • #coordenacao
  • #2pc
Analogia

O 'sim' no altar

O celebrante não declara o casamento e depois pergunta se cada um aceita. Ele primeiro pergunta a cada um — 'você aceita?' — e só declara quando os dois já disseram sim. Se um hesita, ninguém está casado. O prepare é essa pergunta: cada participante promete o sim antes de qualquer coisa ser oficial. E, como no altar, se o celebrante desmaiar entre o 'sim' de um e a declaração final, os dois ficam num limbo constrangedor esperando alguém dizer o que vale — o famoso estado in-doubt.

01Conceito

A janela travada e o coordenador que cai

Entre o prepare e o commit, cada participante que votou sim mantém os recursos travados: ele prometeu commitar e precisa poder cumprir. Essa janela dura o tempo da rodada inteira — o participante mais lento define o ritmo, e ninguém mais toca aquelas linhas até a decisão.

O ponto fatal é o coordenador cair depois de coletar os votos e antes de mandar a decisão. Os participantes ficaram in-doubt: prometeram, travaram, e agora esperam uma ordem que não vem. Eles não podem abortar (talvez o coordenador tenha decidido commit) nem commitar (talvez tenha sido abort). Ficam travados.

É por isso que se diz que o 2PC é um protocolo bloqueante: existe um estado do qual os participantes não conseguem sair sozinhos. Variações como o 3PC tentam reduzir esse bloqueio adicionando uma fase, mas trocam por outras suposições (sobre tempo e falhas) que raramente se sustentam na prática — e por isso quase ninguém usa.

Ligando ao CAP: o 2PC escolhe consistência em detrimento de disponibilidade. Basta um participante indisponível para a transação inteira não poder concluir — a disponibilidade do conjunto é o produto das disponibilidades individuais, então ela só cai à medida que você adiciona participantes. Num sistema de três serviços com 99,9% cada, o 2PC entre eles já vale menos que qualquer um sozinho.

Daí a preferência moderna pela saga: em vez de travar tudo e commitar junto, cada passo commita localmente na hora e, se um passo posterior falhar, os anteriores são desfeitos por ações de compensação (estornar a cobrança, liberar a reserva). A saga abre mão da atomicidade instantânea — existe um intervalo em que o sistema está parcialmente aplicado — em troca de não travar recurso nenhum e de sobreviver a participantes que falham. É o trade-off central de transações distribuídas: atomicidade travada (2PC) × consistência eventual por compensação (saga).

Como se encaixa
  1. Coordenador
  2. pergunta a cada participanteFase 1: prepare
  3. cada um trava e prometeTodos votam sim
  4. coordenador decideFase 2: commit
  5. todos aplicam e liberamAplicado em todos
Do prepare até o commit, os recursos ficam travados em todos os participantes. Se o coordenador cair entre as duas fases, quem votou sim fica in-doubt, travado sem saber a decisão.
02No mundo real

Como isso aparece na prática

Banco e fila num commit só

Cenário

Um serviço precisa gravar um pedido no banco e publicar um evento numa fila, de forma que nunca aconteça um sem o outro. Uma falha entre as duas ações deixaria o sistema inconsistente.

Como o padrão entra

Como banco e broker suportam XA, um coordenador conduziu o 2PC entre os dois recursos, dentro do mesmo processo e da mesma rede de baixa latência.

Trade-off: Funciona porque são dois participantes confiáveis e próximos. Ainda assim, muitos times preferem o padrão Outbox aqui — grava tudo no banco numa transação local e publica depois — justamente para não depender de XA.

O 2PC entre microsserviços que travou o checkout

Cenário

Um time tentou garantir atomicidade entre os serviços de estoque, pagamento e nota fiscal com 2PC. Numa instabilidade de rede, o serviço de nota ficou lento e travou reservas de estoque em toda a plataforma.

Como o padrão entra

O 2PC foi substituído por uma saga: cada serviço commita seu passo localmente, e uma falha adiante dispara compensações (liberar a reserva, estornar a cobrança) em vez de segurar locks.

Trade-off: Passou a existir uma janela em que o pedido está parcialmente processado antes de compensar ou concluir. Em troca, um serviço lento não trava mais os outros, e a disponibilidade deixou de ser refém do participante mais fraco.

2PC entre microsserviços

Compila, roda, passa no code review. E quebra depois.

Alguém quer que três serviços 'aconteçam juntos' e alcança o commit distribuído para conseguir atomicidade. Parece a escolha correta e rigorosa — até a primeira instabilidade de rede, quando os locks distribuídos transformam um serviço lento numa parada geral e um coordenador caído deixa recursos travados sem ninguém para destravá-los.

// "So preciso que estas tres coisas aconteçam juntas."
async function confirmarPedido(pedido) {
  const tx = await coordenador.iniciar(); // 2PC entre 3 servicos
  await estoque.reservar(tx, pedido.itens);   // participante 1: TRAVA linhas
  await pagamentos.cobrar(tx, pedido.total);   // participante 2: TRAVA conta
  await notas.emitir(tx, pedido);              // participante 3: TRAVA sequencia
  await coordenador.commit(tx); // so agora os locks sao liberados
}

// Os locks ficam presos do "prepare" ate o "commit" — a duracao
// da chamada MAIS LENTA das tres, somada a latencia de rede.
//
// Agora o coordenador cai depois do prepare e antes do commit.
// Os tres participantes ficam "in-doubt": travados, sem saber se
// commitam ou abortam, esperando um coordenador que nao volta.
// O estoque daquele item esta congelado. Ninguem mais compra.

O que quebra

  • Um participante fica lentoOs locks do prepare ficam presos pelo tempo do mais lento, e a latência do conjunto vira a soma das piores — a plataforma inteira arrasta.
  • O coordenador caiOs participantes ficam in-doubt: travados, sem poder commitar nem abortar, esperando uma decisão que não chega até o coordenador voltar.
  • Mais serviços entramA disponibilidade da transação é o produto das disponibilidades de cada participante, então cada serviço novo a derruba um pouco mais.

A saída: Entre serviços independentes, troque atomicidade travada por consistência eventual: use uma saga. Cada passo commita localmente na hora, e uma falha adiante dispara compensações que desfazem os passos anteriores — ninguém segura lock esperando os outros, e um serviço fora do ar não congela o resto.

04Cuidado

Onde isto dá errado

  1. Usar 2PC entre microsserviços

    É a tentação clássica de quem quer atomicidade entre serviços. Os locks distribuídos e o coordenador único derrubam a disponibilidade e acoplam serviços que deveriam ser independentes. Quase sempre a resposta certa é uma saga.

  2. Ignorar o estado in-doubt

    Um coordenador que cai entre as fases deixa participantes travados sem saber a decisão. Sem um coordenador com log durável e recuperação, esses recursos ficam presos até intervenção manual — e ninguém planejou para isso.

  3. Subestimar a duração dos locks

    Os recursos ficam travados do prepare ao commit, não por um instante. Sob latência de rede ou um participante lento, essa janela vira gargalo de concorrência e o throughput despenca.

  4. Achar que mais participantes é de graça

    Cada participante adicional multiplica a chance de a transação não concluir: basta um indisponível para tudo parar. A disponibilidade do 2PC cai a cada nó que entra, não sobe.

05Implementação

As duas fases, e onde mora o perigo

// 2PC: um coordenador conduz duas fases entre os participantes.
// Fase 1 (prepare): "voce consegue commitar? trave e prometa."
// Fase 2 (commit):  "todos disseram sim -> apliquem de verdade."

async function doisFases(coord: Coordenador, parts: Participante[]) {
  // FASE 1 — prepare: cada um trava recursos e vota
  const votos = await Promise.all(
    parts.map((p) => p.prepare()), // retorna "sim" (pronto) ou "nao"
  );

  if (votos.every((v) => v === "sim")) {
    // FASE 2a — todos prometeram: nao ha volta, commita em todos
    await Promise.all(parts.map((p) => p.commit()));
  } else {
    // FASE 2b — alguem recusou: aborta em todos
    await Promise.all(parts.map((p) => p.abort()));
  }
}

// Entre o prepare e o commit os recursos ficam TRAVADOS.
// Se o coordenador morrer nessa janela, os participantes ficam
// "in-doubt" — travados sem saber a decisao. Esse e o calcanhar
// do 2PC, e o motivo de sagas existirem.
06Decisão

Quando usar × quando evitar

Use quando

3
  • Atomicidade inegociável entre sistemas, com passos curtos.
  • Dentro de uma fronteira confiável e de baixa latência.
  • Entre recursos que já falam XA, como um banco e um broker.

Evite quando

2
  • Entre microsserviços independentes — prefira saga.
  • Quando os passos são longos ou dependem de terceiros.

Testar o que ficou

4 armadilhas de Two-Phase Commit (2PC) viram perguntas, com o nome do padrão escondido, misturadas com padrões parecidos.

Onde isto aparece de verdade

Você já usa isto, talvez sem saber o nome.

  • transações XAO padrão que coordena o commit entre bancos e filas de mensagens diferentes através de um coordenador — 2PC com outro nome.
  • PREPARE TRANSACTION do PostgresO comando que deixa uma transação pronta para o commit de duas fases, aguardando a decisão final do coordenador.
  • MSDTC no mundo .NETO coordenador de transações distribuídas da Microsoft, que orquestra o prepare e o commit entre os recursos participantes.

O padrão em uma linha

Sem nome de domínio, sem cerimônia. Só a ideia.

// Prepare em todos; depois commit ou abort.
await coordenador.prepare(participantes);

O que ele cobra

2 saltos a mais entre a chamada e o efeito.

  • Trava os recursos de todos os participantes do prepare até o commit — a duração da chamada mais lenta, somada à rede
  • Um coordenador que cai entre as fases deixa participantes 'in-doubt', travados sem saber se commitam ou abortam
  • A disponibilidade despenca: basta um participante fora do ar para a transação inteira não poder concluir

Não vale se: os passos toleram acontecer em momentos diferentes e ser desfeitos por compensação. Aí uma saga entrega o mesmo objetivo sem travar recurso nenhum.

Costuma ser confundido com

Onde isto aparece

Continue por aqui