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 1Outros 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 acimaConstraint 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çãoComo 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çãoasync 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.