Cet article vous présentera les verrous distribués dans Redis, pourquoi les verrous distribués sont nécessaires et comment Redis implémente les verrous distribués. J'espère qu'il vous sera utile !
Pourquoi les verrous distribués sont nécessaires
Le but de l'utilisation des verrous distribués n'est rien de plus que de garantir qu'un seul client peut opérer sur des ressources partagées en même temps.
Nous rencontrons souvent des problèmes de concurrence lors de l'exécution de traitements logiques dans des applications distribuées. [Recommandations associées : Tutoriel vidéo Redis]
Par exemple, si une opération nécessite de modifier le statut de l'utilisateur, la modification du statut nécessite d'abord de lire le statut de l'utilisateur, de le modifier en mémoire, puis de le réenregistrer une fois la modification terminée. . Si de telles opérations sont effectuées en même temps, des problèmes de concurrence surgiront car les deux opérations de lecture et de sauvegarde de l’état ne sont pas atomiques.
À l'heure actuelle, des verrous distribués doivent être utilisés pour limiter l'exécution simultanée du programme. En tant que système middleware de mise en cache, redis peut fournir ce type de mécanisme de verrouillage distribué. Son essence est d'occuper une fosse dans Redis. Lorsque d'autres processus veulent occuper la fosse et découvrent qu'elle a été occupée, attendez et réessayez plus tard
. De manière générale, les verrous distribués disponibles dans les environnements de production doivent répondre aux points suivants :
L'exclusion mutuelle est la fonctionnalité de base des verrous. Un seul thread peut le contenir en même temps. les opérations critiques sont effectuées ;
innodb_lock_wait_timeout
dans le moteur MySQL InnoDB pour éviter une attente inutile des threads lors de la libération du délai d'attente. gaspillage de ressources ; innodb_lock_wait_timeout
配置,通过超时释放,防止不必要的线程等待和资源浪费;使用SETNX实现
SETNX的使用方式为:SETNX key value
,只在键key不存在的情况下,将键key的值设置为value,若键key存在,则SETNX不做任何动作。
boolean result = jedis.setnx("lock-key",true)== 1L; if (result) { try { // do something } finally { jedis.del("lock-key"); } }
这种方案有一个致命问题,就是某个线程在获取锁之后由于某些异常因素(比如宕机)而不能正常的执行解锁操作,那么这个锁就永远释放不掉了。
为此,我们可以为这个锁加上一个超时时间
执行 SET key value EX seconds
的效果等同于执行 SETEX key seconds value
执行 SET key value PX milliseconds
的效果等同于执行 PSETEX key milliseconds value
String result = jedis.set("lock-key",true, 5); if ("OK".equals(result)) { try { // do something } finally { jedis.del("lock-key"); } }
方案看上去很完美,但实际上还是会有问题
试想一下,某线程A获取了锁并且设置了过期时间为10s,然后在执行业务逻辑的时候耗费了15s,此时线程A获取的锁早已被Redis的过期机制自动释放了
在线程A获取锁并经过10s之后,改锁可能已经被其它线程获取到了。当线程A执行完业务逻辑准备解锁(DEL key
)的时候,有可能删除掉的是其它线程已经获取到的锁。
所以最好的方式是在解锁时判断锁是否是自己的,我们可以在设置key
的时候将value设置为一个唯一值uniqueValue
(可以是随机值、UUID、或者机器号+线程号的组合、签名等)。
当解锁时,也就是删除key的时候先判断一下key对应的value是否等于先前设置的值,如果相等才能删除key
String velue= String.valueOf(System.currentTimeMillis()) String result = jedis.set("lock-key",velue, 5); if ("OK".equals(result)) { try { // do something } finally { //非原子操作 if(jedis.get("lock-key")==value){ jedis.del("lock-key"); } } }
这里我们一眼就可以看出问题来:GET
和DEL
是两个分开的操作,在GET执行之后且在DEL执行之前的间隙是可能会发生异常的。
如果我们只要保证解锁的代码是原子性的就能解决问题了
这里我们引入了一种新的方式,就是Lua脚本,示例如下:
if redis.call("get",KEYS[1]) == ARGV[1] then return redis.call("del",KEYS[1]) else return 0 end
其中ARGV[1]
表示设置key时指定的唯一值。
由于Lua脚本的原子性,在Redis执行该脚本的过程中,其他客户端的命令都需要等待该Lua脚本执行完才能执行。
确保过期时间大于业务执行时间
为了防止多个线程同时执行业务代码,需要确保过期时间大于业务执行时间
增加一个boolean类型的属性isOpenExpirationRenewal
,用来标识是否开启定时刷新过期时间
在增加一个scheduleExpirationRenewal
方法用于开启刷新过期时间的线程
加锁代码在获取锁成功后将isOpenExpirationRenewal置为true,并且调用scheduleExpirationRenewal
Utilisez SETNX pour implémenter
;La façon d'utiliser SETNX est la suivante : Valeur de la clé SETNX
Uniquement lorsque la clé clé n'existe pas, la valeur de la clé clé est définie sur valeur. Si la clé clé existe, SETNX n'en crée aucune. action.
SET key value EX seconds
est équivalent à l'exécution de SETEX key seconds value
🎜🎜 ExécuterSET key value PX milliseconds équivaut à exécuter PSETEX key milliseconds value
🎜rrreee🎜🎜La solution semble parfaite, mais en fait il y a encore des problèmes🎜🎜🎜 Imaginez, un thread A a acquis le verrou et a fixé le délai d'expiration à 10 secondes, puis il a fallu 15 secondes pour exécuter la logique métier. À ce moment-là, le verrou acquis par le thread A a déjà été automatiquement libéré par le mécanisme d'expiration de Redis🎜. 🎜Après que le thread A ait acquis le verrou et réussi. Après 10 secondes, le verrou modifié peut avoir été acquis par d'autres threads. Lorsque le thread A termine d'exécuter la logique métier et se prépare au déverrouillage (DEL key
), il est possible de supprimer le verrou qui a été acquis par d'autres threads. 🎜🎜Le meilleur moyen est donc de déterminer si le verrou vous appartient lors du déverrouillage. Nous pouvons définir la valeur sur une valeur unique uniqueValue
lors de la définition de la clé
(cela peut être le cas). Valeur aléatoire, UUID ou combinaison numéro de machine + numéro de thread, signature, etc.). 🎜🎜Lors du déverrouillage, c'est-à-dire lors de la suppression de la clé, déterminez d'abord si la valeur correspondant à la clé est égale à la valeur précédemment définie. Si elle est égale, la clé peut être supprimée🎜rrreee🎜Ici, nous pouvons voir le problème à. un coup d'œil : GET code> et <code>DEL
sont deux opérations distinctes. Une exception peut se produire entre l'exécution de GET et avant l'exécution de DEL. 🎜🎜Si nous devons seulement nous assurer que le code de déverrouillage est atomique, le problème peut être résolu🎜🎜Ici, nous introduisons une nouvelle méthode, qui est 🎜Lua script🎜, l'exemple est le suivant : 🎜rrreee🎜où ARGV [1]
représente la valeur unique spécifiée lors de la définition de la clé. 🎜🎜En raison de l'atomicité du script Lua, pendant le processus d'exécution du script par Redis, les autres commandes client doivent attendre que le script Lua soit exécuté avant de pouvoir être exécutées. 🎜🎜🎜Assurez-vous que le délai d'expiration est supérieur au temps d'exécution de l'entreprise🎜🎜🎜Afin d'éviter que plusieurs threads n'exécutent du code métier en même temps, il est nécessaire de s'assurer que le délai d'expiration est supérieur au temps d'exécution de l'entreprise🎜 🎜Ajoutez un attribut de type booléen isOpenExpirationRenewal
pour identifier si le délai d'expiration de l'actualisation planifiée est activé🎜🎜Ajoutez une méthode scheduleExpirationRenewal
pour ouvrir le fil de discussion pour actualiser le délai d'expiration🎜🎜Le verrouillage le code définit isOpenExpirationRenewal sur true après avoir acquis avec succès le verrou et appelle la méthode planningExpirationRenewal
, démarrez le thread qui actualise le délai d'expiration🎜🎜Ajoutez une ligne de code au code de déverrouillage, définissez l'attribut isOpenExpirationRenewal sur false, arrêter le thread polling qui actualise le délai d'expiration🎜🎜🎜Implémentation de Redisson🎜🎜🎜Il sera ouvert après avoir acquis avec succès le verrou Une tâche planifiée, la tâche planifiée sera vérifiée régulièrement pour le renouvellement🎜Le décalage horaire entre chaque appel de cet horaire programmé est internalLockLeaseTime / 3
, soit 10 secondesinternalLockLeaseTime / 3
,也就10秒
默认情况下,加锁的时间是30秒.如果加锁的业务没有执行完,那么到 30-10 = 20
秒的时候,就会进行一次续期,把锁重置成30秒
在集群中,主节点挂掉时,从节点会取而代之,客户端上却并没有明显感知。原先第一个客户端在主节点中申请成功了一把锁,但是这把锁还没有来得及同步到从节点,主节点突然挂掉了。然后从节点变成了主节点,这个新的节点内部没有这个锁,所以当另一个客户端过来请求加锁时,立即就批准了。这样就会导致系统中同样一把锁被两个客户端同时持有,不安全性由此产生
Redlock算法就是为了解决这个问题
使用 Redlock,需要提供多个 Redis
实例,这些实例之前相互独立没有主从关系。同很多分布式算法一样,redlock 也使用大多数机制
加锁时,它会向过半节点发送 set指令,只要过半节点 set
成功,那就认为加锁成功。释放锁时,需要向所有节点发送 del 指令。不过 Redlock 算法还需要考虑出错重试、时钟漂移等很多细节问题,同时因为 Redlock
需要向多个节点进行读写,意味着相比单实例 Redis 性能会下降一些
Redlock 算法是在单 Redis 节点基础上引入的高可用模式,Redlock 基于 N 个完全独立的 Redis 节点,一般是大于 3 的奇数个(通常情况下 N 可以设置为 5),可以基本保证集群内各个节点不会同时宕机。
假设当前集群有 5 个节点,运行 Redlock 算法的客户端依次执行下面各个步骤,来完成获取锁的操作
也就是说,假设锁30秒过期,三个节点加锁花了31秒,自然是加锁失败了
在 Redis 官方推荐的 Java 客户端 Redisson
中,内置了对 RedLock
30-10 = 20
secondes, un renouvellement sera effectué et le verrouillage sera réinitialisé à 30 secondesRedlock L'algorithme est un mode haute disponibilité introduit sur la base d'un seul nœud Redis. Redlock est basé sur N nœuds Redis complètement indépendants, généralement un nombre impair supérieur à 3 (généralement). N peut être défini sur 5), ce qui peut essentiellement garantir que chaque nœud du cluster ne sera pas en panne en même temps.RedLock
Dans le cluster, le nœud maître raccroche À cette fois, c'est le nœud esclave qui prendra le relais, mais il n'y a pas de perception évidente sur le client. Il s'avère que le premier client a demandé avec succès un verrou sur le nœud maître, mais avant que le verrou puisse être synchronisé avec le nœud esclave, le nœud maître est soudainement mort. Ensuite, le nœud esclave devient le nœud maître. Ce nouveau nœud n'a pas ce verrou à l'intérieur, donc lorsqu'un autre client vient demander un verrou, il est immédiatement approuvé. Cela entraînera le maintien du même verrou dans le système par deux clients en même temps, ce qui entraînera une insécurité. L'algorithme Redlock vise à résoudre ce problème. Pour utiliser Redlock, vous devez fournir plusieurs
Redis. instances, ces instances étaient auparavant indépendantes les unes des autres et n'avaient aucune relation maître-esclave. Comme de nombreux algorithmes distribués, redlock utilise également la plupart des mécanismes lors du verrouillage, il enverra des instructions set à plus de la moitié des nœuds. Tant que plus de la moitié des nœuds <code>set
réussissent, le verrouillage est pris en compte. réussi. Lors de la libération du verrou, une instruction del doit être envoyée à tous les nœuds. Cependant, l'algorithme Redlock doit également prendre en compte de nombreux problèmes détaillés tels que les nouvelles tentatives d'erreur et la dérive d'horloge. En même temps, parce queRedlock
doit lire et écrire sur plusieurs nœuds, cela signifie que les performances de Redis. sera inférieur à celui d'une seule instance.
En supposant que le cluster actuel comporte 5 nœuds, le client exécutant l'algorithme Redlock effectue les étapes suivantes dans l'ordre pour terminer l'opération d'acquisition du verrou
Le client enregistre l'heure actuelle du système en millisecondes Essayez de ; commencer à partir de Dans 5 instances Redis, la même clé est utilisée pour acquérir le verrou. Lorsqu'il demande à Redis d'acquérir le verrou, le client doit définir un délai de connexion réseau et un délai d'expiration de réponse. Le délai d'expiration doit être inférieur au délai d'expiration du verrou pour éviter les problèmes dus. aux pannes de réseau ;
Le client utilise l'heure actuelle moins l'heure à laquelle il a commencé à acquérir le verrou pour obtenir le temps utilisé pour acquérir le verrou. Le verrou est compté si et seulement si le verrou est acquis à partir de plus de la moitié du temps. Les nœuds Redis et le temps utilisé sont inférieurs au temps d'expiration du verrou. L'acquisition est réussie
Dans le client Java
https://redis.io/topics/distlockRedisson
officiellement recommandé par Redis. , Implémentation intégrée deRedLock
https://github.com/redisson/redisson/wiki
🎜Problème de RedLock : 🎜🎜 🎜RedLock assure uniquement la haute disponibilité du verrou, mais ne garantit pas l'exactitude du verrou 🎜🎜RedLock est un 🎜système distribué qui s'appuie fortement sur l'horloge système🎜🎜🎜Critique de Martin à l'égard de RedLock : 🎜🎜🎜Pour. scénarios d'amélioration de l'efficacité, RedLock est trop lourd. 🎜🎜Pour les scénarios nécessitant une précision extrêmement élevée, RedLock ne peut pas garantir l'exactitude. 🎜🎜🎜🎜Cet article est réimprimé de : https://juejin.cn/post/7018968452136173576🎜🎜Auteur : With a Distant Mind🎜🎜🎜Pour plus de connaissances liées à la programmation, veuillez visiter : 🎜Vidéo de 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!