Como aplicar o padrão two-phase commit com segurança em sistemas críticos

1. Fundamentos do Two-Phase Commit (2PC) e seus desafios em sistemas críticos

O protocolo two-phase commit (2PC) é a base para garantir atomicidade em transações distribuídas que envolvem múltiplos recursos. Em sistemas críticos — como processamento financeiro, controle de estoque ou sistemas de saúde — a consistência dos dados não pode ser negociada.

O funcionamento clássico divide-se em duas fases:

  • Fase de preparação (votação): O coordenador envia uma solicitação de preparação a todos os participantes. Cada participante executa as operações localmente, persiste o estado e responde com "sim" (pronto para commit) ou "não" (abort).
  • Fase de decisão (commit/abort): Se todos responderem "sim", o coordenador envia um comando de commit global. Caso contrário, envia abort.

Os principais desafios em sistemas críticos incluem:
- Falha do coordenador durante a fase de decisão
- Participantes que ficam bloqueados aguardando resposta
- Timeouts de rede que podem levar a estados inconsistentes
- Trade-off entre consistência forte (garantida pelo 2PC) e disponibilidade (reduzida durante bloqueios)

2. Desenho robusto do coordenador: garantindo atomicidade sem ponto único de falha

O coordenador é o ponto nevrálgico do 2PC. Para evitar que sua falha comprometa todo o sistema, é essencial implementar persistência confiável do log de decisão.

Estratégia de write-ahead log (WAL):

Antes de enviar qualquer notificação aos participantes, o coordenador deve persistir em disco:
- O identificador único da transação (XID)
- A lista de participantes
- O estado atual (preparando, commit solicitado, abort solicitado)
- O timestamp da decisão

Exemplo de estrutura de log persistido:

LOG ENTRY:
  XID: TXN-20250327-001
  Phase: PREPARE_SENT
  Participants: [DB_PRIMARY, QUEUE_SERVICE, AUDIT_SERVICE]
  Timestamp: 2025-03-27T14:30:00.123Z

LOG ENTRY:
  XID: TXN-20250327-001
  Phase: COMMIT_DECIDED
  Participants: [DB_PRIMARY, QUEUE_SERVICE, AUDIT_SERVICE]
  Timestamp: 2025-03-27T14:30:01.456Z

Recuperação após crash do coordenador:

Ao reiniciar, o coordenador deve:
1. Ler o log persistido
2. Identificar transações no estado "preparando" ou "commit solicitado"
3. Para transações com decisão de commit já registrada, reenviar o comando de commit a todos os participantes
4. Para transações sem decisão registrada, enviar abort

Replicação do coordenador com quorum:

Para alta disponibilidade, utilize um algoritmo de consenso como Raft ou Paxos para replicar o estado do coordenador entre múltiplas instâncias. Um quorum de 3 coordenadores (com tolerância a 1 falha) é suficiente para a maioria dos sistemas críticos.

3. Tratamento de timeouts e falhas de comunicação na fase de preparação

Timeouts mal configurados são a causa mais comum de inconsistências em sistemas 2PC. Em sistemas críticos, adote as seguintes estratégias:

Política de retry com backoff exponencial:

Tentativa 1: timeout de 2 segundos
Tentativa 2: timeout de 4 segundos
Tentativa 3: timeout de 8 segundos
Tentativa 4 (última): timeout de 16 segundos
Após 4 tentativas: abort automático da transação

Deadlines distribuídos com tolerância a skew:

Sincronize os relógios dos servidores usando NTP com pools de alta precisão. Configure uma margem de segurança de 100ms para absorver diferenças de clock.

Abort automático vs. espera por decisão:

Em sistemas críticos, a abordagem recomendada é:
- Cada participante define um timeout máximo (ex.: 30 segundos) para receber a decisão do coordenador
- Se o timeout expirar, o participante executa um rollback local e registra o evento no log de auditoria
- O coordenador, ao se recuperar, reconcilia os estados com os participantes

4. Garantias de idempotência e compensação nas transações críticas

A idempotência é fundamental para evitar duplicação de operações em cenários de retry.

Implementação de XID único:

Cada transação deve receber um identificador global único, composto por:
- Timestamp (precisão de nanossegundos)
- Identificador do coordenador
- Número sequencial

Exemplo de formato:

XID: 20250327T143000.123456Z-COORD01-000042

Sagas como alternativa complementar:

Quando o 2PC falha de forma irreversível (ex.: participante inacessível permanentemente), utilize o padrão Saga para executar compensações:

Transação original:
  1. Débito na conta A (OK)
  2. Crédito na conta B (FALHA)

Compensação:
  1. Estorno do débito na conta A

Transações aninhadas e salvaguardas:

Para sistemas financeiros, implemente um mecanismo de "savepoint" que permite rollback parcial sem afetar operações já confirmadas em outros participantes.

5. Isolamento e concorrência: prevenindo deadlocks e inconsistências de leitura

O controle de concorrência em 2PC exige cuidado redobrado para evitar deadlocks que paralisem o sistema.

Two-Phase Locking (2PL) vs. MVCC:

  • 2PL: Adequado para operações com alta taxa de escrita, mas propenso a deadlocks
  • MVCC: Melhor para cenários de leitura intensa, mas requer gerenciamento cuidadoso de versões

Estratégias de detecção de deadlock:

Implemente o algoritmo wait-die (prevenção) ou wound-wait (detecção mais agressiva):

Wait-die: 
  - Transação mais nova espera pela mais velha
  - Se a mais velha precisar de recurso da mais nova, ela "mata" a mais nova (rollback)

Níveis de isolamento recomendados:

Para sistemas críticos, utilize Serializable como padrão. Apesar do impacto no throughput, a garantia de consistência é indispensável. Se o desempenho for crítico, considere Repeatable Read com validação explícita de conflitos.

6. Monitoramento, auditoria e testes de resiliência em produção

Métricas críticas a monitorar:

- Tempo médio de preparação (ideal: < 500ms)
- Taxa de abortos por transação (alerta se > 5%)
- Latência de commit (p95: < 2s)
- Número de transações bloqueadas (alerta se > 10)
- Tempo de recuperação do coordenador (ideal: < 5s)

Logs de auditoria imutáveis:

Cada fase da transação deve ser registrada em um log imutável (append-only):

2025-03-27T14:30:00.123Z | TXN-001 | PREPARE_SENT | DB_PRIMARY
2025-03-27T14:30:00.456Z | TXN-001 | PREPARE_OK | DB_PRIMARY
2025-03-27T14:30:00.789Z | TXN-001 | COMMIT_DECIDED | COORDINATOR
2025-03-27T14:30:01.012Z | TXN-001 | COMMIT_EXECUTED | DB_PRIMARY

Testes de caos (Chaos Engineering):

Simule regularmente:
- Queda do coordenador primário
- Perda de pacotes entre coordenador e participantes
- Atraso artificial de respostas (simulando rede lenta)
- Falha de disco em um participante

7. Casos práticos e padrões de implementação segura

Integração com PostgreSQL (prepared transactions):

O PostgreSQL oferece suporte nativo a prepared transactions via PREPARE TRANSACTION e COMMIT PREPARED. Exemplo de fluxo:

-- Participante 1: Preparação
BEGIN;
UPDATE contas SET saldo = saldo - 100 WHERE id = 1;
PREPARE TRANSACTION 'txn-001-participante1';

-- Participante 2: Preparação
BEGIN;
UPDATE contas SET saldo = saldo + 100 WHERE id = 2;
PREPARE TRANSACTION 'txn-001-participante2';

-- Coordenador decide commit
COMMIT PREPARED 'txn-001-participante1';
COMMIT PREPARED 'txn-001-participante2';

Uso de middlewares de transação distribuída:

Ferramentas como Atomikos e Narayana oferecem implementações prontas de 2PC com suporte a:
- Configuração de timeouts por transação
- Log de transações persistido
- Recuperação automática após falhas

Exemplo de coordenador 2PC com persistência e retry:

Função executarTwoPhaseCommit(XID, participantes):
    # Fase 1: Preparação
    persistirLog(XID, "PREPARE_SENT", participantes)
    respostas = []

    para cada participante em participantes:
        tentativas = 0
        sucesso = falso
        enquanto tentativas < MAX_RETRIES e não sucesso:
            tentar:
                resposta = enviarPreparacao(participante, XID)
                respostas.adicionar(resposta)
                sucesso = verdadeiro
            capturar TimeoutException:
                tentativas += 1
                esperar(backoff(tentativas))

        se não sucesso:
            persistirLog(XID, "ABORT_DECIDED", participantes)
            enviarAbortParaTodos(participantes, XID)
            retornar FALHA

    # Fase 2: Decisão
    se todas as respostas são "OK":
        persistirLog(XID, "COMMIT_DECIDED", participantes)
        para cada participante em participantes:
            tentar:
                enviarCommit(participante, XID)
            capturar Exception:
                # Registrar falha, compensação será feita na recuperação
                registrarFalhaCommit(XID, participante)
        retornar SUCESSO
    senão:
        persistirLog(XID, "ABORT_DECIDED", participantes)
        enviarAbortParaTodos(participantes, XID)
        retornar FALHA

Conclusão

A aplicação segura do padrão two-phase commit em sistemas críticos exige mais do que a implementação básica do protocolo. É necessário um design robusto do coordenador com persistência e replicação, tratamento cuidadoso de timeouts, garantias de idempotência, controle de concorrência adequado, monitoramento contínuo e testes de resiliência. Quando bem implementado, o 2PC oferece a consistência forte que sistemas financeiros, de saúde e outros domínios críticos exigem, sem comprometer a disponibilidade dentro de limites aceitáveis.

Referências