Artigo Diretório
endereço do github
Há um código anotado no endereço do github , faça o download para facilitar a leitura.
Descrição do Problema
Recentemente, quando estava escrevendo um projeto, encontrei um bug. A situação é a seguinte. Temos um sistema de fluxo de dados que reporta continuamente as placas de veículos identificadas. As informações das placas de veículos relatadas precisam ser processadas da seguinte maneira
- Os registros de placas de veículos registrados são mantidos na tabela de arquivos do veículo.
- Ligue para o sistema de caso de terceiros, se deseja gerar um novo caso.
- Se um novo caso for gerado, o número de casos será atualizado.
O seguinte é provavelmente a estrutura desta tabela
Portanto, o processo acima inclui três operações de banco de dados,
- selecione 操作, selecione truck_num de truck_recored where truck_num = ?;
- Se o registro não existir, adicione uma operação TruckRecord, insira ...
- Ligue para o sistema de casos de terceiros, se for um novo caso, atualize o número de novos casos.
Para não introduzir outros problemas, determinamos provisoriamente que o sistema de casos de terceiros é um sistema perfeito e sempre retornará a resposta correta para mim perfeitamente.
A causa do impasse
Por motivos de negócios, o fluxo de dados empurra simultaneamente dois dados com o mesmo número de placa de carro, de modo que os dois threads realizaram operações de salvamento e atualização respectivamente.
Porque o nível de isolamento usado pelo mysql é de leitura repetível.
Portanto, um bloqueio de intenção de inserção será gerado ao salvar
e a atualização é necessária para atualizar um único registro, pois o registro atual não é enviado, um bloqueio de lacuna será gerado.
O bloqueio de intenção de inserção e o bloqueio de espaço comum são mutuamente exclusivos.
Solução de deadlock
Por ser uma operação de tabela única, e o mysql abriu a detecção automática da configuração de deadlock, ou a configuração das configurações de tempo limite de bloqueio, existem logs correspondentes para ver.
Para resolver a solução acima, a operação de salvamento pode ser transformada em uma nova transação por si só, para não causar deadlock.
Ou mude a lógica da seguinte maneira
- Envie ao sistema de terceiros para devolver o resultado.
- Salvar ou atualizar o registro (a única diferença é que quando o registro é encontrado para ser atualizado, ele é inserido, se não existir, mas as informações do caso que acabaram de ser retornadas já estão nas informações inseridas)
Isso evita problemas causados pela existência simultânea de bloqueio de espaço e bloqueio de intenção de inserção.
Mas a lógica acima é muito boa, a situação obviamente não é
Além do impasse
Devido à existência de mvcc, podemos selecionar um registro e não podemos encontrar o registro não confirmado, portanto, ambos os registros serão atualizados.
Para resolver esse problema. Existem várias opções.
- Ajuste o nível de isolamento do MySQL para se tornar o nível de leitura não confirmada, de modo que você possa julgar a situação que vem primeiro, mas uma vez que a transação seja revertida, isso pode levar à falha de ambos os registros ao mesmo tempo, e ajustar o nível de isolamento do MySQL afeta o mundo inteiro A operação obviamente não é uma boa ideia.
- Crie um índice exclusivo de forma que, mesmo devido à existência de mvcc, dois registros idênticos sejam inseridos simultaneamente. O mecanismo do índice exclusivo também solicitará a exceção de chaves duplicadas, mas o índice exclusivo não tem um desempenho particularmente bom porque torna a operação de inserção em uma operação de thread único. Não vou analisá-lo separadamente aqui, a lógica geral está relacionada ao buffer de alteração do mysql.
- Pois quando um truckNum estabelece um bloqueio distribuído, o alcance do bloqueio é controlado após a primeira consulta. A lógica aproximada do código é a seguinte
public void saveOrUpdate(TruckRecord record){
TruckRecord persist = selectByTruckNum(truckNum);
if(persist==null){
tryLock(record.getTruckNum());
//这里要用new transaction 去查避免mvcc的可重复读属性
persist=selectByTruckNum(truckNum);
//双重检测
if(persist==null){
//new transaction 事务传播方式
saveRecord(record)
}else{
updateRecord(record);
}
unLock(record.getTruckNum());
}
}
@Transactional(propagation = Propagation.REQUIRES_NEW)
TruckRecord selectByTruckNum(truckNum){
}
@Transactional(propagation = Propagation.REQUIRES_NEW)
void saveRecord(truckNum){
}
Desta forma, ele não entrará no estado de bloqueio após a inserção, e a única maneira de manter o registro é maximizar o controle do intervalo de bloqueio.
Quando você ingenuamente pensa que o código acima não tem poços, os poços irão naturalmente encontrar você.
Falha de transação declarativa
No código acima, usei duas novas transações. Na primeira consulta, assegurei-me de que a consulta era o registro mais recente, não a visualização antiga causada por leitura consistente. O segundo salvamento usa uma nova transação para garantir que os dados possam ser confirmados durante o período de bloqueio. Para que a primeira consulta possa ser feita a tempo.
No entanto, na estrutura do Spring Boot, as duas anotações não são eficazes.A essência da transação declarativa é usar o proxy dinâmico para permitir que a chamada do método original entre na chamada do método do proxy e execute uma série de códigos de aspecto. O método acima só pode inserir este método, portanto nenhuma nova transação é gerada.No caso de simultaneidade, dois registros idênticos ainda serão gerados devido à leitura consistente.
Maneira certa:
public void saveOrUpdate(TruckRecord record){
TruckRecord persist = selectByTruckNum(truckNum);
if(persist==null){
tryLock(record.getTruckNum());
TruckService proxy = SpringUtils.getBean(TruckService.class);
//这里要用new transaction 去查避免mvcc的可重复读属性
persist=proxy.selectByTruckNum(truckNum);
//双重检测
if(persist==null){
//new transaction 事务传播方式
proxy.saveRecord(record)
}else{
updateRecord(record);
}
unLock(record.getTruckNum());
}
}
Obviamente, devido à implementação de bloqueios distribuídos em bloqueios distribuídos, como bloqueios distribuídos redis, se o bloqueio falhar , dois registros ainda podem ser gerados em condições muito extremas. É recomendado que, para cenários onde os dados são muito precisos e a operação de inserção não é tão frequente, usar um bloqueio exclusivo ainda seja uma boa solução.
Resumindo
Este artigo é principalmente para reproduzir uma pequena cena no projeto e, em seguida, pensar sobre a situação. Claro, também deve considerar a idempotência do sistema de terceiros, a situação de nova tentativa de erro e a falha de bloqueio. Soluções corretivas e assim por diante; na verdade, não há uma resposta única para essas em sistemas diferentes, portanto, a análise caso a caso ainda é necessária.