Série de codage habituelle: souvenez-vous d'une pensée sur l'impasse et la solution


adresse github

Il y a du code annoté dans l' adresse github , téléchargez-le pour une lecture facile.

Description du problème

Lorsque j'écrivais un projet récemment, j'ai rencontré un bogue. La situation est la suivante. Nous avons un système de flux de données qui signalera en permanence les plaques d'immatriculation identifiées. Les informations sur les plaques d'immatriculation doivent être traitées comme suit

  1. Les enregistrements de plaque d'immatriculation enregistrés sont conservés dans la table des fichiers du véhicule.
  2. Appelez le système de cas tiers, s'il faut générer un nouveau cas.
  3. Si un nouveau cas est généré, le nombre de cas est mis à jour.

Ce qui suit est probablement la structure de ce tableau

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

Ainsi, le processus ci-dessus comprend trois opérations de base de données,

  1. sélectionnez 操作 , sélectionnez truck_num dans truck_recored où truck_num = ?;
  2. Si l'enregistrement n'existe pas, ajoutez une opération TruckRecord, insérez ...
  3. Appelez le système de cas tiers, s'il s'agit d'un nouveau cas, mettez à jour le nombre de nouveaux cas.

Afin de ne pas introduire d'autres problèmes, nous déterminons d'abord provisoirement que le système de cas tiers est un système parfait, et il me renverra parfaitement la réponse correcte à chaque fois.

La cause de l'impasse

Pour des raisons commerciales, le flux de données pousse simultanément deux données avec le même numéro de plaque d'immatriculation, de sorte que les deux threads ont respectivement effectué des opérations de sauvegarde et de mise à jour.
Parce que le niveau d'isolement utilisé par mysql est une lecture répétable.
Par conséquent, un verrou d'intention d'insertion sera généré lors de l'enregistrement
et une mise à jour est requise pour mettre à jour un enregistrement unique, car l'enregistrement actuel n'est pas soumis, un verrou d'espacement sera généré.

Le verrou d'intention d'insertion et le verrou d'écart ordinaire s'excluent mutuellement.

Insérez la description de l'image ici

Solution de blocage

Comme il s'agit d'une opération sur une seule table et que mysql a ouvert la détection automatique de la configuration du blocage, ou la configuration des paramètres de délai de verrouillage, il existe des journaux correspondants à voir.
Pour résoudre la solution ci-dessus, l'opération de sauvegarde peut être transformée en une nouvelle transaction par elle-même, afin de ne pas provoquer de blocage.
Ou changez la logique comme suit

  1. Soumettez-le au système tiers pour renvoyer le résultat.
  2. Sauvegardez ou mettez à jour l'enregistrement (la seule différence est que lorsque l'enregistrement est mis à jour, il est inséré s'il n'existe pas, mais les informations de cas qui viennent d'être renvoyées sont déjà dans les informations insérées)

Cela évite les problèmes causés par l'existence simultanée de verrous d'espacement et de verrous d'intention d'insertion.
Mais la logique ci-dessus est vraiment bonne, la situation ne l'est évidemment pas

Au-delà de l'impasse

En raison de l'existence de mvcc, nous pouvons sélectionner un enregistrement et ne pouvons pas trouver l'enregistrement non validé, les deux enregistrements seront donc mis à jour.
Pour résoudre ce problème. Il existe plusieurs options.

  1. Ajustez le niveau d'isolement de MySQL pour devenir le niveau de lecture non validée, afin que vous puissiez juger de la situation qui vient en premier, mais une fois la transaction annulée, cela peut entraîner un échec des deux enregistrements en même temps, et l'ajustement du niveau d'isolement de MySQL affecte le monde entier L'opération n'est évidemment pas une bonne idée.
  2. Créez un index unique afin que même en raison de l'existence de mvcc, deux enregistrements identiques soient insérés simultanément. Le mécanisme de l'index unique invitera également l'exception des clés en double, mais l'index unique n'est pas une performance particulièrement bonne car il transforme l'opération d'insertion en une opération à un seul thread. Je ne vais pas l'analyser séparément ici, la logique générale est liée au tampon de changement de mysql.
  3. Lorsqu'un truckNum établit un verrou distribué, la plage du verrou est contrôlée après la première requête. La logique du code approximatif est la suivante
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){
    
    
}



De cette manière, il n'entrera pas dans l'état de verrouillage après l'insertion, et la seule façon de conserver l'enregistrement est de maximiser le contrôle de la plage de verrouillage.
Lorsque vous pensez naïvement que le code ci-dessus n'a pas de fosses, les fosses vous trouveront naturellement.

Échec de la transaction déclarative

Dans le code ci-dessus, j'ai utilisé deux nouvelles transactions. Dans la première requête, je me suis assuré que la requête était le dernier enregistrement, et non l'ancienne vue causée par une lecture cohérente. La deuxième sauvegarde utilise une nouvelle transaction pour garantir que les données peuvent être validées pendant la période de verrouillage. Pour que la première requête puisse être demandée à temps.

Cependant, sous le framework Spring Boot, les deux annotations ne sont pas efficaces. L'essence de la transaction déclarative consiste à utiliser un proxy dynamique pour laisser l'appel de méthode d'origine entrer l'appel de méthode proxy et exécuter une série de codes d'aspect. La méthode ci-dessus ne peut entrer que cette méthode, donc aucune nouvelle transaction n'est générée. Dans le cas de la concurrence, deux enregistrements identiques seront toujours générés en raison d'une lecture cohérente.

La bonne façon:

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());
    }

}

Bien entendu, en raison de la mise en œuvre de verrous distribués dans les verrous distribués, tels que les verrous distribués redis, si le verrou échoue , deux enregistrements peuvent encore être générés dans des conditions très extrêmes. Il est recommandé que pour les scénarios où les données sont très précises et l'opération d'insertion n'est pas si fréquente, l'utilisation d'un verrou unique reste une bonne solution.

Pour résumer

Cet article vise principalement à reproduire une petite scène dans le projet, puis à réfléchir à la situation. Bien sûr, il doit également tenir compte de l'idempotence du système tiers, de la situation de nouvelle tentative d'erreur et de l'échec du verrou. Des solutions correctives et ainsi de suite, il n'y a en fait pas de réponse unique à ces derniers dans différents systèmes, donc une analyse au cas par cas est toujours nécessaire.

Je suppose que tu aimes

Origine blog.csdn.net/qq_33361976/article/details/109891654
conseillé
Classement