Maison  >  Article  >  base de données  >  À quoi devons-nous prêter attention lors de la mise en œuvre de verrous distribués dans Redis ? [Résumé des précautions]

À quoi devons-nous prêter attention lors de la mise en œuvre de verrous distribués dans Redis ? [Résumé des précautions]

青灯夜游
青灯夜游avant
2022-03-04 16:21:583735parcourir

À quoi devez-vous faire attention lors de la mise en œuvre de verrous distribués dans Redis ? L'article suivant résumera et partagera avec vous quelques points à noter lors de l'utilisation de Redis comme verrou distribué. J'espère qu'il vous sera utile !

À quoi devons-nous prêter attention lors de la mise en œuvre de verrous distribués dans Redis ? [Résumé des précautions]

Redis implémente les verrous distribués

J'ai récemment vu un bon article en train de lire les verrous distribués, et j'ai spécialement traité ma propre compréhension :

Trois aspects de la mise en œuvre du verrou distribué Redis Éléments de base :

1. Le moyen le plus simple de verrouiller

est d'utiliser la commande setnx. La clé est l'identifiant unique du verrou, qui est nommé en fonction de l'entreprise. La valeur est l'ID du thread actuel. [Recommandation associée : Tutoriel vidéo Redis]

Par exemple, si vous souhaitez verrouiller l'activité de vente flash d'un produit, vous pouvez nommer la clé "lock_sale_ID". Et quelle est la valeur définie ? Nous pouvons temporairement le mettre à 1. Le pseudo-code pour le verrouillage est le suivant :

setnx(key, 1)Lorsqu'un thread exécute setnx, il renvoie 1, indiquant que le thread a obtenu le verrou avec succès lorsque d'autres threads s'exécutent. setnx, il renvoie 0, indiquant que la clé existe déjà et que le thread n'a pas réussi à saisir le verrou.

2. Déverrouiller

Si vous souhaitez verrouiller, vous devez déverrouiller. Lorsque le thread qui a obtenu le verrou termine sa tâche, il doit libérer le verrou pour que d'autres threads puissent entrer. Le moyen le plus simple de libérer le verrou est d'exécuter l'instruction del Le pseudo-code est le suivant :

del(key)Une fois le verrou libéré, d'autres threads peuvent continuer à exécuter la commande setnx pour obtenir. la serrure.

3. Délai d'expiration du verrouillage

Que signifie le délai d'expiration du verrouillage ? Si un thread qui obtient le verrou meurt pendant l'exécution de la tâche et n'a pas le temps de libérer explicitement le verrou, la ressource sera verrouillée pour toujours et les autres threads ne pourront plus jamais y accéder.

Ainsi, la clé de setnx doit définir un délai d'attente pour garantir que même si elle n'est pas explicitement libérée, le verrou sera automatiquement libéré après une certaine période de temps. setnx ne prend pas en charge les paramètres de délai d'attente, des instructions supplémentaires sont donc nécessaires. Le pseudocode est le suivant :

expire (clé, 30) Pour résumer, la première version de notre pseudocode d'implémentation de verrouillage distribué est la suivante :

if(setnx(key,1) == 1){
    expire(key,30)
    try {
        do something ......
    }catch()  {  }  finally {
       del(key)
    }

}
.

À cause de ce qui précède Dans le pseudocode, il y a trois problèmes fatals :

1 La non-atomicité de setnx et expire

Imaginez un scénario extrême, lorsqu'un thread exécute setnx et obtient avec succès le verrou :

.

setnx vient d'être exécuté avec succès, avant que la commande expire puisse être exécutée, le nœud 1 est mort avec un son Duang.

if(setnx(key,1) == 1){  //此处挂掉了.....
    expire(key,30)
    try {
        do something ......
    }catch()
  {
  }
  finally {
       del(key)
    }
 
}

De cette façon, le verrou n'a pas de délai d'expiration défini et devient "immortel", et les autres threads ne peuvent plus obtenir le verrou.

Comment le résoudre ? L'instruction setnx elle-même ne prend pas en charge le délai d'attente entrant. Redis 2.6.12 ou supérieur ajoute des paramètres facultatifs à l'instruction set. Le pseudo-code est le suivant : set (key, 1, 30, NX), qui peut remplacer le setnx. instruction .

2. L'utilisation de après un délai d'attente entraîne la suppression accidentelle des verrous d'autres threads

C'est un autre scénario extrême si un thread obtient avec succès le verrou et que le délai d'attente est défini sur 30. secondes.

Si, pour une raison quelconque, le thread A s'exécute très lentement et n'a pas fini de s'exécuter après 30 secondes, le verrou sera automatiquement libéré à l'expiration et le thread B obtiendra le verrou.

Ensuite, le thread A termine la tâche, et le thread A exécute ensuite l'instruction del pour libérer le verrou. Mais pour le moment, le thread B n'a pas fini de s'exécuter. Le thread A supprime en fait le verrou ajouté par le thread B.

Comment éviter cette situation ? Vous pouvez porter un jugement avant que del ne libère le verrou pour vérifier si le verrou actuel est un verrou ajouté par vous-même.

En ce qui concerne l'implémentation spécifique, vous pouvez utiliser l'ID de thread actuel comme valeur lors du verrouillage et vérifier si la valeur correspondant à la clé est l'ID de votre propre thread avant de le supprimer.

加锁:
String threadId = Thread.currentThread().getId()
set(key,threadId ,30,NX)
doSomething.....
 
解锁:
if(threadId .equals(redisClient.get(key))){
    del(key)
}

Cependant, cela implique un nouveau problème, si le jugement et le déverrouillage sont deux opérations indépendantes, et non atomiques.

Nous sommes tous des programmeurs qui recherchent la perfection, donc cette partie doit être implémentée à l'aide du script Lua :

String luaScript = 'if redis.call('get', KEYS[1]) == ARGV[ 1] puis retournez redis.call('del', KEYS[1]) sinon return 0 end';

redisClient.eval(luaScript , Collections.singletonList( key), Collections.singletonList(threadId));

De cette façon, le processus de vérification et de suppression est une opération atomique.

3. Possibilité de simultanéité

还是刚才第二点所描述的场景,虽然我们避免了线程A误删掉key的情况,但是同一时间有A,B两个线程在访问代码块,仍然是不完美的。

怎么办呢?我们可以让获得锁的线程开启一个守护线程,用来给快要过期的锁“续航”

当过去了29秒,线程A还没执行完,这时候守护线程会执行expire指令,为这把锁“续命”20秒。守护线程从第29秒开始执行,每20秒执行一次。

当线程A执行完任务,会显式关掉守护线程。

另一种情况,如果节点1 忽然断电,由于线程A和守护线程在同一个进程,守护线程也会停下。这把锁到了超时的时候,没人给它续命,也就自动释放了。

 memcache实现分布式锁

首页top 10, 由数据库加载到memcache缓存n分钟
微博中名人的content cache, 一旦不存在会大量请求不能命中并加载数据库
需要执行多个IO操作生成的数据存在cache中, 比如查询db多次
问题
在大并发的场合,当cache失效时,大量并发同时取不到cache,会同一瞬间去访问db并回设cache,可能会给系统带来潜在的超负荷风险。我们曾经在线上系统出现过类似故障。

解决方法

if (memcache.get(key) == null) {
// 3 min timeout to avoid mutex holder crash
if (memcache.add(key_mutex, 3 * 60 * 1000) == true) {
value = db.get(key);
memcache.set(key, value);
memcache.delete(key_mutex);
} else {
 
sleep(50);
retry();
}
}

在load db之前先add一个mutex key, mutex key add成功之后再去做加载db, 如果add失败则sleep之后重试读取原cache数据。为了防止死锁,mutex key也需要设置过期时间。伪代码如下

Zookeeper实现分布式缓存

Zookeeper的数据存储结构就像一棵树,这棵树由节点组成,这种节点叫做Znode

Znode分为四种类型:

  • 1.持久节点 (PERSISTENT)

默认的节点类型。创建节点的客户端与zookeeper断开连接后,该节点依旧存在 。

  • 2.持久节点顺序节点(PERSISTENT_SEQUENTIAL)

所谓顺序节点,就是在创建节点时,Zookeeper根据创建的时间顺序给该节点名称进行编号:

  • 3.临时节点(EPHEMERAL)

和持久节点相反,当创建节点的客户端与zookeeper断开连接后,临时节点会被删除:

  • 4.临时顺序节点(EPHEMERAL_SEQUENTIAL)

顾名思义,临时顺序节点结合和临时节点和顺序节点的特点:在创建节点时,Zookeeper根据创建的时间顺序给该节点名称进行编号;当创建节点的客户端与zookeeper断开连接后,临时节点会被删除。

Zookeeper分布式锁恰恰应用了临时顺序节点。具体如何实现呢?让我们来看一看详细步骤:

  • 获取锁

首先,在Zookeeper当中创建一个持久节点ParentLock。当第一个客户端想要获得锁时,需要在ParentLock这个节点下面创建一个临时顺序节点 Lock1

之后,Client1查找ParentLock下面所有的临时顺序节点并排序,判断自己所创建的节点Lock1是不是顺序最靠前的一个。如果是第一个节点,则成功获得锁。

这时候,如果再有一个客户端 Client2 前来获取锁,则在ParentLock下载再创建一个临时顺序节点Lock2

Client2查找ParentLock下面所有的临时顺序节点并排序,判断自己所创建的节点Lock2是不是顺序最靠前的一个,结果发现节点Lock2并不是最小的。

Donc, Client2 enregistre Watcher avec le nœud Lock1 qui est seulement classé plus haut que lui, pour surveiller Lock1 > Si le nœud existe. Cela signifie que Client2 n'a pas réussi à saisir le verrou et est entré dans l'état d'attente. Client2向排序仅比它靠前的节点Lock1注册Watcher,用于监听Lock1节点是否存在。这意味着Client2抢锁失败,进入了等待状态。

这时候,如果又有一个客户端Client3前来获取锁,则在ParentLock下载再创建一个临时顺序节点Lock3

Client3查找ParentLock下面所有的临时顺序节点并排序,判断自己所创建的节点Lock3是不是顺序最靠前的一个,结果同样发现节点Lock3并不是最小的。

于是,Client3向排序仅比它靠前的节点Lock2注册Watcher,用于监听Lock2节点是否存在。这意味着Client3同样抢锁失败,进入了等待状态。

这样一来,Client1得到了锁,Client2监听了Lock1Client3监听了Lock2。这恰恰形成了一个等待队列,很像是Java当中ReentrantLock所依赖的AQS(AbstractQueuedSynchronizer)

  • 释放锁

释放锁分为两种情况:

1.任务完成,客户端显示释放

当任务完成时,Client1会显示调用删除节点Lock1的指令。

2.任务执行过程中,客户端崩溃

获得锁的Client1在任务执行过程中,如果Duang的一声崩溃,则会断开与Zookeeper服务端的链接。根据临时节点的特性,相关联的节点Lock1会随之自动删除。

由于Client2一直监听着Lock1的存在状态,当Lock1节点被删除,Client2会立刻收到通知。这时候Client2会再次查询ParentLock下面的所有节点,确认自己创建的节点Lock2是不是目前最小的节点。如果是最小,则Client2顺理成章获得了锁。

同理,如果Client2也因为任务完成或者节点崩溃而删除了节点Lock2,那么Cient3就会接到通知。

最终,Client3

En ce moment, if Si un autre client Client3 vient acquérir le verrou, il téléchargera et créera un nœud de séquence temporaire Lock3 dans ParentLock.

Client3 Trouvez tous les nœuds de séquence temporaires sous ParentLock et triez-les, et jugez si le nœud Lock3 que vous avez créé est celui avec l'ordre le plus élevé. le nœud Lock3 n'est pas le plus petit.

Ainsi, Client3 enregistre Watcher avec le nœud Lock2 qui est classé seulement plus haut que lui, pour surveiller Lock2 Si le nœud existe. Cela signifie que Client3 n'a pas non plus réussi à saisir le verrou et est entré dans l'état d'attente.

De cette façon, Client1 a obtenu le verrou, Client2 a écouté Lock1 et Client3 a écouté Lock2 . Cela forme simplement une file d'attente, un peu comme le AQS (AbstractQueuedSynchronizer) sur lequel ReentrantLock s'appuie en Java.

  • Déverrouiller
Il existe deux situations pour déverrouiller le verrou :

1 Lorsque la tâche est terminée, le client affiche la libération Lorsque la tâche est terminée, Client1 affichera l'appel. pour supprimer la directive nœud Lock1.

🎜🎜🎜2. Pendant le processus, le client plante 🎜🎜Client1 qui a obtenu le verrou. Lors de l'exécution de la tâche, si Duang plante, le lien avec le serveur Zookeeper sera déconnecté. Selon les caractéristiques du nœud temporaire, le nœud associé Lock1 sera automatiquement supprimé. 🎜🎜🎜🎜En raison de Client2 surveille l'état d'existence de Lock1 Lorsque le nœud Lock1 est supprimé, Client2 recevra une notification immédiatement. À ce stade, Client2 interrogera à nouveau tous les nœuds sous ParentLock pour confirmer si le nœud Lock2 créé par lui-même est le plus petit nœud actuellement. S'il est le plus petit, Client2 obtient le verrou naturellement. 🎜🎜🎜🎜De même, si Client2 supprime également le nœud Lock2 en raison de l'achèvement de la tâche ou d'un crash du nœud, puis Cient3 sera notifié. 🎜🎜🎜🎜Enfin, Client3 a réussi à obtenir le verrou. 🎜🎜🎜🎜🎜🎜🎜🎜🎜🎜🎜Comparaison des verrous distribués Zookeeper et Redis🎜🎜Le tableau suivant résume les avantages et les inconvénients des verrous distribués Zookeeper et Redis :🎜🎜🎜🎜🎜🎜🎜🎜Plus de connaissances en matière de programmation, visiter : 🎜Démarrez avec la programmation🎜 ! ! 🎜

Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!

Déclaration:
Cet article est reproduit dans:. en cas de violation, veuillez contacter admin@php.cn Supprimer