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 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 !
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和守护线程在同一个进程,守护线程也会停下。这把锁到了超时的时候,没人给它续命,也就自动释放了。
首页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的数据存储结构就像一棵树,这棵树由节点组成,这种节点叫做Znode
。
Znode
分为四种类型:
持久节点
(PERSISTENT)默认的节点类型。创建节点的客户端与zookeeper断开连接后,该节点依旧存在 。
持久节点顺序节点
(PERSISTENT_SEQUENTIAL)所谓顺序节点,就是在创建节点时,Zookeeper根据创建的时间顺序给该节点名称进行编号:
临时节点
(EPHEMERAL)和持久节点相反,当创建节点的客户端与zookeeper断开连接后,临时节点会被删除:
临时顺序节点
(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
监听了Lock1
,Client3
监听了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.
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.
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!