Maison  >  Article  >  base de données  >  Pourquoi le verrouillage distribué est-il nécessaire dans Redis ? Comment y parvenir ?

Pourquoi le verrouillage distribué est-il nécessaire dans Redis ? Comment y parvenir ?

青灯夜游
青灯夜游avant
2021-10-20 10:37:584312parcourir

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 le verrouillage distribué est-il nécessaire dans Redis ? Comment y parvenir ?

Pourquoi les verrous distribués sont nécessaires

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 ;

    La libération du délai d'attente est une autre fonctionnalité nécessaire du verrou. Vous pouvez la comparer avec la configuration 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 ;
  • Réentrée, dans un environnement distribué, si le même thread sur le même nœud acquiert le verrou, une autre requête peut toujours réussir
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");
            }    
        }
    }

    这里我们一眼就可以看出问题来:GETDEL是两个分开的操作,在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

    Méthode d'implémentation

    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.

    rrreee🎜Un problème fatal avec cette solution est que si un thread ne peut pas effectuer l'opération de déverrouillage normalement en raison de certains facteurs anormaux (tels qu'un temps d'arrêt) après l'acquisition du verrou, celui-ci ne sera jamais libéré. 🎜🎜À cette fin, nous pouvons ajouter un délai d'attente à ce verrou🎜🎜L'effet de l'exécution de 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算法就是为了解决这个问题

    使用 Redlock,需要提供多个 Redis 实例,这些实例之前相互独立没有主从关系。同很多分布式算法一样,redlock 也使用大多数机制

    加锁时,它会向过半节点发送 set指令,只要过半节点 set 成功,那就认为加锁成功。释放锁时,需要向所有节点发送 del 指令。不过 Redlock 算法还需要考虑出错重试、时钟漂移等很多细节问题,同时因为 Redlock 需要向多个节点进行读写,意味着相比单实例 Redis 性能会下降一些

    Redlock 算法是在单 Redis 节点基础上引入的高可用模式,Redlock 基于 N 个完全独立的 Redis 节点,一般是大于 3 的奇数个(通常情况下 N 可以设置为 5),可以基本保证集群内各个节点不会同时宕机。

    假设当前集群有 5 个节点,运行 Redlock 算法的客户端依次执行下面各个步骤,来完成获取锁的操作

    • 客户端记录当前系统时间,以毫秒为单位;
    • 依次尝试从 5 个 Redis 实例中,使用相同的 key 获取锁,当向 Redis 请求获取锁时,客户端应该设置一个网络连接和响应超时时间,超时时间应该小于锁的失效时间,避免因为网络故障出现的问题;
    • 客户端使用当前时间减去开始获取锁时间就得到了获取锁使用的时间,当且仅当从半数以上的 Redis 节点获取到锁,并且当使用的时间小于锁失效时间时,锁才算获取成功;
    • 如果获取到了锁,key 的真正有效时间等于有效时间减去获取锁所使用的时间,减少超时的几率;
    • 如果获取锁失败,客户端应该在所有的 Redis 实例上进行解锁,即使是上一步操作请求失败的节点,防止因为服务端响应消息丢失,但是实际数据添加成功导致的不一致。

    也就是说,假设锁30秒过期,三个节点加锁花了31秒,自然是加锁失败了

    在 Redis 官方推荐的 Java 客户端 Redisson 中,内置了对 RedLock

    Par défaut, le temps de verrouillage est de 30 secondes. Si l'affaire verrouillée n'est pas terminée, alors Quand. 30-10 = 20 secondes, un renouvellement sera effectué et le verrouillage sera réinitialisé à 30 secondes

    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 que Redlock doit lire et écrire sur plusieurs nœuds, cela signifie que les performances de Redis. sera inférieur à celui d'une seule instance.

    Redlock 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.

    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

      Si le verrou est acquis, le temps effectif réel de la clé est égal au temps effectif moins le temps utilisé pour acquérir le verrou, réduisant ainsi le temps effectif de la clé. le risque d'expiration du délai ;
    • Si l'acquisition du verrou échoue, le client doit le déverrouiller sur toutes les instances Redis, même s'il s'agit du nœud sur lequel la demande d'opération précédente a échoué, pour éviter l'incohérence causée par la perte du message de réponse du serveur mais le les données réelles sont ajoutées avec succès.
    En d'autres termes, en supposant que le verrou expire dans 30 secondes, il faut 31 secondes pour verrouiller les trois nœuds. Naturellement, le verrouillage échoue

    Dans le client Java Redisson officiellement recommandé par Redis. , Implémentation intégrée de RedLock

    https://redis.io/topics/distlock

    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!

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