Boas práticas de gestão de transações em operações críticas

1. Fundamentos de transações em sistemas críticos

Uma transação em banco de dados é uma unidade lógica de trabalho que deve ser executada de forma atômica, consistente, isolada e durável — as conhecidas propriedades ACID. Em operações críticas, como transferências financeiras, registro de doses de medicamentos ou atualização de estoque em logística, a violação de qualquer uma dessas propriedades pode gerar prejuízos financeiros, riscos à saúde ou falhas operacionais graves.

Transações curtas (milissegundos) minimizam contenção de locks, enquanto transações longas (segundos ou minutos) aumentam a probabilidade de deadlocks e degradação de performance. Uma operação é considerada crítica quando sua falha ou inconsistência pode causar danos irreversíveis — por exemplo, debitar o saldo de uma conta sem efetivar o crédito correspondente.

2. Escolha do nível de isolamento adequado

O nível de isolamento define como as transações enxergam modificações feitas por outras transações concorrentes. Os principais níveis e seus riscos:

  • READ COMMITTED: evita leitura suja, mas permite leituras não repetíveis e fantasmas. Adequado para operações onde pequenas inconsistências são toleráveis.
  • REPEATABLE READ: garante que leituras repetidas retornem os mesmos dados, mas ainda permite fantasmas. Indicado para relatórios financeiros.
  • SERIALIZABLE: elimina todas as anomalias, mas reduz drasticamente a concorrência. Essencial em operações de alta criticidade, como reserva de assentos em voos.

No PostgreSQL, o nível padrão é READ COMMITTED. Para operações críticas, configure:

-- Sessão atual
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;

-- Transação específica
BEGIN;
SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;
-- operações críticas
COMMIT;

No MySQL (InnoDB), o padrão é REPEATABLE READ. Para máxima consistência:

SET SESSION TRANSACTION ISOLATION LEVEL SERIALIZABLE;
START TRANSACTION;
-- operações críticas
COMMIT;

3. Controle de concorrência e deadlocks

Existem duas abordagens principais:

Lock pessimista: bloqueia explicitamente os recursos antes de usá-los. Exemplo em PostgreSQL:

BEGIN;
SELECT * FROM contas WHERE id = 123 FOR UPDATE;
UPDATE contas SET saldo = saldo - 500 WHERE id = 123;
UPDATE contas SET saldo = saldo + 500 WHERE id = 456;
COMMIT;

Lock otimista: usa versões de linha (row versioning) e verifica conflitos no commit. Exemplo com número de versão:

BEGIN;
SELECT saldo, versao FROM contas WHERE id = 123;
-- aplicação verifica versao
UPDATE contas SET saldo = saldo - 500, versao = versao + 1 
WHERE id = 123 AND versao = 10;
-- se linhas afetadas = 0, conflito detectado
COMMIT;

Para evitar deadlocks:
- Acesse recursos sempre na mesma ordem (ex: menor ID primeiro)
- Defina timeouts razoáveis: SET lock_timeout = '5s';
- Monitore deadlocks com pg_stat_database e logs do PostgreSQL

4. Gestão de erros e rollback em transações

A estrutura básica de tratamento deve garantir rollback em caso de falha:

BEGIN;
SAVEPOINT antes_operacao1;
UPDATE contas SET saldo = saldo - 1000 WHERE id = 1;
-- se falhar:
ROLLBACK TO SAVEPOINT antes_operacao1;

SAVEPOINT antes_operacao2;
UPDATE contas SET saldo = saldo + 1000 WHERE id = 2;
-- se falhar:
ROLLBACK TO SAVEPOINT antes_operacao2;

-- se tudo ok:
RELEASE SAVEPOINT antes_operacao1;
RELEASE SAVEPOINT antes_operacao2;
COMMIT;

Para auditoria, registre logs de transação:

CREATE TABLE log_transacoes (
    id SERIAL PRIMARY KEY,
    operacao TEXT,
    dados_antes JSONB,
    dados_depois JSONB,
    usuario TEXT,
    timestamp TIMESTAMPTZ DEFAULT NOW()
);

5. Transações distribuídas e consistência eventual

Em sistemas distribuídos, o two-phase commit (2PC) oferece consistência forte, mas com latência e risco de bloqueio. Alternativas incluem:

Saga pattern: divide a transação longa em etapas com compensações:

-- Etapa 1: reservar estoque
BEGIN;
UPDATE estoque SET quantidade = quantidade - 1 WHERE produto_id = 10;
COMMIT;

-- Se etapa 2 falhar:
BEGIN;
UPDATE estoque SET quantidade = quantidade + 1 WHERE produto_id = 10;
COMMIT;

Para alta disponibilidade, considere BASE (Basically Available, Soft state, Eventual consistency) em vez de ACID, aceitando inconsistências temporárias em troca de resiliência.

6. Performance e escalabilidade em transações críticas

Para minimizar contenção:

  • Mova operações não críticas para fora da transação (ex: envio de email após commit)
  • Use batch processing para atualizações em lote
  • Otimize índices: CREATE INDEX idx_contas_saldo ON contas(saldo) WHERE ativo = true;
  • Configure pool de conexões com pgbouncer ou HikariCP

Exemplo de query otimizada:

-- Ruim (varredura completa)
UPDATE contas SET saldo = saldo - 100 WHERE id = 123 AND saldo >= 100;

-- Bom (usa índice)
BEGIN;
SELECT saldo FROM contas WHERE id = 123 FOR UPDATE;
-- verificação em código
UPDATE contas SET saldo = saldo - 100 WHERE id = 123;
COMMIT;

7. Testes e validação de transações em produção

Simule concorrência com pgbench:

pgbench -c 10 -j 2 -T 60 -f transacao_critica.sql -n -P 5 meu_banco

Monitore métricas essenciais:

SELECT 
    datname,
    xact_commit,
    xact_rollback,
    deadlocks,
    conflicts
FROM pg_stat_database;

Implemente canary releases com feature flags:

-- Feature flag para novo fluxo de transação
DO $$
BEGIN
    IF current_setting('app.feature_novo_fluxo') = 'true' THEN
        -- novo fluxo de transação
    ELSE
        -- fluxo legado
    END IF;
END $$;

8. Documentação e governança de transações

Política de retry com backoff exponencial:

-- Tenta até 3 vezes com espera crescente
DO $$
DECLARE
    tentativa INTEGER := 0;
    max_tentativas INTEGER := 3;
    espera_base INTEGER := 1; -- segundos
BEGIN
    LOOP
        BEGIN
            -- transação crítica
            COMMIT;
            EXIT;
        EXCEPTION
            WHEN serialization_failure THEN
                tentativa := tentativa + 1;
                IF tentativa >= max_tentativas THEN
                    RAISE;
                END IF;
                PERFORM pg_sleep(espera_base * tentativa);
        END;
    END LOOP;
END $$;

Padrões de nomenclatura:

-- Prefixo: tx_ para transações, sp_ para stored procedures
CREATE OR REPLACE PROCEDURE sp_transferencia_entre_contas(
    p_conta_origem INT,
    p_conta_destino INT,
    p_valor NUMERIC
)
LANGUAGE plpgsql
AS $$
BEGIN
    -- corpo da transação
END;
$$;

Revise planos de execução periodicamente:

EXPLAIN (ANALYZE, BUFFERS) 
UPDATE contas SET saldo = saldo - 100 WHERE id = 123;

SELECT * FROM pg_locks WHERE NOT granted;

Referências