Voltar para o blog
PostgreSQLBackendDatabase

Deadlocks no PostgreSQL: causas, diagnóstico e prevenção

19 de maio de 20269 min de leiturapor Sávio Araújo

O que é um deadlock?

Um deadlock acontece quando duas ou mais transações ficam esperando uma pela outra liberar um lock, criando uma dependência circular que nenhuma consegue resolver sozinha.

Transação A aguarda um lock que B segura. Transação B aguarda um lock que A segura. Ninguém avança. O PostgreSQL detecta o ciclo e aborta uma das transações com o erro:

ERROR: deadlock detected
DETAIL: Process 12345 waits for ShareLock on transaction 67890;
        blocked by process 67890.
        Process 67890 waits for ShareLock on transaction 12345;
        blocked by process 12345.
HINT: See server log for query details.

O padrão que causa 90% dos deadlocks

O caso mais comum é simples: duas transações atualizando as mesmas linhas em ordem diferente.

-- Transação A:
UPDATE accounts SET balance = balance - 100 WHERE id = 1;  -- lock na linha 1
UPDATE accounts SET balance = balance + 100 WHERE id = 2;  -- aguarda lock na linha 2
 
-- Transação B (simultânea):
UPDATE accounts SET balance = balance - 50 WHERE id = 2;   -- lock na linha 2
UPDATE accounts SET balance = balance + 50 WHERE id = 1;   -- aguarda lock na linha 1
-- DEADLOCK: A espera por 2, B espera por 1

Outros padrões comuns

Lock de tabela acidental

-- Isso trava a tabela inteira:
UPDATE orders SET status = 'processed' WHERE 1=1;
 
-- Enquanto outra transação tenta atualizar uma linha específica:
UPDATE orders SET total = 150.00 WHERE id = 42;
-- Aguarda o lock de tabela da query acima

Constraint checks

Ao inserir ou atualizar dados com foreign keys, o PostgreSQL pode adquirir locks em linhas da tabela referenciada. Se duas transações fazem isso em ordens diferentes, deadlock.

SELECT FOR UPDATE sem ordem consistente

-- Transação A:
SELECT * FROM users WHERE id IN (1, 2) FOR UPDATE;
 
-- Transação B:
SELECT * FROM users WHERE id IN (2, 1) FOR UPDATE;
-- Pode resultar em deadlock dependendo do plano de execução

Como diagnosticar em produção

Verificando locks ativos

SELECT
  pid,
  query,
  state,
  wait_event_type,
  wait_event,
  pg_blocking_pids(pid) AS blocked_by
FROM pg_stat_activity
WHERE wait_event_type = 'Lock'
ORDER BY query_start;

Visualizando o grafo de locks

SELECT
  blocked.pid AS blocked_pid,
  blocked.query AS blocked_query,
  blocking.pid AS blocking_pid,
  blocking.query AS blocking_query
FROM pg_stat_activity blocked
JOIN pg_stat_activity blocking
  ON blocking.pid = ANY(pg_blocking_pids(blocked.pid))
WHERE blocked.wait_event_type = 'Lock';

Log de deadlocks

Configure no postgresql.conf para logar deadlocks:

deadlock_timeout = 1s        # tempo antes de checar deadlock (padrão: 1s)
log_lock_waits = on          # loga queries que esperam por locks

Estratégias de prevenção

1. Ordem consistente de locks (resolve 90% dos casos)

Sempre atualize linhas na mesma ordem em todas as transações:

async function transfer(fromId: string, toId: string, amount: number) {
  // Ordena IDs para garantir sempre a mesma ordem de lock
  const [first, second] = [fromId, toId].sort();
 
  await db.transaction(async (trx) => {
    await trx("accounts").where({ id: first }).forUpdate();
    await trx("accounts").where({ id: second }).forUpdate();
 
    await trx("accounts").where({ id: fromId }).decrement("balance", amount);
    await trx("accounts").where({ id: toId }).increment("balance", amount);
  });
}

2. Transações curtas

Quanto mais curta a transação, menor a janela de tempo em que ela segura locks. Evite I/O externo (chamadas de API, envio de email) dentro de transações.

// Ruim: chama API externa dentro da transação
await db.transaction(async (trx) => {
  await trx("orders").where({ id }).update({ status: "paid" });
  await paymentGateway.capture(chargeId); // ← não faça isso
});
 
// Bom: separa o I/O externo
await paymentGateway.capture(chargeId);
await db("orders").where({ id }).update({ status: "paid" });

3. Advisory locks para coordenação de aplicação

Para locks que não mapeiam diretamente para linhas de banco:

-- Adquire um lock advisory pelo ID numérico
SELECT pg_advisory_xact_lock(hashtext('operation:user:123'));
-- Lock é liberado automaticamente no fim da transação
async function processUser(userId: string) {
  await db.transaction(async (trx) => {
    // Lock no escopo da transação
    await trx.raw("SELECT pg_advisory_xact_lock(hashtext(?))", [`user:${userId}`]);
 
    // Operações seguras aqui
    await trx("users").where({ id: userId }).update({ /* ... */ });
  });
}

4. Retry em deadlocks

Mesmo com prevenção, deadlocks podem ocorrer. Trate com retry:

async function withDeadlockRetry<T>(
  fn: () => Promise<T>,
  maxRetries = 3
): Promise<T> {
  for (let attempt = 1; attempt <= maxRetries; attempt++) {
    try {
      return await fn();
    } catch (error) {
      const isDeadlock = error.message?.includes("deadlock detected");
      if (isDeadlock && attempt < maxRetries) {
        // Backoff exponencial com jitter
        const delay = Math.pow(2, attempt) * 50 + Math.random() * 50;
        await new Promise((resolve) => setTimeout(resolve, delay));
        continue;
      }
      throw error;
    }
  }
  throw new Error("Max retries exceeded");
}

Conclusão

Deadlocks são inevitáveis em sistemas com alta concorrência: a questão é minimizá-los e tratá-los de forma segura. A combinação de ordem consistente de locks + transações curtas + retry elimina praticamente todos os casos reais.

Quer conversar sobre esse tema?

Se você tem dúvidas, quer aplicar isso num projeto ou só quer trocar uma ideia, pode me chamar.

Falar com a SA Tech