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?
- Transação curta. Abra depois da validação e commite antes de chamar API, enviar e-mail ou gerar PDF.
- Ordem de acesso fixa. Documente a ordem das tabelas e mantenha igual em todas as rotinas.
READ ONLYpara relatório. Consulta pesada não deve segurar versão de registro nem disputar linha.- Commit em lote. Laço que atualiza 10 mil linhas em uma transação segura todo mundo por minutos; commite a cada 500.
- Índice no filtro do
UPDATE. Sem índice, o motor varre a tabela e trava mais linhas do que precisa. DeadlockTimeoutconsciente. 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 numberdo 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 -sweepagendado 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: