Conteúdo
Se alguma vez viu um sistema transacional congelar sob carga pesada, sabe o tipo de pânico silencioso que se instala. As consultas acumulam-se, o uso da CPU parece normal, os registos parecem normais, mas tudo está bloqueado. Deadlocks são problemas sorrateiros que não se manifestam até causarem danos reais. Eles não corrompem dados, mas matam o tempo de atividade e abalam a confiança no sistema, forçando as equipas a correr para os antecipar antes que as coisas piorem. Um deadlock ocorre quando duas ou mais transações esperam pelos bloqueios umas das outras num ciclo. O sistema reconhece este ciclo e termina uma transação para o quebrar. Em sistemas que lidam com muitas transações online, uma transação terminada frequentemente leva a tentativas repetidas, tempos limite e uma cascata de falhas vistas pelos utilizadores. Por isso, prevenir deadlocks não é apenas desejável para o desempenho; é crucial para a resiliência do sistema.\n\nEspecialistas que desenham sistemas de alto débito concordam que deadlocks geralmente não são aleatórios. Martin Kleppmann aponta que inconsistências subtis na ordem em que os bloqueios são adquiridos causam a maioria dos deadlocks. Pat Helland argumenta que sistemas que tratam a ordenação e a idempotência como preocupações primárias raramente enfrentam deadlocks persistentes. Tammy Butow acrescenta que muitas empresas subestimam como tempestades de tentativas causadas por estratégias de recuo pobres agravam os deadlocks. O conselho deles resume-se a três coisas: ordenação previsível dos bloqueios, tentativas limitadas e uma compreensão clara de como os bloqueios são concedidos.\n\nEntão, por que acontecem deadlocks? Não são acidentes. Quatro condições geralmente ocorrem em conjunto: exclusão mútua (apenas uma transação pode deter um bloqueio de cada vez), retenção e espera (transações mantêm bloqueios enquanto esperam por outros), não preempção (bloqueios não podem ser retirados à força) e espera circular (cada transação espera por um bloqueio detido por outra num ciclo). Bases de dados relacionais têm todas estas por design. Os programadores podem controlar principalmente as esperas circulares. Causas comuns incluem ordenações inconsistentes dos bloqueios, transações de longa duração, uso de níveis de isolamento excessivamente rigorosos e padrões de acesso adversos durante picos de tráfego. Por exemplo, se uma transação atualiza as linhas 1 depois 2, e outra faz 2 depois 1, podem ocorrer deadlocks quando se sobrepõem de forma errada.\n\nO verdadeiro dano dos deadlocks não é a única transação terminada; é a tempestade de tentativas que se segue. Os clientes tentam novamente imediatamente sem qualquer atraso ou aleatoriedade, causando colisões repetidas nas tentativas. Isto pode transformar-se em falhas que duram minutos. Um caso foi uma API de pagamentos onde um deadlock de 80ms causou uma interrupção parcial de cinco minutos porque as tentativas colidiram fortemente. Em grande escala, mesmo uma pequena percentagem de deadlocks pode causar uma inundação de falhas que saturam os pools de conexões. É muito mais fácil impedir que o deadlock aconteça do que conter a tempestade depois.\n\nPara prevenir deadlocks, comece por impor uma ordenação estrita e documentada dos bloqueios. Cada tipo de transação deve adquirir bloqueios na mesma ordem — por chave primária, tipo de recurso ou hierarquia. Não deixe que a entrada do utilizador ou condições dinâmicas mudem essa ordem em tempo real, ou convidará ciclos. Se escritas condicionais forem necessárias, faça primeiro uma leitura simples de metadados para escolher o caminho ordenado correto.\n\nManter as transações curtas também ajuda. Transações longas não causam deadlocks diretamente, mas aumentam a janela para conflitos. Remova consultas desnecessárias e operações dispendiosas como chamadas API dentro das transações. Trate a atomicidade como um bisturi em vez de um martelo — proteja apenas o que precisa de garantias transacionais.\n\nUse o nível de isolamento correto em vez do mais forte. O isolamento serializável pode parecer mais seguro, mas frequentemente significa mais bloqueios e maior risco de deadlocks. Escolha o nível de isolamento mais fraco que ainda proteja a correção, como isolamento por snapshot para leituras ou leitura confirmada para escritas com verificações explícitas. Maior isolamento nem sempre é melhor — apenas mais restritivo.\n\nMesmo com tudo isso, alguns deadlocks vão acontecer. Por isso, adicione sempre recuo exponencial com jitter aos ciclos de tentativa. Não tente novamente imediatamente após uma falha. Aleatorize os atrasos para que as tentativas não se acumulem e colidam novamente. Mesmo um pequeno jitter reduz drasticamente as colisões de tentativas, muitas vezes salvando-o de passar de um pequeno contratempo para um incidente grave.\n\nPor fim, acompanhe a contenção de bloqueios como uma métrica chave. A maioria das equipas observa a latência geral das consultas, mas ignora quanto tempo as consultas esperam por bloqueios. Medir o tempo de bloqueio, contagens de deadlocks e os principais bloqueadores ajuda a detetar problemas cedo antes que causem interrupções.