Maison >base de données >Redis >Vous donner une compréhension approfondie des verrous distribués dans Redis

Vous donner une compréhension approfondie des verrous distribués dans Redis

青灯夜游
青灯夜游avant
2022-03-08 10:13:502052parcourir

Comprenez-vous vraiment les verrous distribués Redis ? L'article suivant vous donnera une introduction approfondie aux verrous distribués dans Redis et expliquera comment implémenter les verrous, libérer les verrous, les défauts des verrous distribués, etc. J'espère qu'il vous sera utile !

Vous donner une compréhension approfondie des verrous distribués dans Redis

Qu'est-ce qu'un verrou distribué

En parlant de Redis, la première fonction à laquelle nous pensons est la possibilité de mettre en cache les données. De plus, Redis est souvent utilisé pour la distribution en raison de son style de processus unique et de ses hautes performances. verrouillage. [Recommandations associées : Tutoriel vidéo Redis]

Nous savons tous que les verrous fonctionnent comme un outil de synchronisation dans les programmes pour garantir que les ressources partagées ne sont accessibles que par un seul thread à la fois. Nous connaissons tous les verrous en Java, comme synchronisé et Lock sont tous deux couramment utilisés par nous, mais les verrous Java ne peuvent être garantis que sur une seule machine et sont impuissants dans un environnement de cluster distribué. À l'heure actuelle, nous devons utiliser des verrous distribués.

Les verrous distribués, comme leur nom l'indique, sont des verrous utilisés dans le développement de projets distribués. Ils peuvent être utilisés pour contrôler l'accès synchrone aux ressources partagées entre les systèmes distribués. De manière générale, les verrous distribués doivent répondre aux caractéristiques suivantes :

1. exclusivité : A tout moment, pour une même donnée, une seule application peut obtenir le verrou distribué ;

2 Haute disponibilité : Dans un scénario distribué, le temps d'arrêt d'un petit nombre de serveurs n'affecte pas l'utilisation normale. dans ce cas, le service qui fournit les verrous distribués doit être déployé dans un cluster ;

3. Empêcher le délai d'expiration du verrouillage : si le client ne libère pas activement le verrou, le serveur le libérera automatiquement après un certain temps pour empêcher le verrouillage. client de tomber en panne. Ou un blocage se produit lorsque le réseau est inaccessible ;

4. Exclusivité : le verrouillage et le déverrouillage doivent être effectués par le même serveur, c'est-à-dire que seul le détenteur du verrou peut libérer le verrou. verrouillez et quelqu'un d'autre le déverrouille pour vous. ;

Il existe de nombreux outils dans l'industrie qui peuvent obtenir des effets de verrouillage distribués, mais les opérations sont simplement les suivantes : verrouiller, déverrouiller et empêcher l'expiration du délai de verrouillage.

Puisque cet article parle du verrouillage distribué Redis, il est naturel pour nous de l'étendre avec les points de connaissance de Redis.

Commandes pour implémenter les verrous

Introduisez d'abord quelques commandes de Redis,

1 SETNX, l'utilisation est Valeur de la clé SETNXSETNX key value

SETNX是『 SET if Not eXists』(如果不存在,则 SET)的简写,设置成功就返回1,否则返回0。

setnx用法

可以看出,当把keylock的值设置为"Java"后,再设置成别的值就会失败,看上去很简单,也好像独占了锁,但有个致命的问题,就是key没有过期时间,这样一来,除非手动删除key或者获取锁后设置过期时间,不然其他线程永远拿不到锁。

既然这样,我们给key加个过期时间总可以吧,直接让线程获取锁的时候执行两步操作:

`SETNX Key 1`
`EXPIRE Key Seconds`

这个方案也有问题,因为获取锁和设置过期时间分成两步了,不是原子性操作,有可能获取锁成功但设置时间失败,那样不就白干了吗。

不过也不用急,这种事情Redis官方早为我们考虑到了,所以就引出了下面这个命令

2、SETEX,用法SETEX key seconds value

将值 value 关联到 key ,并将 key 的生存时间设为 seconds (以秒为单位)。如果 key 已经存在,SETEX 命令将覆写旧值。

这个命令类似于以下两个命令:

`SET key value`
`EXPIRE key seconds  # 设置生存时间`

这两步动作是原子性的,会在同一时间完成。

setex用法

3、PSETEX ,用法PSETEX key milliseconds value

这个命令和SETEX命令相似,但它以毫秒为单位设置 key

SETNX est "SET if Not eXists" (s'il n'existe pas). existe, L'abréviation de SET) renvoie 1 si le réglage est réussi, sinon il renvoie 0.

utilisation de setnx

On peut voir que lors du changement de key sur la valeur de lock strong> Après l'avoir défini sur "Java", il échouera s'il est défini sur une autre valeur. Cela semble très simple et semble monopoliser le verrou, mais il y a un problème fatal, c'est-à-dire la clé. n'a pas de délai d'expiration. De cette façon, à moins que vous ne supprimiez manuellement la clé ou que vous définissiez le délai d'expiration après avoir acquis le verrou, les autres threads n'obtiendront jamais le verrou.

Dans ce cas, on peut toujours ajouter un délai d'expiration à la clé, et laisser directement le thread effectuer une opération en deux étapes lors de l'acquisition du verrou :

`SET key value NX EX seconds`

Cette solution pose également des problèmes, car l'acquisition du verrou et le réglage de l'expiration le temps est divisé en deux étapes, ce qui n'est pas atomique. Dans cette opération, il est possible d'acquérir le verrou avec succès mais de ne pas régler l'heure.

Mais ne vous inquiétez pas, les responsables de Redis ont déjà envisagé ce genre de chose pour nous, nous avons donc introduit la commande suivante🎜🎜2 SETEX, en utilisant la Valeur des secondes clés SETEX🎜🎜Changez la valeur <. code> value est associé à key et définit la durée de vie de la key à seconds (en secondes). Si key existe déjà, la commande SETEX écrasera l'ancienne valeur. 🎜🎜Cette commande est similaire aux deux commandes suivantes : 🎜
`if redis.call("get",KEYS[1]) == ARGV[1]`
`then`
 `return redis.call("del",KEYS[1])`
`else`
 `return 0`
`end`
🎜Ces deux étapes sont atomiques et seront complétées en même temps. 🎜🎜🎜🎜utilisation de setex🎜🎜3. PSETEX, utilisation Valeur en millisecondes de la clé PSETEX🎜🎜Cette commande est similaire à SETEX Mais elle définit la durée de vie de la clé en millisecondes, et non en secondes comme la commande SETEX. 🎜🎜Cependant, à partir de la version 2.6.12 de Redis, la commande SET peut utiliser des paramètres pour obtenir le même effet que les trois commandes SETNX, SETEX et PSETEX. 🎜🎜Par exemple, cette commande🎜
`public class RedisLockUtil {`
 `private String LOCK_KEY = "redis_lock";`
 `// key的持有时间,5ms`
 `private long EXPIRE_TIME = 5;`
 `// 等待超时时间,1s`
 `private long TIME_OUT = 1000;`
 `// redis命令参数,相当于nx和px的命令合集`
 `private SetParams params = SetParams.setParams().nx().px(EXPIRE_TIME);`
 `// redis连接池,连的是本地的redis客户端`
 `JedisPool jedisPool = new JedisPool("127.0.0.1", 6379);`
 `/**`
 `* 加锁`
 `*`
 `* @param id`
 `*            线程的id,或者其他可识别当前线程且不重复的字段`
 `* @return`
 `*/`
 `public boolean lock(String id) {`
 `Long start = System.currentTimeMillis();`
 `Jedis jedis = jedisPool.getResource();`
 `try {`
 `for (;;) {`
 `// SET命令返回OK ,则证明获取锁成功`
 `String lock = jedis.set(LOCK_KEY, id, params);`
 `if ("OK".equals(lock)) {`
 `return true;`
 `}`
 `// 否则循环等待,在TIME_OUT时间内仍未获取到锁,则获取失败`
 `long l = System.currentTimeMillis() - start;`
 `if (l >= TIME_OUT) {`
 `return false;`
 `}`
 `try {`
 `// 休眠一会,不然反复执行循环会一直失败`
 `Thread.sleep(100);`
 `} catch (InterruptedException e) {`
 `e.printStackTrace();`
 `}`
 `}`
 `} finally {`
 `jedis.close();`
 `}`
 `}`
 `/**`
 `* 解锁`
 `*`
 `* @param id`
 `*            线程的id,或者其他可识别当前线程且不重复的字段`
 `* @return`
 `*/`
 `public boolean unlock(String id) {`
 `Jedis jedis = jedisPool.getResource();`
 `// 删除key的lua脚本`
 `String script = "if redis.call(&#39;get&#39;,KEYS[1]) == ARGV[1] then" + "   return redis.call(&#39;del&#39;,KEYS[1]) " + "else"`
 `+ "   return 0 " + "end";`
 `try {`
 `String result =`
 `jedis.eval(script, Collections.singletonList(LOCK_KEY), Collections.singletonList(id)).toString();`
 `return "1".equals(result);`
 `} finally {`
 `jedis.close();`
 `}`
 `}`
`}`
🎜Après avoir ajouté les paramètres NX et EX, l'effet est équivalent à SETEX, qui est également le moyen le plus courant d'écrire des verrous dans Redis. 🎜🎜Comment déverrouiller le verrou🎜🎜La commande pour déverrouiller le verrou est simple, il suffit de supprimer la clé, mais comme nous l'avons dit précédemment, parce que le verrou distribué doit être libéré par le détenteur du verrou lui-même, nous devons d'abord nous assurer que le fil qui libère actuellement le verrou. S'il n'y a pas de problème, supprimez-le. De cette façon, cela semble à nouveau violer l'atomicité. 🎜🎜Pas de panique, on peut utiliser le script lua pour assembler l'opération en deux étapes, juste comme ceci : 🎜
`if redis.call("get",KEYS[1]) == ARGV[1]`
`then`
 `return redis.call("del",KEYS[1])`
`else`
 `return 0`
`end`

KEYS[1]是当前key的名称,ARGV[1]可以是当前线程的ID(或者其他不固定的值,能识别所属线程即可),这样就可以防止持有过期锁的线程,或者其他线程误删现有锁的情况出现。

代码实现

知道了原理后,我们就可以手写代码来实现Redis分布式锁的功能了,因为本文的目的主要是为了讲解原理,不是为了教大家怎么写分布式锁,所以我就用伪代码实现了。

首先是redis锁的工具类,包含了加锁和解锁的基础方法:

`public class RedisLockUtil {`
 `private String LOCK_KEY = "redis_lock";`
 `// key的持有时间,5ms`
 `private long EXPIRE_TIME = 5;`
 `// 等待超时时间,1s`
 `private long TIME_OUT = 1000;`
 `// redis命令参数,相当于nx和px的命令合集`
 `private SetParams params = SetParams.setParams().nx().px(EXPIRE_TIME);`
 `// redis连接池,连的是本地的redis客户端`
 `JedisPool jedisPool = new JedisPool("127.0.0.1", 6379);`
 `/**`
 `* 加锁`
 `*`
 `* @param id`
 `*            线程的id,或者其他可识别当前线程且不重复的字段`
 `* @return`
 `*/`
 `public boolean lock(String id) {`
 `Long start = System.currentTimeMillis();`
 `Jedis jedis = jedisPool.getResource();`
 `try {`
 `for (;;) {`
 `// SET命令返回OK ,则证明获取锁成功`
 `String lock = jedis.set(LOCK_KEY, id, params);`
 `if ("OK".equals(lock)) {`
 `return true;`
 `}`
 `// 否则循环等待,在TIME_OUT时间内仍未获取到锁,则获取失败`
 `long l = System.currentTimeMillis() - start;`
 `if (l >= TIME_OUT) {`
 `return false;`
 `}`
 `try {`
 `// 休眠一会,不然反复执行循环会一直失败`
 `Thread.sleep(100);`
 `} catch (InterruptedException e) {`
 `e.printStackTrace();`
 `}`
 `}`
 `} finally {`
 `jedis.close();`
 `}`
 `}`
 `/**`
 `* 解锁`
 `*`
 `* @param id`
 `*            线程的id,或者其他可识别当前线程且不重复的字段`
 `* @return`
 `*/`
 `public boolean unlock(String id) {`
 `Jedis jedis = jedisPool.getResource();`
 `// 删除key的lua脚本`
 `String script = "if redis.call(&#39;get&#39;,KEYS[1]) == ARGV[1] then" + "   return redis.call(&#39;del&#39;,KEYS[1]) " + "else"`
 `+ "   return 0 " + "end";`
 `try {`
 `String result =`
 `jedis.eval(script, Collections.singletonList(LOCK_KEY), Collections.singletonList(id)).toString();`
 `return "1".equals(result);`
 `} finally {`
 `jedis.close();`
 `}`
 `}`
`}`

具体的代码作用注释已经写得很清楚了,然后我们就可以写一个demo类来测试一下效果:

`public class RedisLockTest {`
 `private static RedisLockUtil demo = new RedisLockUtil();`
 `private static Integer NUM = 101;`
 `public static void main(String[] args) {`
 `for (int i = 0; i < 100; i++) {`
 `new Thread(() -> {`
 `String id = Thread.currentThread().getId() + "";`
 `boolean isLock = demo.lock(id);`
 `try {`
 `// 拿到锁的话,就对共享参数减一`
 `if (isLock) {`
 `NUM--;`
 `System.out.println(NUM);`
 `}`
 `} finally {`
 `// 释放锁一定要注意放在finally`
 `demo.unlock(id);`
 `}`
 `}).start();`
 `}`
 `}`
`}`

我们创建100个线程来模拟并发的情况,执行后的结果是这样的:

代码执行结果

可以看出,锁的效果达到了,线程安全是可以保证的。

当然,上面的代码只是简单的实现了效果,功能肯定是不完整的,一个健全的分布式锁要考虑的方面还有很多,实际设计起来不是那么容易的。

我们的目的只是为了学习和了解原理,手写一个工业级的分布式锁工具不现实,也没必要,类似的开源工具一大堆(Redisson),原理都差不多,而且早已经过业界同行的检验,直接拿来用就行。

虽然功能是实现了,但其实从设计上来说,这样的分布式锁存在着很大的缺陷,这也是本篇文章想重点探讨的内容。

分布式锁的缺陷

一、客户端长时间阻塞导致锁失效问题

客户端1得到了锁,因为网络问题或者GC等原因导致长时间阻塞,然后业务程序还没执行完锁就过期了,这时候客户端2也能正常拿到锁,可能会导致线程安全的问题。

客户端长时间阻塞

那么该如何防止这样的异常呢?我们先不说解决方案,介绍完其他的缺陷后再来讨论。

二、redis服务器时钟漂移问题

如果redis服务器的机器时钟发生了向前跳跃,就会导致这个key过早超时失效,比如说客户端1拿到锁后,key的过期时间是12:02分,但redis服务器本身的时钟比客户端快了2分钟,导致key在12:00的时候就失效了,这时候,如果客户端1还没有释放锁的话,就可能导致多个客户端同时持有同一把锁的问题。

三、单点实例安全问题

如果redis是单master模式的,当这台机宕机的时候,那么所有的客户端都获取不到锁了,为了提高可用性,可能就会给这个master加一个slave,但是因为redis的主从同步是异步进行的,可能会出现客户端1设置完锁后,master挂掉,slave提升为master,因为异步复制的特性,客户端1设置的锁丢失了,这时候客户端2设置锁也能够成功,导致客户端1和客户端2同时拥有锁。

为了解决Redis单点问题,redis的作者提出了RedLock算法。

RedLock算法

该算法的实现前提在于Redis必须是多节点部署的,可以有效防止单点故障,具体的实现思路是这样的:

1、获取当前时间戳(ms);

2、先设定key的有效时长(TTL),超出这个时间就会自动释放,然后client(客户端)尝试使用相同的key和value对所有redis实例进行设置,每次链接redis实例时设置一个比TTL短很多的超时时间,这是为了不要过长时间等待已经关闭的redis服务。并且试着获取下一个redis实例。

比如:TTL(也就是过期时间)为5s,那获取锁的超时时间就可以设置成50ms,所以如果50ms内无法获取锁,就放弃获取这个锁,从而尝试获取下个锁;

3、client通过获取所有能获取的锁后的时间减去第一步的时间,还有redis服务器的时钟漂移误差,然后这个时间差要小于TTL时间并且成功设置锁的实例数>= N/2 + 1(N为Redis实例的数量),那么加锁成功

比如TTL是5s,连接redis获取所有锁用了2s,然后再减去时钟漂移(假设误差是1s左右),那么锁的真正有效时长就只有2s了;

4、如果客户端由于某些原因获取锁失败,便会开始解锁所有redis实例。

根据这样的算法,我们假设有5个Redis实例的话,那么client只要获取其中3台以上的锁就算是成功了,用流程图演示大概就像这样: 

temps de validité de la clé

D'accord, l'algorithme a été introduit d'un point de vue conception, il ne fait aucun doute que l'idée de​​l'algorithme RedLock est principalement de prévenir efficacement le problème de Redis. défaillance en un seul point, et lors de la conception du TTL, l'erreur de dérive de l'horloge du serveur est également prise en compte, ce qui améliore considérablement la sécurité des verrous distribués.

Mais est-ce vraiment le cas ? Quoi qu'il en soit, je pense personnellement que l'effet est moyen,

Tout d'abord, nous pouvons voir que dans l'algorithme RedLock, le temps effectif du verrouillage sera réduit du temps nécessaire pour se connecter à l'instance Redis si ce processus. prend trop de temps en raison de problèmes de réseau. Si tel est le cas, le temps effectif restant pour le verrouillage sera considérablement réduit. Le temps nécessaire au client pour accéder aux ressources partagées est très court et il est très probable que le verrouillage expire pendant ce temps. traitement du programme. De plus, le temps effectif du verrouillage doit être soustrait de la dérive d'horloge du serveur, mais combien faut-il soustraire si cette valeur n'est pas correctement définie, des problèmes peuvent facilement survenir ?

Ensuite, le deuxième point est que bien que cet algorithme prenne en compte l'utilisation de plusieurs nœuds pour éviter le point de défaillance unique de Redis, si un nœud plante et redémarre, il est toujours possible pour plusieurs clients d'acquérir des verrous en même temps.

Supposons qu'il y ait un total de 5 nœuds Redis : A, B, C, D, E. Les clients 1 et 2 sont respectivement verrouillés

  • Le client 1 a verrouillé avec succès A, B, C et a obtenu le verrou avec succès (mais D et E ne sont pas verrouillés).

  • Le maître du nœud C est en panne, puis le verrou n'a pas été synchronisé avec l'esclave. Après la mise à niveau de l'esclave vers le maître, il a perdu le verrou ajouté par le client 1.

  • Le client 2 a acquis le verrou à ce moment-là, a verrouillé C, D et E et a acquis le verrou avec succès.

De cette façon, le client 1 et le client 2 obtiennent le verrou en même temps, et le danger caché de la sécurité du programme existe toujours. De plus, si une dérive temporelle se produit dans l’un de ces nœuds, cela peut également entraîner des problèmes de sécurité de verrouillage.

Ainsi, bien que la disponibilité et la fiabilité soient améliorées grâce au déploiement multi-instance, RedLock ne résout pas complètement le danger caché du point de défaillance unique de Redis, ni l'échec du délai de verrouillage causé par la dérive d'horloge et le blocage à long terme du client. Des problèmes et des risques de sécurité liés aux serrures existent toujours.

Conclusion

Certaines personnes voudront peut-être demander plus loin : que faut-il faire pour garantir la sécurité absolue de la serrure ?

Je peux seulement dire que vous ne pouvez pas avoir le gâteau et le manger aussi. La raison pour laquelle nous utilisons Redis comme outil de verrouillage distribué est en grande partie due à la haute efficacité de Redis et à ses caractéristiques de processus unique, même dans des scénarios à forte concurrence. Il peut également garantir de bonnes performances dans certaines circonstances, mais dans de nombreux cas, les performances et la sécurité ne peuvent pas être pleinement équilibrées. Si vous devez assurer la sécurité du verrou, vous pouvez utiliser d'autres middlewares tels que db et zookeeper pour le contrôle. très efficace. Un bon garantit la sécurité de la serrure, mais ses performances ne peuvent être considérées que comme insatisfaisantes, sinon tout le monde l'aurait utilisé depuis longtemps.

D'une manière générale, si Redis est utilisé pour contrôler des ressources partagées et que des exigences élevées en matière de sécurité des données sont requises, la solution de garantie finale consiste à mettre en œuvre un contrôle idempotent des données d'entreprise de cette manière, même si plusieurs clients obtiennent des verrous, cela n'affectera pas. cohérence des données. Bien entendu, toutes les scènes ne sont pas adaptées à cela. Le choix spécifique doit être décidé par chaque juge. Après tout, il n’existe pas de technologie parfaite, seule celle qui convient est la meilleure.

Pour plus de connaissances sur la programmation, veuillez visiter : Introduction à 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