Em ambientes de alto volume de transações, como sistemas de logística de pacotes, problemas de concorrência durante atualizações de dados são desafios comuns. Considere um cenário onde uma tarefa de triagem de pacotes possui dois campos críticos: tipo_excecao e estado. Normalmente, alterações nesses campos ocorrem em momentos distintos. No entanto, em equipamentos específicos, ambas as atualizações podem ser enviadas simultaneamente, causando uma condição de lost update (atualização perdida).
A operação padrão segue a sequência: ler o registro, modificar os campos necessários e escrever de volta no banco. Quando duas transações concorrentes executam essa sequência no mesmo registro, uma pode sobrescrever as alterações da outra, resultando em perda de dados.
Por exemplo, com o valor inicial do banco sendo 1, 0, 0, se a operação de exceção e a de conclusão ocorrerem quase simultaneamente, ambas leem esse valor. A operação de exceção define tipo_excecao como 9 e persiste (1, 9, 0). Em seguida, a operação de conclusão define estado como 1, mas usa o valor lido originalmente, escrevendo 1, 0, 1, eliminando a exceção registrada.
Abordagens para Prevenir Atualizações Perdidas
1. Bloqueio Pessimista no Banco de Dados
Utilizar cláusulas como FOR UPDATE em consultas SELECT adquire um bloqueio exclusivo na linha. Outras transações que tentraem ler ou modificar a mesma linha ficam bloqueadas até a liberação do lock. Esta abordagem serializa efetivamente o acesso ao registro.
-- Exemplo de consulta com bloqueio
SELECT tipo_excecao, estado
FROM tarefas
WHERE pacote_id = ?
FOR UPDATE;
É crucial avaliar o impacto de desempenho, garantindo que o bloqueio seja aplicado em nível de linha e que não bloqueie operações de leitura de longa duração desnecessariamente.
2. Atualização Parcial de Campos
Se a lógica de negócio permitir, atualize apenas os campos necessários em cada operação. Isso evita o conflito, pois as transações modificam colunas diferentes.
// Processando apenas a atualização do tipo de excecao
public void atualizarExcecao(Long tarefaId, Integer novoTipoExcecao) {
Tarefa tarefa = new Tarefa();
tarefa.setId(tarefaId);
tarefa.setTipoExcecao(novoTipoExcecao);
repositorioTarefa.atualizarParcial(tarefa);
}
Esta solução exige um conhecimento profundo da domínio do negócio e testes rigorosos para garantir que nenhuma dependência oculta entre campos seja violada.
3. Lock Distribuído via Redis
Utilize um lock ditsribuído (ex: com Redisson) para garantir exclusividade na execução do bloco crítico para um dado pacote. O lock é baseado em um identificador único, como o número do pacote.
String lockKey = "lock:triagem:" + numeroPacote;
RLock lock = redisson.getLock(lockKey);
try {
lock.lock(5, TimeUnit.SECONDS); // Timeout para aquisição
// Executa a lógica de negócio de leitura-modificação-escrita
processarTriagem(pacote);
} finally {
if (lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
Esta abordagem reduz a contensão ao nível de cada pacote individual, sem sobrecarregar o banco de dados. É essencial tratar exceções e garantir a liberação do lock no bloco finally.
4. Controle de Versão Otimista (CAS)
Adicione uma coluna de versão ou timestamp (versao) à tabela. Durante a atualização, inclua uma condição WHERE para verificar se a versão do registro não foi alterada desde a leitura.
-- SQL de atualização com CAS
UPDATE tarefas
SET tipo_excecao = ?, estado = ?, versao = versao + 1
WHERE id = ? AND versao = ?;
A aplicação deve ler a versão atual junto com os outros campos. Se a atualização afetar zero linhas (devido à condição de versão falhar), a operação deve ser repetida. É necessário implementar uma lógica de retentativa com limites definidos.
A escolha da técnica adequada depende dos requisitos específicos de desempenho, consistência e complexidade do sistema. Cmopreender os mecanismos de controle de concorrência é fundamental para projetar soluções robustas em aplicações transacionais.