Le problème du verrouillage distribué de la structure maître-esclave
Le moyen le plus simple d'implémenter le verrouillage distribué Redis est de créer une clé dans Redis. Cette clé a un délai d'expiration (TTL). Il est garanti que le verrou sera éventuellement libéré automatiquement. Lorsque le client libère la ressource (la déverrouille), la clé sera supprimée.
En apparence, cela semble bien fonctionner, mais il existe un grave problème de point de défaillance unique : que se passe-t-il si Redis se bloque ? On pourrait dire que ce problème peut être résolu en ajoutant un nœud esclave. Mais cela ne fonctionne généralement pas. La synchronisation maître-esclave de Redis est généralement asynchrone, elle ne peut donc pas permettre une utilisation exclusive des ressources.
Il existe une condition de concurrence évidente dans ce scénario (structure maître-esclave) :
Le client A acquiert le verrou du maître
Avant que le maître ne synchronise le verrou avec l'esclave, le maître échoue.
Le nœud esclave est promu au rang de nœud maître
Le client B acquiert le verrou du nouveau maître
La ressource correspondant à ce verrou a déjà été acquise par le client A. Échec de la sécurité !
Parfois, le programme a beaucoup de chance. Par exemple, lorsqu'un nœud raccroche, plusieurs clients obtiennent le verrou en même temps. Tant que vous pouvez tolérer cette faible probabilité d’erreur, il n’y a aucun problème à adopter cette solution basée sur la réplication. Dans le cas contraire, nous vous recommandons de mettre en œuvre la solution décrite ci-dessous.
Introduction
Redis a introduit le concept de cadenas rouge pour cette situation. Dans un système de verrouillage rouge, le signe de réussite de l’acquisition ou de la libération d’un verrou est que l’opération réussit sur plus de la moitié des nœuds.
Principe
En supposant qu'il existe N maîtres Redis dans l'environnement distribué de Redis. Ces nœuds sont complètement indépendants les uns des autres et il n'existe pas de réplication maître-esclave ni d'autre mécanisme de coordination de cluster. Nous avons déjà expliqué comment acquérir et libérer des verrous en toute sécurité dans une seule instance de Redis. Nous garantissons que le verrou sera acquis et libéré en utilisant cette méthode sur chaque (N) instance. Dans cet exemple, nous supposons qu'il y a 5 nœuds maîtres Redis. Il s'agit d'un paramètre plus raisonnable, nous devons donc exécuter ces instances sur 5 machines ou 5 machines virtuelles pour nous assurer qu'elles ne tomberont pas en panne en même temps.
Pour obtenir le verrou, le client doit effectuer les opérations suivantes :
Obtenir l'heure Unix actuelle en millisecondes.
Essayez d'acquérir des verrous à partir de N instances dans l'ordre, en utilisant la même clé et la même valeur aléatoire.
Lors de la définition d'un verrou sur Redis, le client doit définir une connexion réseau et un délai d'expiration de réponse, qui doivent être inférieurs au délai d'expiration du verrou.
Si votre verrou expire automatiquement dans 10 secondes, le délai d'attente doit être réglé entre 5 et 50 millisecondes. Empêchez le client d'attendre indéfiniment les résultats de la réponse, même lorsque le serveur Redis est en panne. Lorsqu'aucune réponse du serveur n'est reçue dans le délai spécifié, le client doit essayer de se connecter à d'autres instances Redis dès que possible.
Le client utilise l'heure actuelle moins l'heure de début pour acquérir le verrou (l'heure enregistrée à l'étape 1) pour obtenir l'heure utilisée pour acquérir le verrou.
La condition pour réussir l'acquisition du verrou est que le verrou doit être acquis à partir de la majorité des nœuds Redis (3 nœuds), et le temps d'utilisation ne peut pas dépasser le délai d'expiration du verrou.
Si la serrure est acquise, le temps de validité réel de la clé est égal au temps de validité moins le temps utilisé pour acquérir la serrure (le résultat calculé à l'étape 3).
Si pour une raison quelconque, l'acquisition du verrou échoue (le verrou n'est pas acquis sur au moins N/2+1 instances Redis ou le temps d'acquisition du verrou a dépassé le temps effectif), le client doit se déverrouiller sur toutes les instances Redis ( Même si certaines instances Redis ne sont pas verrouillées du tout correctement).
Site officiel
Github officiel : 8. Verrouillage et synchroniseur distribués · redisson/redisson Wik
L'objet Redisson red lock RedissonRedLock basé sur Redis implémente l'algorithme de verrouillage introduit par Redlock. Cet objet peut également être utilisé pour associer plusieurs objets RLock en tant que verrou rouge. Chaque instance d'objet RLock peut provenir d'une instance Redisson différente.
RLock lock1 = redissonInstance1.getLock("lock1"); RLock lock2 = redissonInstance2.getLock("lock2"); RLock lock3 = redissonInstance3.getLock("lock3"); RedissonRedLock lock = new RedissonRedLock(lock1, lock2, lock3); // 同时加锁:lock1 lock2 lock3 // 红锁在大部分节点上加锁成功就算成功。 lock.lock(); ... lock.unlock();
Tout le monde sait que si certains nœuds Redis responsables du stockage de certains verrous distribués tombent en panne et que ces verrous sont verrouillés, ces verrous le seront. Afin d'éviter cette situation, Redisson fournit en interne un chien de garde qui surveille le verrou. Sa fonction est de prolonger en permanence la période de validité du verrou avant la fermeture de l'instance Redisson. Par défaut, le délai d'expiration de la vérification du verrouillage du chien de garde est de 30 secondes, ce qui peut également être spécifié en modifiant Config.lockWatchdogTimeout.
Redisson peut également spécifier l'heure de verrouillage en verrouillant et en définissant le paramètre leaseTime. Une fois ce délai écoulé, le verrou se déverrouillera automatiquement.
RedissonRedLock lock = new RedissonRedLock(lock1, lock2, lock3); // 给lock1,lock2,lock3加锁,如果没有手动解开的话,10秒钟后将会自动解开 lock.lock(10, TimeUnit.SECONDS); // 为加锁等待100秒时间,并在加锁成功10秒钟后自动解开 boolean res = lock.tryLock(100, 10, TimeUnit.SECONDS); ... lock.unlock();
RedissonRedLock étend RedissonMultiLock, donc en fait, redLock.tryLock appelle en fait : org.redisson.RedissonMultiLock.java#tryLock(), puis appelle son tryLock similaire (long waitTime, long bailTime, unité TimeUnit ), le paramètre d'entrée est : tryLock(-1, -1, null)
org.redisson.RedissonMultiLock.java#tryLock(long waitTime, long leaseTime, TimeUnit unit)源码如下:
final List<RLock> locks = new ArrayList<>(); /** * Creates instance with multiple {@link RLock} objects. * Each RLock object could be created by own Redisson instance. * * @param locks - array of locks */ public RedissonMultiLock(RLock... locks) { if (locks.length == 0) { throw new IllegalArgumentException("Lock objects are not defined"); } this.locks.addAll(Arrays.asList(locks)); } public boolean tryLock(long waitTime, long leaseTime, TimeUnit unit) throws InterruptedException { long newLeaseTime = -1; if (leaseTime != -1) { newLeaseTime = unit.toMillis(waitTime)*2; } long time = System.currentTimeMillis(); long remainTime = -1; if (waitTime != -1) { remainTime = unit.toMillis(waitTime); } long lockWaitTime = calcLockWaitTime(remainTime); /** * 1. 允许加锁失败节点个数限制(N-(N/2+1)) */ int failedLocksLimit = failedLocksLimit(); /** * 2. 遍历所有节点通过EVAL命令执行lua加锁 */ List<RLock> acquiredLocks = new ArrayList<>(locks.size()); for (ListIterator<RLock> iterator = locks.listIterator(); iterator.hasNext();) { RLock lock = iterator.next(); boolean lockAcquired; /** * 3.对节点尝试加锁 */ try { if (waitTime == -1 && leaseTime == -1) { lockAcquired = lock.tryLock(); } else { long awaitTime = Math.min(lockWaitTime, remainTime); lockAcquired = lock.tryLock(awaitTime, newLeaseTime, TimeUnit.MILLISECONDS); } } catch (RedisResponseTimeoutException e) { // 如果抛出这类异常,为了防止加锁成功,但是响应失败,需要解锁所有节点 unlockInner(Arrays.asList(lock)); lockAcquired = false; } catch (Exception e) { // 抛出异常表示获取锁失败 lockAcquired = false; } if (lockAcquired) { /** *4. 如果获取到锁则添加到已获取锁集合中 */ acquiredLocks.add(lock); } else { /** * 5. 计算已经申请锁失败的节点是否已经到达 允许加锁失败节点个数限制 (N-(N/2+1)) * 如果已经到达, 就认定最终申请锁失败,则没有必要继续从后面的节点申请了 * 因为 Redlock 算法要求至少N/2+1 个节点都加锁成功,才算最终的锁申请成功 */ if (locks.size() - acquiredLocks.size() == failedLocksLimit()) { break; } if (failedLocksLimit == 0) { unlockInner(acquiredLocks); if (waitTime == -1 && leaseTime == -1) { return false; } failedLocksLimit = failedLocksLimit(); acquiredLocks.clear(); // reset iterator while (iterator.hasPrevious()) { iterator.previous(); } } else { failedLocksLimit--; } } /** * 6.计算 目前从各个节点获取锁已经消耗的总时间,如果已经等于最大等待时间,则认定最终申请锁失败,返回false */ if (remainTime != -1) { remainTime -= System.currentTimeMillis() - time; time = System.currentTimeMillis(); if (remainTime <= 0) { unlockInner(acquiredLocks); return false; } } } if (leaseTime != -1) { List<RFuture<Boolean>> futures = new ArrayList<>(acquiredLocks.size()); for (RLock rLock : acquiredLocks) { RFuture<Boolean> future = ((RedissonLock) rLock).expireAsync(unit.toMillis(leaseTime), TimeUnit.MILLISECONDS); futures.add(future); } for (RFuture<Boolean> rFuture : futures) { rFuture.syncUninterruptibly(); } } /** * 7.如果逻辑正常执行完则认为最终申请锁成功,返回true */ return true; }
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!