← Voltar ao blog

Lock conflict e deadlock no Firebird: como diagnosticar

Lock conflict on no wait transaction e deadlock no Firebird: como identificar a transação travada, diagnosticar a causa e evitar a reincidência.


Lock conflict e deadlock não são bugs do Firebird: são o motor recusando duas transações que tentam alterar a mesma linha ao mesmo tempo. O erro assusta porque vem acompanhado de "update conflicts with concurrent update" e de um número de transação, mas o diagnóstico é sempre o mesmo: descobrir quem segurou a linha e por quanto tempo.


Qual é a diferença entre lock conflict e deadlock?

Lock conflict é uma disputa simples: uma transação pediu a linha e a outra já estava com ela, e o modo NO WAIT transformou a espera em erro imediato. Deadlock é uma disputa circular: A espera por B e B espera por A, e o motor escolhe uma das duas para morrer — a "vítima" sempre perde.

Aspecto Lock conflict (335544345) Deadlock (335544336)
Causa Duas transações na mesma linha Ciclo de espera entre duas ou mais
Mensagem lock conflict on no wait transaction deadlock
Modo de transação NO WAIT falha na hora WAIT também ocorre
Quem perde Quem chegou depois Vítima escolhida pelo motor
Solução Esperar ou encurtar a transação Ordenar o acesso e tratar retry

As duas mensagens costumam vir com o mesmo sufixo, o que confunde na leitura do log:

-- Exemplo real de log (as duas linhas são o mesmo erro)
lock conflict on no wait transaction
- update conflicts with concurrent update
- concurrent transaction number is 48213

deadlock
- update conflicts with concurrent update
- concurrent transaction number is 48213

O sufixo concurrent transaction number is N é o presente escondido no erro: N é o MON$TRANSACTION_ID da transação concorrente, e é por ele que se começa a investigação.

Por que o erro aparece mesmo em transação WAIT?

Porque a linha foi alterada e commitada por outra transação entre a sua leitura e a sua escrita. Em modo SNAPSHOT (o antigo CONCURRENCY), a sua transação enxergou a versão antiga do registro; ao tentar gravar, o motor detecta que existe uma versão mais nova e recusa.

É o caso clássico de tela de cadastro: usuário A abre o cliente, usuário B abre o mesmo cliente, B salva primeiro, A salva depois. Não há deadlock, mas há atualização perdida em potencial — e o Firebird protege o dado recusando a segunda gravação.

Como reproduzir em dois minutos?

Duas sessões isql abertas ao mesmo tempo reproduzem o erro sem depender da aplicação. O isql mantém a transação aberta entre comandos, o que é exatamente o que se precisa.

# Sessão 1
isql -user SYSDBA -password sua_senha 10.0.0.15/3050:/dados/empresa.fdb
SQL> SET TRANSACTION NO WAIT;
SQL> UPDATE clientes SET nome = 'TESTE A' WHERE id = 1;   -- não commite

# Sessão 2
isql -user SYSDBA -password sua_senha 10.0.0.15/3050:/dados/empresa.fdb
SQL> SET TRANSACTION NO WAIT;
SQL> UPDATE clientes SET nome = 'TESTE B' WHERE id = 1;
-- lock conflict on no wait transaction

Troque NO WAIT por WAIT na sessão 2 e o comando fica pendurado até a sessão 1 commitar. Se a sessão 1 nunca commitar, o isql espera indefinidamente — e a aplicação exibe "salvando" para sempre.

Como escolher o modo de transação?

A escolha vale mais que qualquer ajuste de configuração: WAIT evita o erro imediato, NO WAIT evita fila. A regra prática é WAIT para gravação interativa e NO WAIT para rotina de lote que precisa falhar rápido.

-- Gravação interativa: espera a linha liberar em vez de falhar
SET TRANSACTION READ WRITE WAIT ISOLATION LEVEL READ COMMITTED;

-- Lote em fila: falhar rápido e deixar o retry cuidar
SET TRANSACTION READ WRITE NO WAIT ISOLATION LEVEL READ COMMITTED;

-- Relatório: nunca deve segurar nem disputar linha
SET TRANSACTION READ ONLY ISOLATION LEVEL SNAPSHOT;

Isolamento também importa. READ COMMITTED libera as versões antigas mais cedo e reduz a janela de conflito; SNAPSHOT mantém a visão estável do início da transação e, em troca, acumula versões de registro enquanto estiver aberta.

Como descobrir quem está travando o quê?

O monitor do Firebird não expõe a lista de locks linha a linha, mas expõe o que importa: quais transações estão ativas, há quanto tempo e de qual conexão. Transação ativa há 40 minutos é a resposta em quase todos os casos.

-- Transações ativas, ordenadas pelas mais antigas
SELECT a.MON$ATTACHMENT_ID        AS conexao,
       a.MON$USER                 AS usuario,
       a.MON$REMOTE_ADDRESS       AS origem,
       t.MON$TRANSACTION_ID       AS transacao,
       t.MON$TIMESTAMP            AS iniciada_em,
       t.MON$LOCK_TIMEOUT         AS timeout_lock,
       t.MON$ISOLATION_MODE       AS isolamento
FROM MON$ATTACHMENTS a
JOIN MON$TRANSACTIONS t ON t.MON$ATTACHMENT_ID = a.MON$ATTACHMENT_ID
WHERE t.MON$STATE = 1                  -- ativa (confira os valores no manual da sua versão)
ORDER BY t.MON$TIMESTAMP;

Com o transacao retornado, procure o comando que ela está executando em MON$STATEMENTS filtrando por MON$TRANSACTION_ID. Na prática, o statement costuma ser um UPDATE dentro de um laço sem COMMIT, ou uma procedure com SUSPEND que fica aberta enquanto o cliente consome o resultado.

-- Comando em execução na transação suspeita
SELECT s.MON$STATEMENT_ID, s.MON$SQL_TEXT, s.MON$STATE
FROM MON$STATEMENTS s
WHERE s.MON$TRANSACTION_ID = 48213;

Como encontrar a ordem que gera o deadlock?

Deadlock tem uma causa estrutural: transações que tocam as mesmas tabelas em ordens diferentes. Se A atualiza pedidos e depois itens, e B faz o inverso, o ciclo aparece sob carga — nunca no teste com um usuário.

-- Transação A: pedidos -> itens
UPDATE pedidos SET total = 100 WHERE id = 10;
UPDATE itens_pedido SET valor = 50 WHERE pedido_id = 10;

-- Transação B (ordem inversa): itens -> pedidos  <- ciclo garantido
UPDATE itens_pedido SET valor = 60 WHERE pedido_id = 10;
UPDATE pedidos SET total = 110 WHERE id = 10;

Padronize a ordem de acesso no sistema inteiro — por exemplo "sempre cabeçalho antes de itens" — e o deadlock deixa de ocorrer por construção. Vale documentar essa ordem; ela é uma regra de projeto, não um detalhe de implementação.

Consultar o MON$ sob pressão de incidente não é o momento ideal para escrever SQL. O Sqlify transforma a descrição em português na query pronta, com cada cláusula explicada.

Como tratar o erro na aplicação?

O único tratamento correto é reprocessar a operação inteira, não o comando que falhou. A transação foi desfeita; repetir só o UPDATE deixa o dado pela metade.

// Retry de transação inteira, com espera progressiva
function salvarComRetry(PDO $pdo, callable $operacao, int $tentativas = 3): void
{
    for ($i = 1; $i <= $tentativas; $i++) {
        try {
            $pdo->beginTransaction();
            $operacao($pdo);
            $pdo->commit();
            return;
        } catch (PDOException $e) {
            $pdo->rollBack();
            // 335544345 = lock conflict | 335544336 = deadlock
            $lock = in_array('335544345', $e->errorInfo, true)
                 || in_array('335544336', $e->errorInfo, true);
            if (!$lock || $i === $tentativas) {
                throw $e;
            }
            usleep(random_int(50, 250) * 1000); // 50-250 ms
        }
    }
}

Dois cuidados que evitam retry inútil: nunca use retry em unique key violation (repetir vai falhar igual) e limpe o estado em memória antes de repetir, senão a segunda tentativa grava valores da tentativa que falhou.

Como reduzir a incidência de forma estrutural?

  1. Transação curta. Abra depois da validação e commite antes de chamar API, enviar e-mail ou gerar PDF.
  2. Ordem de acesso fixa. Documente a ordem das tabelas e mantenha igual em todas as rotinas.
  3. READ ONLY para relatório. Consulta pesada não deve segurar versão de registro nem disputar linha.
  4. Commit em lote. Laço que atualiza 10 mil linhas em uma transação segura todo mundo por minutos; commite a cada 500.
  5. Índice no filtro do UPDATE. Sem índice, o motor varre a tabela e trava mais linhas do que precisa.
  6. DeadlockTimeout consciente. O padrão é 10 segundos; com timeout curto demais o motor declara deadlock antes de o ciclo se resolver.
# firebird.conf — tempo de detecção de deadlock, em segundos
DeadlockTimeout = 10

Depois de um incidente, rode gfix -sweep em janela de manutenção: transação longa acumulou versões de registro e a varredura de garbage collection fica atrasada, o que degrada todas as consultas.

Checklist pós-incidente

  • Anotado o concurrent transaction number do erro
  • Identificada a conexão e o usuário pela MON$
  • Verificado o tempo de vida da transação
  • Localizado o statement sem COMMIT
  • Revisada a ordem de acesso das tabelas envolvidas
  • Retry implementado na operação inteira, não no comando
  • gfix -sweep agendado após transação longa

Perguntas frequentes

Lock conflict é bug do Firebird?

Não. É o comportamento esperado de um banco com bloqueio em nível de registro: alguém está editando a mesma linha. O erro só aparece porque a transação foi aberta como NO WAIT; trocar para WAIT transforma o erro em espera.

Como saber qual transação causou o conflito?

A mensagem termina com concurrent transaction number is N. Esse N é o MON$TRANSACTION_ID; consulte MON$TRANSACTIONS e MON$STATEMENTS por ele para achar conexão, usuário e comando em execução.

Qual a diferença entre READ COMMITTED e SNAPSHOT?

READ COMMITTED vê dados commitados no momento da leitura, reduzindo a janela de conflito e liberando versões antigas. SNAPSHOT congela a visão do início da transação, o que dá consistência para relatório mas acumula versões enquanto a transação estiver aberta.

Deadlock se resolve com índice?

Parcialmente. Índice reduz a quantidade de linhas varridas e, portanto, a chance de duas transações tocarem as mesmas linhas. O que realmente elimina deadlock é a ordem de acesso consistente entre as transações.

Conclusão

Lock conflict e deadlock são sintomas de transação longa e de ordem de acesso inconsistente, não de defeito do motor. O caminho é sempre o mesmo: pegar o número da transação no erro, achar quem a segura no MON$, encurtar a transação e padronizar a ordem de UPDATE. O Sqlify gera a query a partir da descrição em português, com cada cláusula explicada — útil quando o diagnóstico exige consultar o MON$ no meio do incidente.

Leia também: