Maison > Java > javaDidacticiel > Dois-je utiliser Redis ou Zookeeper pour les verrous distribués ?

Dois-je utiliser Redis ou Zookeeper pour les verrous distribués ?

Libérer: 2023-08-22 15:48:46
avant
740 Les gens l'ont consulté

Les verrous distribués sont généralement implémentés dans :

  • base de données
  • cache (par exemple : Redis)
  • Zookeeper
  • etcd

Dans le développement réel, utilisez The les plus courants sont Redis et Zookeeper, cet article ne parlera donc que de ces deux-là.

Avant de discuter de ce problème, examinons d'abord un scénario commercial :

Le système A est un système de commerce électronique, actuellement déployé sur une machine. Il existe une interface permettant aux utilisateurs de passer des commandes dans le système, mais les utilisateurs doivent vous. Vous devez vérifier l'inventaire pour vous assurer qu'il y a suffisamment d'inventaire avant de passer une commande pour l'utilisateur.

Étant donné que le système a une certaine concurrence, l'inventaire des marchandises sera enregistré à l'avance dans l'inventaire de Redis中,用户下单的时候会更新Redis.

L'architecture du système à ce moment est la suivante :

Dois-je utiliser Redis ou Zookeeper pour les verrous distribués ?

Mais cela va créer un problème : Si à un certain moment, l'inventaire d'un certain produit dans redis est 1, à ce moment deux demandes arrivent à en même temps, dont l'un Après avoir exécuté l'étape 3 dans la figure ci-dessus, l'inventaire dans la base de données est mis à jour à 0, mais l'étape 4 n'a pas encore été exécutée.

L'autre demande a atteint l'étape 2 et a constaté que l'inventaire était toujours à 1, elle a donc continué à l'étape 3.

Le résultat est que 2 articles sont vendus, mais en fait il n'y a qu'un seul article en stock.

De toute évidence, quelque chose ne va pas ! Il s'agit d'un problème typique de survente d'inventaire

À ce stade, nous pouvons facilement penser à une solution : utilisez un verrou pour verrouiller les étapes 2, 3 et 4, afin qu'une fois celles-ci terminées, un autre thread puisse intervenir pour exécuter l'étape 2. .

Dois-je utiliser Redis ou Zookeeper pour les verrous distribués ?

Selon la figure ci-dessus, lors de l'exécution de l'étape 2, utilisez synchronisé ou ReentrantLock fourni par Java pour verrouiller, puis relâchez le verrou une fois l'étape 4 exécutée.

De cette façon, les trois étapes 2, 3 et 4 sont "verrouillées" et plusieurs threads ne peuvent être exécutés qu'en série.

Mais les bons moments n'ont pas duré longtemps, la concurrence de l'ensemble du système s'est envolée et une machine ne pouvait plus la gérer. Nous devons maintenant ajouter une machine, comme indiqué ci-dessous :

Dois-je utiliser Redis ou Zookeeper pour les verrous distribués ?

Après avoir ajouté la machine, le système devient comme indiqué dans l'image ci-dessus, mon Dieu !

Supposons que les demandes de deux utilisateurs arrivent en même temps, mais tombent sur des machines différentes. Ces deux demandes peuvent-elles être exécutées en même temps, ou le problème de survente d'inventaire se produira-t-il ?

Pourquoi ? Étant donné que les deux systèmes A dans l'image ci-dessus s'exécutent dans deux JVM différentes, les verrous qu'ils ajoutent ne sont valides que pour les threads de leur propre JVM et ne sont pas valides pour les threads d'autres JVM.

Donc, le problème ici est le suivant : le mécanisme de verrouillage natif fourni par Java échoue dans un scénario de déploiement multi-machines

C'est parce que les verrous ajoutés par les deux machines ne sont pas les mêmes (les deux verrous sont dans des JVM différentes) .

Alors, tant que nous nous assurons que les verrous ajoutés aux deux machines sont les mêmes, le problème ne sera-t-il pas résolu ?

À ce stade, il est temps que les verrous distribués fassent leur grande apparition. L'idée des verrous distribués est la suivante :

Fournir une "chose" globale et unique pour acquérir des verrous dans l'ensemble du système, puis dans chaque système. peut se verrouiller quand il le faut, tous demandent à cette "chose" d'obtenir un verrou, afin que différents systèmes puissent le considérer comme le même verrou.

Quant à cette "chose", cela peut être Redis, Zookeeper, ou une base de données.

La description textuelle n'est pas très intuitive, regardons l'image ci-dessous :

Dois-je utiliser Redis ou Zookeeper pour les verrous distribués ?
Grâce à l'analyse ci-dessus, nous savons que dans le cas de scénarios de survente d'inventaire, l'utilisation du mécanisme de verrouillage natif de Java ne peut pas garantir la sécurité des threads dans un système distribué. système de déploiement. Nous devons donc utiliser une solution de verrouillage distribué.

Alors, comment mettre en œuvre des verrous distribués ? Alors lisez la suite !

Implémentation de verrous distribués basés sur Redis

L'analyse ci-dessus explique pourquoi les verrous distribués doivent être utilisés. Nous examinerons ici de plus près comment les verrous distribués doivent être gérés une fois implémentés.

La solution la plus courante consiste à utiliser Redis comme verrou distribué

L'idée d'utiliser Redis pour les verrous distribués est à peu près la suivante : définissez une valeur dans redis pour indiquer que le verrou est ajouté, puis supprimez la clé lorsque le verrou est libéré.

Le code spécifique est le suivant :

// 获取锁
// NX是指如果key不存在就成功,key存在返回false,PX可以指定过期时间
SET anyLock unique_value NX PX 30000

// 释放锁:通过执行一段lua脚本
// 释放锁涉及到两条指令,这两条指令不是原子性的
// 需要用到redis的lua脚本支持特性,redis执行lua脚本是原子性的
if redis.call("get",KEYS[1]) == ARGV[1] then
return redis.call("del",KEYS[1])
else
return 0
end
Copier après la connexion

Il y a plusieurs points importants dans cette méthode :

  • Assurez-vous d'utiliser la commande SET key value NX PX milliseconds

    Sinon, définissez d'abord la valeur, puis définir le délai d'expiration, ce n'est pas une opération atomique. Il peut planter avant de définir le délai d'expiration, ce qui provoquera un blocage (la clé existe en permanence)

  • La valeur doit être unique

    C'est à vérifier. que la valeur est et lors du déverrouillage La clé ne sera supprimée que si les serrures sont cohérentes.

    Cela évite une situation : supposons que A acquière le verrou et que le délai d'expiration soit de 30 s. Après 35 s, le verrou a été automatiquement libéré, mais B peut avoir acquis le verrou à ce moment-là. Le client A ne peut pas supprimer le verrou de B.

Dois-je utiliser Redis ou Zookeeper pour les verrous distribués ?

En plus de considérer la manière dont le client implémente les verrous distribués, vous devez également envisager le déploiement de Redis.

Redis dispose de 3 méthodes de déploiement :

  • Mode mono-machine
  • mode d'élection maître-esclave + sentinelle
  • mode cluster redis

L'inconvénient de l'utilisation de redis pour les verrous distribués est : si vous utiliser une seule machine En mode déploiement, il n'y aura qu'un seul point de problème, tant que Redis échoue. Le verrouiller ne fonctionnera pas.

Adoptez le mode maître-esclave. Lors du verrouillage, un seul nœud est verrouillé Même si la haute disponibilité est obtenue via sentinelle, si le nœud maître tombe en panne et qu'un commutateur maître-esclave se produit, le problème de perte de verrouillage peut survenir.

Sur la base des considérations ci-dessus, en fait, l'auteur de redis a également réfléchi à cette question. Il a proposé un algorithme RedLock. La signification de cet algorithme est à peu près la suivante :

Supposons que le mode de déploiement de redis soit un cluster redis. il y a un total de 5 maîtres, obtenez un verrou en suivant les étapes suivantes :

  • 获取当前时间戳,单位是毫秒
  • 轮流尝试在每个master节点上创建锁,过期时间设置较短,一般就几十毫秒
  • 尝试在大多数节点上建立一个锁,比如5个节点就要求是3个节点(n / 2 +1)
  • 客户端计算建立好锁的时间,如果建立锁的时间小于超时时间,就算建立成功了
  • 要是锁建立失败了,那么就依次删除这个锁
  • 只要别人建立了一把分布式锁,你就得不断轮询去尝试获取锁

但是这样的这种算法还是颇具争议的,可能还会存在不少的问题,无法保证加锁的过程一定正确。

Dois-je utiliser Redis ou Zookeeper pour les verrous distribués ?

另一种方式:Redisson

此外,实现Redis的分布式锁,除了自己基于redis client原生api来实现之外,还可以使用开源框架:Redission

Redisson是一个企业级的开源Redis Client,也提供了分布式锁的支持。我也非常推荐大家使用,为什么呢?

回想一下上面说的,如果自己写代码来通过redis设置一个值,是通过下面这个命令设置的。

  • SET anyLock unique_value NX PX 30000

这里设置的超时时间是30s,假如我超过30s都还没有完成业务逻辑的情况下,key会过期,其他线程有可能会获取到锁。

这样一来的话,第一个线程还没执行完业务逻辑,第二个线程进来了也会出现线程安全问题。所以我们还需要额外的去维护这个过期时间,太麻烦了~

我们来看看redisson是怎么实现的?先感受一下使用redission的爽:

Config config = new Config();
config.useClusterServers()
.addNodeAddress("redis://192.168.31.101:7001")
.addNodeAddress("redis://192.168.31.101:7002")
.addNodeAddress("redis://192.168.31.101:7003")
.addNodeAddress("redis://192.168.31.102:7001")
.addNodeAddress("redis://192.168.31.102:7002")
.addNodeAddress("redis://192.168.31.102:7003");

RedissonClient redisson = Redisson.create(config);


RLock lock = redisson.getLock("anyLock");
lock.lock();
lock.unlock();
Copier après la connexion

就是这么简单,我们只需要通过它的api中的lock和unlock即可完成分布式锁,他帮我们考虑了很多细节:

  • redisson所有指令都通过lua脚本执行,redis支持lua脚本原子性执行

  • redisson设置一个key的默认过期时间为30s,如果某个客户端持有一个锁超过了30s怎么办?

    redisson中有一个watchdog的概念,翻译过来就是看门狗,它会在你获取锁之后,每隔10秒帮你把key的超时时间设为30s

    这样的话,就算一直持有锁也不会出现key过期了,其他线程获取到锁的问题了。

  • redisson的“看门狗”逻辑保证了没有死锁发生。

    (如果机器宕机了,看门狗也就没了。此时就不会延长key的过期时间,到了30s之后就会自动过期了,其他线程可以获取到锁)

Dois-je utiliser Redis ou Zookeeper pour les verrous distribués ?

这里稍微贴出来其实现代码:

// 加锁逻辑
private <T> RFuture<Long> tryAcquireAsync(long leaseTime, TimeUnit unit, final long threadId) {
    if (leaseTime != -1) {
        return tryLockInnerAsync(leaseTime, unit, threadId, RedisCommands.EVAL_LONG);
    }
    // 调用一段lua脚本,设置一些key、过期时间
    RFuture<Long> ttlRemainingFuture = tryLockInnerAsync(commandExecutor.getConnectionManager().getCfg().getLockWatchdogTimeout(), TimeUnit.MILLISECONDS, threadId, RedisCommands.EVAL_LONG);
    ttlRemainingFuture.addListener(new FutureListener<Long>() {
        @Override
        public void operationComplete(Future<Long> future) throws Exception {
            if (!future.isSuccess()) {
                return;
            }

            Long ttlRemaining = future.getNow();
            // lock acquired
            if (ttlRemaining == null) {
                // 看门狗逻辑
                scheduleExpirationRenewal(threadId);
            }
        }
    });
    return ttlRemainingFuture;
}


<T> RFuture<T> tryLockInnerAsync(long leaseTime, TimeUnit unit, long threadId, RedisStrictCommand<T> command) {
    internalLockLeaseTime = unit.toMillis(leaseTime);

    return commandExecutor.evalWriteAsync(getName(), LongCodec.INSTANCE, command,
              "if (redis.call(&#39;exists&#39;, KEYS[1]) == 0) then " +
                  "redis.call(&#39;hset&#39;, KEYS[1], ARGV[2], 1); " +
                  "redis.call(&#39;pexpire&#39;, KEYS[1], ARGV[1]); " +
                  "return nil; " +
              "end; " +
              "if (redis.call(&#39;hexists&#39;, KEYS[1], ARGV[2]) == 1) then " +
                  "redis.call(&#39;hincrby&#39;, KEYS[1], ARGV[2], 1); " +
                  "redis.call(&#39;pexpire&#39;, KEYS[1], ARGV[1]); " +
                  "return nil; " +
              "end; " +
              "return redis.call(&#39;pttl&#39;, KEYS[1]);",
                Collections.<Object>singletonList(getName()), internalLockLeaseTime, getLockName(threadId));
}



// 看门狗最终会调用了这里
private void scheduleExpirationRenewal(final long threadId) {
    if (expirationRenewalMap.containsKey(getEntryName())) {
        return;
    }

    // 这个任务会延迟10s执行
    Timeout task = commandExecutor.getConnectionManager().newTimeout(new TimerTask() {
        @Override
        public void run(Timeout timeout) throws Exception {

            // 这个操作会将key的过期时间重新设置为30s
            RFuture<Boolean> future = renewExpirationAsync(threadId);

            future.addListener(new FutureListener<Boolean>() {
                @Override
                public void operationComplete(Future<Boolean> future) throws Exception {
                    expirationRenewalMap.remove(getEntryName());
                    if (!future.isSuccess()) {
                        log.error("Can&#39;t update lock " + getName() + " expiration", future.cause());
                        return;
                    }

                    if (future.getNow()) {
                        // reschedule itself
                        // 通过递归调用本方法,无限循环延长过期时间
                        scheduleExpirationRenewal(threadId);
                    }
                }
            });
        }

    }, internalLockLeaseTime / 3, TimeUnit.MILLISECONDS);

    if (expirationRenewalMap.putIfAbsent(getEntryName(), new ExpirationEntry(threadId, task)) != null) {
        task.cancel();
    }
}
Copier après la connexion

另外,redisson还提供了对redlock算法的支持,

它的用法也很简单:

RedissonClient redisson = Redisson.create(config);
RLock lock1 = redisson.getFairLock("lock1");
RLock lock2 = redisson.getFairLock("lock2");
RLock lock3 = redisson.getFairLock("lock3");
RedissonRedLock multiLock = new RedissonRedLock(lock1, lock2, lock3);
multiLock.lock();
multiLock.unlock();
Copier après la connexion

小结

本节分析了使用Redis作为分布式锁的具体落地方案,以及其一些局限性,然后介绍了一个Redis的客户端框架redisson。这也是我推荐大家使用的,比自己写代码实现会少care很多细节。

基于zookeeper实现分布式锁

常见的分布式锁实现方案里面,除了使用redis来实现之外,使用zookeeper也可以实现分布式锁。

在介绍zookeeper(下文用zk代替)实现分布式锁的机制之前,先粗略介绍一下zk是什么东西:

Zookeeper是一种提供配置管理、分布式协同以及命名的中心化服务。

zk的模型是这样的:zk包含一系列的节点,叫做znode,就好像文件系统一样每个znode表示一个目录,然后znode有一些特性:

  • Nœud ordonné : s'il existe actuellement un nœud parent /lock,我们可以在这个父节点下面创建子节点;

    zookeeper提供了一个可选的有序特性,例如我们可以创建子节点“/lock/node-”并且指明有序,那么zookeeper在生成子节点时会根据当前的子节点数量自动添加整数序号

    也就是说,如果是第一个创建的子节点,那么生成的子节点为/lock/node-0000000000,下一个节点则为/lock/node-0000000001

    zookeeper fournit une fonctionnalité de classement facultative, par exemple, nous pouvons créer un nœud enfant "/lock/node-" et spécifier l'ordre, puis zookeeper ajoutera automatiquement un numéro de série entier basé sur le nombre actuel de nœuds enfants lors de la génération de nœuds enfants
  • C'est-à-dire que s'il s'agit du premier nœud enfant créé, alors le nœud enfant généré sera /lock/node-0000000000, le nœud suivant est /lock/node-0000000001, et ainsi de suite.
  • Nœud temporaire : le client peut créer un nœud temporaire. Zookeeper supprimera automatiquement le nœud après la fin de la session ou l'expiration de la session.
  • Surveillance des événements : lors de la lecture des données, nous pouvons définir la surveillance des événements sur le nœud en même temps. Lorsque les données ou la structure du nœud changent, zookeeper en informera le client. Actuellement, zookeeper propose les quatre événements suivants :

    • Création de nœud
    • Suppression de nœud
    • Modification des données de nœud
    Changement de nœud enfant

  1. Basé sur certains de ce qui précède En raison des caractéristiques de zk, nous pouvons facilement élaborer un plan d'implémentation consistant à utiliser zk pour implémenter des verrous distribués :

  2. En utilisant les nœuds temporaires et les nœuds ordonnés de zk, chaque thread acquérant le verrou signifie créer un nœud ordonné temporaire nœud dans zk. Par exemple, dans le répertoire /lock/.
  3. Après avoir créé avec succès le nœud, obtenez tous les nœuds temporaires dans le répertoire /lock, puis déterminez si le nœud créé par le thread actuel est le nœud avec le plus petit numéro de série de tous les nœuds
  4. Si le nœud créé par le thread actuel est le nœud avec le plus petit numéro de séquence de tous les nœuds, on considère que l'acquisition du verrou est réussie.
  5. 🎜🎜Si le nœud créé par le thread actuel n'est pas le nœud avec le plus petit numéro de série de tous les nœuds, ajoutez un écouteur d'événement au nœud avant le numéro de série du nœud. 🎜

    比如当前线程获取到的节点序号为/lock/003,然后所有的节点列表为[/lock/001,/lock/002,/lock/003],则对/lock/002这个节点添加一个事件监听器。

如果锁释放了,会唤醒下一个序号的节点,然后重新执行第3步,判断是否自己的节点序号是最小。

比如/lock/001释放了,/lock/002监听到时间,此时节点集合为[/lock/002,/lock/003],则/lock/002为最小序号节点,获取到锁。

整个过程如下:

Dois-je utiliser Redis ou Zookeeper pour les verrous distribués ?

具体的实现思路就是这样,至于代码怎么写,这里比较复杂就不贴出来了。

Curator介绍

Curator是一个zookeeper的开源客户端,也提供了分布式锁的实现。

他的使用方式也比较简单:

InterProcessMutex interProcessMutex = new InterProcessMutex(client,"/anyLock");
interProcessMutex.acquire();
interProcessMutex.release();
Copier après la connexion

其实现分布式锁的核心源码如下:

private boolean internalLockLoop(long startMillis, Long millisToWait, String ourPath) throws Exception
{
    boolean  haveTheLock = false;
    boolean  doDelete = false;
    try {
        if ( revocable.get() != null ) {
            client.getData().usingWatcher(revocableWatcher).forPath(ourPath);
        }

        while ( (client.getState() == CuratorFrameworkState.STARTED) && !haveTheLock ) {
            // 获取当前所有节点排序后的集合
            List<String>        children = getSortedChildren();
            // 获取当前节点的名称
            String              sequenceNodeName = ourPath.substring(basePath.length() + 1); // +1 to include the slash
            // 判断当前节点是否是最小的节点
            PredicateResults    predicateResults = driver.getsTheLock(client, children, sequenceNodeName, maxLeases);
            if ( predicateResults.getsTheLock() ) {
                // 获取到锁
                haveTheLock = true;
            } else {
                // 没获取到锁,对当前节点的上一个节点注册一个监听器
                String  previousSequencePath = basePath + "/" + predicateResults.getPathToWatch();
                synchronized(this){
                    Stat stat = client.checkExists().usingWatcher(watcher).forPath(previousSequencePath);
                    if ( stat != null ){
                        if ( millisToWait != null ){
                            millisToWait -= (System.currentTimeMillis() - startMillis);
                            startMillis = System.currentTimeMillis();
                            if ( millisToWait <= 0 ){
                                doDelete = true;    // timed out - delete our node
                                break;
                            }
                            wait(millisToWait);
                        }else{
                            wait();
                        }
                    }
                }
                // else it may have been deleted (i.e. lock released). Try to acquire again
            }
        }
    }
    catch ( Exception e ) {
        doDelete = true;
        throw e;
    } finally{
        if ( doDelete ){
            deleteOurPath(ourPath);
        }
    }
    return haveTheLock;
}
Copier après la connexion

其实curator实现分布式锁的底层原理和上面分析的是差不多的。这里我们用一张图详细描述其原理:

Dois-je utiliser Redis ou Zookeeper pour les verrous distribués ?
图片

小结

本节介绍了Zookeeperr实现分布式锁的方案以及zk的开源客户端的基本使用,简要的介绍了其实现原理。

Comparaison des avantages et des inconvénients des deux solutions

Après avoir appris les deux solutions d'implémentation de verrous distribués, cette section doit discuter des avantages et des inconvénients respectifs des solutions d'implémentation redis et zk.

Pour le verrou distribué de Redis, il présente les inconvénients suivants :

  • La façon d'acquérir le verrou est simple et grossière S'il ne parvient pas à acquérir le verrou, il continuera à essayer d'acquérir le verrou, ce qui consomme des performances. .
  • De plus, le positionnement de conception de redis détermine que ses données ne sont pas fortement cohérentes. Dans certains cas extrêmes, des problèmes peuvent survenir. Le modèle de verrouillage n'est pas assez robuste
  • Même s'il est implémenté à l'aide de l'algorithme redlock, dans certains scénarios complexes, rien ne garantit que sa mise en œuvre sera à 100 % sans problème. Pour une discussion sur redlock, voir Comment faire. faire du verrouillage distribué
  • distribution redis Pour les verrous, vous devez en fait constamment essayer d'acquérir le verrou vous-même, ce qui consomme plus de performances.

Mais d'un autre côté, l'utilisation de Redis pour implémenter des verrous distribués est très courante dans de nombreuses entreprises, et dans la plupart des cas, vous ne rencontrerez pas les soi-disant « scénarios extrêmement complexes »

Donc, utiliser Redis comme verrou distribué est ce n'est pas une mauvaise idée. La chose la plus importante dans une bonne solution est que Redis ait des performances élevées et puisse prendre en charge les opérations d'acquisition et de verrouillage de libération à haute concurrence.

Pour les serrures distribuées zk :

  • Le positionnement naturel de la conception de zookeeper est une coordination distribuée et une forte cohérence. Le modèle de serrure est robuste, facile à utiliser et adapté aux serrures distribuées.
  • Si vous ne parvenez pas à obtenir le verrou, il vous suffit d'ajouter un auditeur. Il n'est pas nécessaire d'interroger tout le temps et la consommation de performances est faible.

Mais zk a aussi ses défauts : s'il y a plus de clients qui demandent fréquemment des verrous et libèrent des verrous, la pression sur le cluster zk sera plus grande.

Résumé :

En résumé, Redis et Zookeeper ont tous deux leurs avantages et leurs inconvénients. Nous pouvons utiliser ces questions comme facteurs de référence lors de la sélection technologique.

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!

Étiquettes associées:
source:Java后端技术全栈
Déclaration de ce site Web
Le contenu de cet article est volontairement contribué par les internautes et les droits d'auteur appartiennent à l'auteur original. Ce site n'assume aucune responsabilité légale correspondante. Si vous trouvez un contenu suspecté de plagiat ou de contrefaçon, veuillez contacter admin@php.cn
Tutoriels populaires
Plus>
Derniers téléchargements
Plus>
effets Web
Code source du site Web
Matériel du site Web
Modèle frontal