English | 简体中文 | 繁體中文 | Русский язык | Français | Español | Português | Deutsch | 日本語 | 한국어 | Italiano | بالعربية

Méthode correcte d'implémentation du verrou distribué Redis en langage Java décrite par le langage Java

Le verrou distribué a généralement trois méthodes d'implémentation :1.Le verrou optimiste de base de données;2.Le verrou distribué basé sur Redis;3.Le verrou distribué basé sur ZooKeeper. Ce blog présentera la deuxième méthode, le verrou distribué basé sur Redis. Bien que des blogs sur l'implémentation du verrou distribué Redis soient déjà nombreux sur Internet, leurs implémentations ont des problèmes de toutes sortes. Pour éviter de tromper les jeunes, ce blog détaillera comment implémenter correctement le verrou distribué Redis.

Fiabilité

Pour garantir que le verrou distribué est disponible, nous devons au moins nous assurer que l'implémentation du verrou satisfait les quatre conditions suivantes :

Mutualité. À tout moment, un seul client peut détenir le verrou.

Il ne se produira pas de verrouillage mort. Même si un client tombe en panne pendant qu'il détient le verrou sans le déverrouiller activement, il peut garantir que d'autres clients peuvent verrouiller.

Il a la capacité de tolérer les pannes. Tant que la plupart des nœuds Redis fonctionnent normalement, le client peut verrouiller et déverrouiller.

Pour déverrouiller, il faut re-verrouiller. Le verrouillage et le déverrouillage doivent être effectués par le même client, le client ne peut pas déverrouiller le verrou de quelqu'un d'autre.

Mise en œuvre du code

Dépendance du composant

Nous devons d'abord intégrer le composant open source Jedis via Maven, ajouter le code suivant au fichier pom.xml :

<dependency> 
  <groupId>redis.clients</groupId> 
  <artifactId>jedis</artifactId> 
  <version>2.9.0</version> 
</dependency> 

Code de verrouillage

Position correcte

Les paroles sont faciles à dire, montrez-moi le code. Montrez d'abord le code, puis expliquez pourquoi cela est réalisé ainsi :

public class RedisTool {
	private static final String LOCK_SUCCESS = "OK";
	private static final String SET_IF_NOT_EXIST = "NX";
	private static final String SET_WITH_EXPIRE_TIME = "PX";
	/** 
   * Essayer d'obtenir un verrou distribué 
   * @param jedis Client Redis 
   * @param lockKey Verrou 
   * @param requestId Identifiant de la demande 
   * @param expireTime Durée d'expiration 
   * @return Si l'obtention a réussi ou non 
   */
	public static Boolean tryGetDistributedLock(Jedis jedis, String lockKey, String requestId, int expireTime) {
		String result = jedis.set(lockKey, requestId, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireTime);
		if (LOCK_SUCCESS.equals(result)) {
			return true;
		}
		return false;
	}
}

On peut voir que nous ajoutons un verrou en une seule ligne : jedis.set(String key, String value, String nxxx, String expx, int time), cette méthode set() a cinq paramètres formels :

Le premier est key, nous utilisons key comme verrou, car la clé est unique.

Le second est value, nous transmettons requestId, beaucoup d'étudiants peuvent ne pas comprendre, pourquoi utiliser value en plus de la clé en tant que verrou, car nous avons mentionné que la clé est suffisante pour verrouiller ? La raison est que, comme nous l'avons expliqué dans la section de la fiabilité, le verrou distribué doit satisfaire la quatrième condition, il faut que le maître du verrou le déverrouille, en assignant la valeur requestId, nous savons quelle requête a verrouillé cette clé, et nous pouvons avoir une base pour le déverrouillage. requestId peut être généré par la méthode UUID.randomUUID().toString().

Le troisième est nxxx, ce paramètre que nous remplissons est NX, ce qui signifie SETIFNOTEXIST, c'est-à-dire que nous effectuons l'opération set si la clé n'existe pas ; si la clé existe déjà, nous ne faisons aucune opération ;

Le quatrième est expx, ce paramètre que nous transmettons est PX, ce qui signifie que nous devons ajouter un paramètre d'expiration à cette clé, et le temps spécifique est déterminé par le cinquième paramètre.

Le cinquième est time, en correspondance avec le quatrième paramètre, représentant le délai d'expiration de la clé.

En résumé, l'exécution de la méthode set() ci-dessus entraînera deux résultats possibles :1.Si le verrou actuel n'existe pas (la clé n'existe pas), alors effectuez l'opération de verrouillage, configurez un délai d'expiration pour le verrou, et utilisez value pour représenter le client qui verrouille.2.Si un verrou existe déjà, aucune action n'est effectuée.

Les étudiants attentifs auront remarqué que notre code de verrouillage satisfait les trois conditions décrites dans notre description de la fiabilité. D'abord, set() ajoute le paramètre NX, ce qui garantit que si une clé existe déjà, la fonction ne réussira pas, ce qui signifie qu'un seul client peut posséder le verrou, satisfaisant la mutualité. Deuxièmement, puisqu'il est configuré un délai d'expiration pour le verrou, même si le détenteur du verrou plante sans déverrouiller, le verrou se déverrouillera automatiquement lorsqu'il atteindra l'heure d'expiration (c'est-à-dire que la clé sera supprimée), évitant ainsi les verrous mortels. Enfin, puisqu'on assigne la valeur requestId au value, représentant l'identifiant de la requête du client qui verrouille, le client peut vérifier s'il s'agit du même client lors du déverrouillage. Étant donné que nous ne considérons que le déploiement mono-machine de Redis, nous ne prêtons pas d'attention à la tolérance aux pannes.

Exemple d'erreur1

Un exemple d'erreur couramment rencontré consiste à utiliser la combinaison jedis.setnx() et jedis.expire() pour implémenter le verrouillage, comme suit :

public static void wrongGetLock1(Jedis jedis, String lockKey, String requestId, int expireTime) {
	long result = jedis.setnx(lockKey, requestId);
	if (result == 1) {
		// Si le programme s'effondre soudainement ici, il ne peut pas définir le temps d'expiration, ce qui entraînera un verrouillage mortel 
		jedis.expire(lockKey, expireTime);
	}
}

La méthode setnx() agit comme SETIFNOTEXIST, la méthode expire() ajoute un temps d'expiration au verrou. À première vue, cela semble être le même résultat que la méthode set() précédente, mais étant donné que ce sont deux commandes Redis, elles ne sont pas atomiques. Si le programme s'effondre après l'exécution de setnx(), le verrou ne sera pas configuré avec un temps d'expiration. Alors cela entraînera un verrouillage mortel. Pourquoi quelqu'un réalise ainsi sur le web, c'est parce que les versions précédentes de jedis ne prenaient pas en charge la méthode set() avec plusieurs paramètres.

Exemple d'erreur2

Ce type d'exemple d'erreur est difficile à détecter et implémenter de manière complexe. L'idée de mise en œuvre : utiliser la commande jedis.setnx() pour verrouiller, où la clé est le verrou et la valeur est le temps d'expiration du verrou. Processus d'exécution :1.Essayer de verrouiller à l'aide de la méthode setnx() si le verrou n'existe pas, renvoyer une réussite de verrouillage.2.Si le verrou existe déjà, obtenir le temps d'expiration du verrou et comparer avec l'heure actuelle. Si le verrou a expiré, définir un nouveau temps d'expiration et renvoyer une réussite de verrouillage. Le code est comme suit :

public static Boolean wrongGetLock2(Jedis jedis, String lockKey, int expireTime) {
	long expires = System.currentTimeMillis() + expireTime;
	String expiresStr = String.valueOf(expires);
	// Si le verrou actuel n'existe pas, retournez succès de verrouillage 
	if (jedis.setnx(lockKey, expiresStr) == 1) {
		return true;
	}
	// Si le verrou existe, obtenez l'heure d'expiration du verrou 
	String currentValueStr = jedis.get(lockKey);
	if (currentValueStr != null && long.parselong(currentValueStr) < System.currentTimeMillis()) {
		// Le verrou a expiré, obtenez l'heure d'expiration du verrou précédent et définissez l'heure d'expiration actuelle du verrou 
		String oldValueStr = jedis.getSet(lockKey, expiresStr);
		if (oldValueStr != null && oldValueStr.equals(currentValueStr)) {
			// Envisageons la situation de concurrence multithreadée, seul un thread qui a la même valeur que la valeur actuelle a le droit de verrouiller 
			return true;
		}
	}
	// Dans d'autres cas, retournez toujours échec de verrouillage 
	return false;
}

Alors, où se situe le problème de ce code ?1.Étant donné que l'heure d'expiration est générée par le client lui-même, il est nécessaire de demander que les heures de chaque client synchronisées dans le réseau distribué soient synchronisées.2.Lorsque le verrou expire, si plusieurs clients exécutent simultanément la méthode jedis.getSet(), bien que finalement qu'un seul client puisse verrouiller, l'heure d'expiration du verrou de ce client peut être couverte par d'autres clients.3.Le verrou n'a pas d'identifiant propriétaire, c'est-à-dire que n'importe quel client peut déverrouiller.

Code de déverrouillage

Position correcte

D'abord, montrons le code, puis expliquons pourquoi cela a été réalisé progressivement :

public class RedisTool {
	private static final long RELEASE_SUCCESS = 1L;
	/** 
   * Libération du verrou distribué 
   * @param jedis Client Redis 
   * @param lockKey Verrou 
   * @param requestId Identifiant de la demande 
   * @return Si le libération a réussi 
   */
	public static Boolean releaseDistributedLock(Jedis jedis, String lockKey, String requestId) {
		String script = "if redis.call('get', KEYS[1]) == ARGV[}}1] alors retournez redis.call('del', KEYS[1]) else return 0 end";
		Object result = jedis.eval(script, Collections.singletonList(lockKey), Collections.singletonList(requestId));
		if (RELEASE_SUCCESS.equals(result)) {
			return true;
		}
		return false;
	}
}

On voit que pour déverrouiller, nous avons besoin de seulement deux lignes de code ! La première ligne, nous écrivons un simple script Lua, la dernière fois que nous avons vu ce langage de programmation était dans le livre 'Hackers & Painters', et soudainement, nous l'utilisons ici. La deuxième ligne, nous transmettons le script Lua à la méthode jedis.eval() et faisons en sorte que les paramètres KEYS[1] et assigner la valeur lockKey, ARGV[1] et assigner la valeur requestId. La méthode eval() transmet le code Lua au serveur Redis.

Alors, que fait ce code Lua ? En réalité, c'est très simple, d'abord, il récupère la valeur associée au verrou, vérifie si elle est égale à requestId, et si elle l'est, supprime le verrou (déverrouillage). Alors pourquoi utiliser le langage Lua ? Parce qu'il faut garantir que les opérations ci-dessus sont atomiques. Concernant les problèmes pouvant survenir en cas de non-atomicité, vous pouvez lire les informations à propos du code de déverrouillage [-Exemple d'erreur2】. Alors pourquoi l'exécution de la méthode eval() peut-elle garantir l'atomicité ? Cela provient des caractéristiques de Redis, voici une partie de l'explication de la commande eval sur le site officiel :

En termes simples, lorsque le script Lua est exécuté via la commande eval, le code Lua est traité comme une commande et Redis ne execute d'autres commandes que lorsque la commande eval est complètement exécutée.

Exemple d'erreur1

Le code de déverrouillage le plus courant consiste à utiliser directement la méthode jedis.del() pour supprimer le verrou, cette méthode de déverrouillage sans préalablement vérifier le propriétaire du verrou peut entraîner que n'importe quel client puisse déverrouiller à tout moment, même si ce verrou ne lui appartient pas.

public static void wrongReleaseLock1(Jedis jedis, String lockKey) { 
  jedis.del(lockKey); 
} 

Exemple d'erreur2

Ce code de déverrouillage à première vue ne pose pas de problème, même si j'ai presque réalisé ainsi auparavant, il est presque similaire à la bonne méthode, la seule différence étant qu'il est exécuté en deux commandes, voici le code :

public static void wrongReleaseLock2(Jedis jedis, String lockKey, String requestId) {
	// Vérifiez si le verrouillage et le déverrouillage sont effectués par le même client 
	if (requestId.equals(jedis.get(lockKey))) {
		// Si à ce moment-là, ce verrou n'appartient plus à ce client, il peut mal interpréter le verrouillage 
		jedis.del(lockKey);
	}
}

Comme les commentaires de code, le problème réside dans le fait que si l'appel à la méthode jedis.del() se produit lorsque ce verrou ne appartient plus au client actuel, il peut déverrouiller le verrou ajouté par un autre client. Alors, existe-t-il vraiment un tel scénario ? La réponse est affirmative, par exemple, si le client A verrouille, après un certain temps, le client A déverrouille, avant d'exécuter jedis.del(), le verrou expire soudainement, alors le client B essaie de verrouiller avec succès, puis le client A exécute la méthode del(), il déverrouillera le verrou du client B.

Résumé

Cet article présente principalement comment implémenter correctement un verrou de distribution Redis en utilisant le code Java, et donne également deux exemples d'erreurs classiques pour verrouiller et déverrouiller. En réalité, il n'est pas difficile de réaliser un verrou de distribution Redis, il suffit de garantir que les quatre conditions de la fiabilité puissent être satisfaites.

Dans quelles scénarios le verrou de distribution est principalement utilisé ? Les endroits nécessitant une synchronisation, par exemple, l'insertion d'une ligne de données nécessite un contrôle préalable de la base de données pour savoir s'il existe des données similaires. Lorsque plusieurs demandes insèrent simultanément, il peut se produire que toutes les demandes retournent que la base de données n'a pas de données similaires, et toutes peuvent être ajoutées. Dans ce cas, une synchronisation doit être effectuée, mais verrouiller directement la table de base de données est trop coûteux en temps, donc nous utilisons un verrou de distribution Redis, où seules les threads peuvent effectuer l'opération d'insertion de données, et les autres threads attendent.

Voici le contenu complet de cet article sur la manière correcte d'implémenter un verrou de distribution Redis pour le langage Java, j'espère que cela vous sera utile. Les amis intéressés peuvent continuer à consulter d'autres sujets pertinents sur ce site. Si vous trouvez des insuffisances, n'hésitez pas à laisser un message. Merci de votre soutien à ce site !

Déclaration : le contenu de cet article est extrait du réseau, et appartient au propriétaire original. Le contenu est apporté par les utilisateurs d'Internet et téléchargé spontanément. Ce site n'appartient pas au propriétaire, n'a pas été édité par l'homme, et n'assume aucune responsabilité juridique. Si vous trouvez du contenu suspect de violation de droits d'auteur, vous êtes invité à envoyer un e-mail à : notice#oldtoolbag.com (veuillez remplacer # par @ lors de l'envoi d'un e-mail pour signaler une violation, et fournir des preuves pertinentes. Une fois vérifié, ce site supprimera immédiatement le contenu suspect de violation de droits d'auteur.)

Vous pourriez aussi aimer