Série de codificação usual: lembre-se de um pensamento sobre impasse e a solução


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

  1. Os registros de placas de veículos registrados são mantidos na tabela de arquivos do veículo.
  2. Ligue para o sistema de caso de terceiros, se deseja gerar um novo caso.
  3. Se um novo caso for gerado, o número de casos será atualizado.

O seguinte é provavelmente a estrutura desta tabela

TruckRecord Long id //truckNum 已加索引 String truckNum Long caseCount

Portanto, o processo acima inclui três operações de banco de dados,

  1. selecione 操作, selecione truck_num de truck_recored where truck_num = ?;
  2. Se o registro não existir, adicione uma operação TruckRecord, insira ...
  3. 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.

Insira a descrição da imagem aqui

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

  1. Envie ao sistema de terceiros para devolver o resultado.
  2. 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.

  1. 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.
  2. 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.
  3. 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.

Acho que você gosta

Origin blog.csdn.net/qq_33361976/article/details/109891654
Recomendado
Clasificación