Maison  >  Article  >  développement back-end  >  Comment implémenter des verrous distribués basés sur Redis

Comment implémenter des verrous distribués basés sur Redis

无忌哥哥
无忌哥哥original
2018-07-20 09:14:481514parcourir

Avant-propos

Les verrous distribués sont largement utilisés dans les applications distribuées. Si vous voulez comprendre une nouvelle chose, vous devez d'abord comprendre son origine, afin de mieux la comprendre et même de tirer des conclusions.

Tout d’abord, lorsqu’on parle de verrous distribués, on pense naturellement aux applications distribuées.

Dans le système autonome avant de diviser l'application en applications distribuées, lors de la lecture des ressources publiques dans certains scénarios simultanés, tels que la déduction de l'inventaire et la vente de billets, nous pouvons simplement utiliser la synchronisation ou le verrouillage. .

Mais une fois l'application distribuée, le système passe du précédent programme mono-processus et multi-thread à un programme multi-processus et multi-thread. À l'heure actuelle, la solution ci-dessus n'est évidemment pas suffisante.

Par conséquent, une solution courante dans l'industrie consiste généralement à s'appuyer sur un composant tiers et à utiliser sa propre exclusivité pour parvenir à l'exclusion mutuelle de plusieurs processus. Tels que :

  • Index unique basé sur DB.

  • Nœuds ordonnés temporaires basés sur ZK.

  • Basé sur les paramètres NX EX de Redis.

La discussion ici est principalement basée sur Redis.

Implémentation

Puisque Redis est choisi, il doit être exclusif. Dans le même temps, il est préférable d'avoir quelques fonctionnalités de base des serrures :

  • Hautes performances (hautes performances lors de l'ajout et du déverrouillage)

  • Vous pouvez utiliser des verrous bloquants avec un verrou non bloquant.

  • Aucune impasse ne peut survenir.

  • Disponibilité (le verrou ne peut pas échouer une fois le nœud en panne).

L'utilisation d'un paramètre NX dans Redis set key ici peut garantir une écriture réussie même si la clé n'existe pas. Et l'ajout du paramètre EX permet à la clé d'être automatiquement supprimée après l'expiration du délai.

Ainsi, l'utilisation des deux fonctionnalités ci-dessus peut garantir qu'un seul processus obtiendra le verrou en même temps, et aucun blocage ne se produira (le pire des cas est que la clé est automatiquement supprimée après l'expiration du délai).

Lock

Le code d'implémentation est le suivant :

    private static final String SET_IF_NOT_EXIST = "NX";
    private static final String SET_WITH_EXPIRE_TIME = "PX";
    
    public  boolean tryLock(String key, String request) {
        String result = this.jedis.set(LOCK_PREFIX + key, request, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, 10 * TIME);

        if (LOCK_MSG.equals(result)){
            return true ;
        }else {
            return false ;
        }
    }

Notez que l'

String set(String key, String value, String nxxx, String expx, long time);

api de jedis est utilisée ici.

Cette commande peut garantir l'atomicité de NX EX.

Assurez-vous de ne pas exécuter les deux commandes (NX EX) séparément S'il y a un problème avec le programme après NX, un blocage peut se produire.

Verrouillage de blocage

Vous pouvez également mettre en place un verrouillage de blocage :

    //一直阻塞
    public void lock(String key, String request) throws InterruptedException {

        for (;;){
            String result = this.jedis.set(LOCK_PREFIX + key, request, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, 10 * TIME);
            if (LOCK_MSG.equals(result)){
                break ;
            }
                
              //防止一直消耗 CPU  
            Thread.sleep(DEFAULT_SLEEP_TIME) ;
        }

    }
    
     //自定义阻塞时间
     public boolean lock(String key, String request,int blockTime) throws InterruptedException {

        while (blockTime >= 0){

            String result = this.jedis.set(LOCK_PREFIX + key, request, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, 10 * TIME);
            if (LOCK_MSG.equals(result)){
                return true ;
            }
            blockTime -= DEFAULT_SLEEP_TIME ;

            Thread.sleep(DEFAULT_SLEEP_TIME) ;
        }
        return false ;
    }

Déverrouillage

Le déverrouillage est également très simple. En fait, il vous suffit de le faire. pour supprimer la clé. Tout ira bien, par exemple, utilisez la commande del key.

Mais la réalité n’est souvent pas si simple.

Si le processus A acquiert le verrou et définit un délai d'attente, mais en raison d'un long cycle d'exécution, le verrou est automatiquement libéré après le délai d'attente. À ce moment-là, le processus B acquiert le verrou et le libère bientôt. De cette façon, le processus B libérera le verrou du processus A.

Le meilleur moyen est donc de déterminer si le cadenas vous appartient à chaque fois que vous le déverrouillez.

À ce stade, il doit être mis en œuvre en conjonction avec le mécanisme de verrouillage.

Vous devez passer un paramètre lors du verrouillage, et utiliser ce paramètre comme valeur de cette clé, afin de pouvoir juger si les valeurs sont égales à chaque déverrouillage.

Le code de déverrouillage ne peut donc pas être un simple del.

    public  boolean unlock(String key,String request){
        //lua script
        String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";

        Object result = null ;
        if (jedis instanceof Jedis){
            result = ((Jedis)this.jedis).eval(script, Collections.singletonList(LOCK_PREFIX + key), Collections.singletonList(request));
        }else if (jedis instanceof JedisCluster){
            result = ((JedisCluster)this.jedis).eval(script, Collections.singletonList(LOCK_PREFIX + key), Collections.singletonList(request));
        }else {
            //throw new RuntimeException("instance is error") ;
            return false ;
        }

        if (UNLOCK_MSG.equals(result)){
            return true ;
        }else {
            return false ;
        }
    }

Un script lua est utilisé ici pour déterminer si les valeurs sont égales, et la commande del ne sera exécutée que si elles sont égales.

L'utilisation de lua peut également garantir l'atomicité des deux opérations ici.

Ainsi, les quatre fonctionnalités de base mentionnées ci-dessus peuvent également être satisfaites :

  • L'utilisation de Redis peut garantir les performances.

  • Voir ci-dessus pour les verrous bloquants et les verrous non bloquants.

  • Utilisez le mécanisme de délai d'attente pour résoudre l'impasse.

  • Redis prend en charge le déploiement de clusters pour améliorer la disponibilité.

En utilisant

J'ai moi-même construit une implémentation complète, et elle a été utilisée en production. Les amis intéressés peuvent l'utiliser immédiatement :

.

Dépendance Maven :

<dependency>
    <groupId>top.crossoverjie.opensource</groupId>
    <artifactId>distributed-redis-lock</artifactId>
    <version>1.0.0</version>
</dependency>

Bean de configuration :

@Configuration
public class RedisLockConfig {

    @Bean
    public RedisLock build(){
        RedisLock redisLock = new RedisLock() ;
        HostAndPort hostAndPort = new HostAndPort("127.0.0.1",7000) ;
        JedisCluster jedisCluster = new JedisCluster(hostAndPort) ;
        // Jedis 或 JedisCluster 都可以
        redisLock.setJedisCluster(jedisCluster) ;
        return redisLock ;
    }

}

Utilisation :

    @Autowired
    private RedisLock redisLock ;

    public void use() {
        String key = "key";
        String request = UUID.randomUUID().toString();
        try {
            boolean locktest = redisLock.tryLock(key, request);
            if (!locktest) {
                System.out.println("locked error");
                return;
            }


            //do something

        } finally {
            redisLock.unlock(key,request) ;
        }

    }

Il est très simple à utiliser. L'objectif principal ici est d'utiliser Spring pour nous aider à gérer le bean singleton RedisLock. Ainsi, lors de la libération du verrou, nous devons transmettre manuellement la clé et la demande (car le contexte entier n'a qu'une seule instance RedisLock) (l'API ne semble pas particulièrement élégant).

Vous pouvez également créer un nouveau RedisLock et transmettre la clé et la demander à chaque fois que vous utilisez la serrure, ce qui est très pratique lors du déverrouillage. Mais vous devez gérer vous-même l'instance RedisLock. Chacun a ses propres avantages et inconvénients.

Test unique

En travaillant sur ce projet, je dois mentionner Test unique.

Parce que cette application est fortement dépendante de composants tiers (Redis), mais nous devons exclure cette dépendance dans le test unique. Par exemple, un autre partenaire a lancé le projet et a voulu exécuter un seul test localement, mais le résultat n'a pas fonctionné :

  1. Il se peut que l'adresse IP et le port de Redis soient incohérents avec ceux dans le seul test.

  2. Redis lui-même peut également avoir des problèmes.

  3. Il est également possible que l'élève n'ait pas Redis dans son environnement.

Il est donc préférable d'éliminer ces facteurs instables externes et de tester uniquement le code que nous avons écrit.

Ensuite, vous pouvez introduire l'outil de test unique Mock.

L'idée est très simple, c'est de bloquer toutes les ressources externes sur lesquelles vous comptez. Tels que : base de données, interface externe, fichiers externes, etc.

使用方式也挺简单,可以参考该项目的单测:

    @Test
    public void tryLock() throws Exception {
        String key = "test";
        String request = UUID.randomUUID().toString();
        Mockito.when(jedisCluster.set(Mockito.anyString(), Mockito.anyString(), Mockito.anyString(),
                Mockito.anyString(), Mockito.anyLong())).thenReturn("OK");

        boolean locktest = redisLock.tryLock(key, request);
        System.out.println("locktest=" + locktest);

        Assert.assertTrue(locktest);

        //check
        Mockito.verify(jedisCluster).set(Mockito.anyString(), Mockito.anyString(), Mockito.anyString(),
                Mockito.anyString(), Mockito.anyLong());
    }

这里只是简单演示下,可以的话下次仔细分析分析。

它的原理其实也挺简单,debug 的话可以很直接的看出来:

Comment implémenter des verrous distribués basés sur Redis

这里我们所依赖的 JedisCluster 其实是一个 cglib 代理对象。所以也不难想到它是如何工作的。

比如这里我们需要用到 JedisCluster 的 set 函数并需要它的返回值。

Mock 就将该对象代理了,并在实际执行 set 方法后给你返回了一个你自定义的值。

这样我们就可以随心所欲的测试了,完全把外部依赖所屏蔽了

总结

至此一个基于 Redis 的分布式锁完成,但是依然有些问题。

  • 如在 key 超时之后业务并没有执行完毕但却自动释放锁了,这样就会导致并发问题。

  • 就算 Redis 是集群部署的,如果每个节点都只是 master 没有 slave,那么 master 宕机时该节点上的所有 key 在那一时刻都相当于是释放锁了,这样也会出现并发问题。就算是有 slave 节点,但如果在数据同步到 salve 之前 master 宕机也是会出现上面的问题。

感兴趣的朋友还可以参考 Redisson 的实现。

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:
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