Maison  >  Article  >  base de données  >  Introduction à la mise en œuvre correcte du verrouillage distribué Redis

Introduction à la mise en œuvre correcte du verrouillage distribué Redis

尚
avant
2019-12-11 17:20:402750parcourir

Introduction à la mise en œuvre correcte du verrouillage distribué Redis

Il existe généralement trois façons d'implémenter des verrous distribués :

1. Verrouillage optimiste de base de données

2. >

3. Verrouillage distribué basé sur ZooKeeper.

Cet article présentera la deuxième façon d'implémenter des verrous distribués basés sur Redis. Bien qu'il existe divers blogs sur Internet qui présentent l'implémentation des verrous distribués Redis, leurs implémentations présentent divers problèmes. Afin d'éviter d'induire les lecteurs en erreur, ce blog présentera en détail comment implémenter correctement les verrous distribués Redis.

Fiabilité

Tout d'abord, afin de garantir que les verrous distribués sont disponibles, nous devons au moins nous assurer que la mise en œuvre du verrouillage répond aux suivant quatre conditions à la fois :

1. Exclusivité mutuelle. A tout moment, un seul client peut détenir le verrou.

2. Aucune impasse ne se produira. Même si un client plante alors qu'il maintient le verrou sans le déverrouiller activement, il est garanti que d'autres clients pourront ensuite le verrouiller.

3. Tolérant aux pannes. Tant que la plupart des nœuds Redis fonctionnent normalement, le client peut se verrouiller et se déverrouiller.

4. Pour dénouer la cloche, vous devez attacher la cloche. Le verrouillage et le déverrouillage doivent être effectués par le même client. Le client lui-même ne peut pas déverrouiller le verrou ajouté par d'autres.

Implémentation du code

Dépendances des composants

Nous devons d'abord introduire les composants open source Jedis via Maven. Ajoutez le code suivant au fichier pom.xml :

<dependency>
    <groupId>redis.clients</groupId>
    <artifactId>jedis</artifactId>
    <version>2.9.0</version>
</dependency>

Code de verrouillage

Posture correcte

Parler est pas cher, montre-moi le code. Montrez d'abord le code, puis expliquez lentement pourquoi il est implémenté de cette façon :

public class RedisTool {

    private static final String LOCK_SUCCESS = "OK";
    private static final String SET_IF_NOT_EXIST = "NX";
    private static final String SET_WITH_EXPIRE_TIME = "PX";

    /**
     * 尝试获取分布式锁
     * @param jedis Redis客户端
     * @param lockKey 锁
     * @param requestId 请求标识
     * @param expireTime 超期时间
     * @return 是否获取成功
     */
    public static boolean tryGetDistributedLock(Jedis jedis, String lockKey, String requestId, int expireTime) {

        String result = jedis.set(lockKey, requestId, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireTime);

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

    }

}

Comme vous pouvez le voir, nous n'avons besoin que d'une seule ligne de code pour verrouiller : jedis.set(String key, String value, String nxxx , String expx, int time ), cette méthode set() a un total de cinq paramètres formels :

Le premier est key Nous utilisons key comme verrou car key est unique.

Le deuxième est la valeur. Ce que nous transmettons est requestId. De nombreux enfants peuvent ne pas comprendre. N'est-il pas suffisant d'avoir une clé comme serrure ? La raison en est que lorsque nous avons parlé de fiabilité ci-dessus, le verrou distribué doit remplir la quatrième condition pour déverrouiller la cloche et la personne qui tient la cloche doit être celle qui a attaché la cloche. En attribuant la valeur à requestId, nous saurons quelle requête. ajouté le verrou Lors du déverrouillage Ensuite, vous pouvez avoir une base. requestId peut être généré à l’aide de la méthode UUID.randomUUID().toString().

Le troisième est nxxx. Nous remplissons ce paramètre avec NX, ce qui signifie SET IF NOT EXIST, c'est-à-dire que lorsque la clé n'existe pas, nous effectuons l'opération set si la clé existe déjà, non ; l'opération est effectuée ;

Le quatrième est expx. Ce paramètre que nous transmettons est PX, ce qui signifie que nous voulons ajouter un paramètre d'expiration à cette clé. L'heure spécifique est déterminée par le cinquième paramètre.

Le cinquième paramètre est le temps, qui correspond au quatrième paramètre et représente le délai d'expiration de la clé.

En général, exécuter la méthode set() ci-dessus ne conduira qu'à deux résultats : 1. Il n'y a actuellement aucun verrou (la clé n'existe pas), puis effectuez l'opération de verrouillage et définissez une période de validité pour le lock et value représente le client verrouillé. 2. Le verrou existe déjà, aucune opération n'est effectuée.

Les enfants prudents constateront que notre code de verrouillage remplit les trois conditions décrites dans notre fiabilité :

1 Tout d'abord, set() ajoute des paramètres NX, qui peuvent garantir que si la clé existe déjà, le. La fonction ne sera pas appelée avec succès, c'est-à-dire qu'un seul client peut détenir le verrou, satisfaisant ainsi l'exclusion mutuelle.

2. Deuxièmement, puisque nous fixons un délai d'expiration pour la serrure, même si le support de la serrure tombe en panne par la suite et ne la déverrouille pas, la serrure sera automatiquement déverrouillée (c'est-à-dire que la clé sera supprimée) en raison jusqu'à l'heure d'expiration. Aucun blocage ne se produira.

3. Enfin, parce que nous attribuons une valeur à requestId, qui représente l'identification de la demande du client verrouillé, lorsque le client se déverrouille, il peut être vérifié s'il s'agit du même client. Puisque nous considérons uniquement le scénario de déploiement autonome de Redis, nous ne considérerons pas la tolérance aux pannes pour le moment.

Exemple d'erreur 1

Un exemple d'erreur plus courant consiste à utiliser une combinaison de jedis.setnx() et jedis.expire() pour implémenter le verrouillage. comme suit :

public static void wrongGetLock1(Jedis jedis, String lockKey, String requestId, int expireTime) {

    Long result = jedis.setnx(lockKey, requestId);
    if (result == 1) {
        // 若在这里程序突然崩溃,则无法设置过期时间,将发生死锁
        jedis.expire(lockKey, expireTime);
    }

}

La fonction de la méthode setnx() est de DÉFINIR SI NON EXISTE, et la méthode expire() est d'ajouter un délai d'expiration au verrou. À première vue, le résultat semble être le même que celui de la méthode set() précédente. Cependant, comme il s'agit de deux commandes Redis, elles ne sont pas atomiques. Si le programme plante soudainement après l'exécution de setnx(), le verrou n'aura pas de valeur. délai d'expiration défini. Une impasse se produira alors. La raison pour laquelle certaines personnes implémentent cela sur Internet est que les versions inférieures de jedis ne prennent pas en charge la méthode multi-paramètres set().

Exemple d'erreur 2

public static boolean wrongGetLock2(Jedis jedis, String lockKey, int expireTime) {

    long expires = System.currentTimeMillis() + expireTime;
    String expiresStr = String.valueOf(expires);

    // 如果当前锁不存在,返回加锁成功
    if (jedis.setnx(lockKey, expiresStr) == 1) {
        return true;
    }

    // 如果锁存在,获取锁的过期时间
    String currentValueStr = jedis.get(lockKey);
    if (currentValueStr != null && Long.parseLong(currentValueStr) < System.currentTimeMillis()) {
        // 锁已过期,获取上一个锁的过期时间,并设置现在锁的过期时间
        String oldValueStr = jedis.getSet(lockKey, expiresStr);
        if (oldValueStr != null && oldValueStr.equals(currentValueStr)) {
            // 考虑多线程并发的情况,只有一个线程的设置值和当前值相同,它才有权利加锁
            return true;
        }
    }
        
    // 其他情况,一律返回加锁失败
    return false;

}

Ce type d'exemple d'erreur est plus difficile à trouver le problème, et la mise en œuvre est également plus compliquée. Idée d'implémentation : utilisez la commande jedis.setnx() pour implémenter le verrouillage, où key est le verrou et value est le délai d'expiration du verrou.

Processus d'exécution :

1. Essayez de verrouiller via la méthode setnx() Si le verrou actuel n'existe pas, renvoyez le verrou avec succès.

2. Si le verrou existe déjà, obtenez le délai d'expiration du verrou et comparez-le avec l'heure actuelle. Si le verrou a expiré, définissez le nouveau délai d'expiration et renvoyez le verrou avec succès. Le code est le suivant :

Alors quel est le problème avec ce code ?

1、由于是客户端自己生成过期时间,所以需要强制要求分布式下每个客户端的时间必须同步。 

2、当锁过期的时候,如果多个客户端同时执行jedis.getSet()方法,那么虽然最终只有一个客户端可以加锁,但是这个客户端的锁的过期时间可能被其他客户端覆盖。

3、锁不具备拥有者标识,即任何客户端都可以解锁。

解锁代码

正确姿势

还是先展示代码,再带大家慢慢解释为什么这样实现:

public class RedisTool {

    private static final Long RELEASE_SUCCESS = 1L;

    /**
     * 释放分布式锁
     * @param jedis Redis客户端
     * @param lockKey 锁
     * @param requestId 请求标识
     * @return 是否释放成功
     */
    public static boolean releaseDistributedLock(Jedis jedis, String lockKey, String requestId) {

        String script = "if redis.call(&#39;get&#39;, KEYS[1]) == ARGV[1] then return redis.call(&#39;del&#39;, KEYS[1]) else return 0 end";
        Object result = jedis.eval(script, Collections.singletonList(lockKey), Collections.singletonList(requestId));

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

    }

}

可以看到,我们解锁只需要两行代码就搞定了!第一行代码,我们写了一个简单的Lua脚本代码,上一次见到这个编程语言还是在《黑客与画家》里,没想到这次居然用上了。第二行代码,我们将Lua代码传到jedis.eval()方法里,并使参数KEYS[1]赋值为lockKey,ARGV[1]赋值为requestId。eval()方法是将Lua代码交给Redis服务端执行。

那么这段Lua代码的功能是什么呢?其实很简单,首先获取锁对应的value值,检查是否与requestId相等,如果相等则删除锁(解锁)。那么为什么要使用Lua语言来实现呢?因为要确保上述操作是原子性的。关于非原子性会带来什么问题,可以阅读【解锁代码-错误示例2】 。

那么为什么执行eval()方法可以确保原子性,源于Redis的特性,下面是官网对eval命令的部分解释:

简单来说,就是在eval命令执行Lua代码的时候,Lua代码将被当成一个命令去执行,并且直到eval命令执行完成,Redis才会执行其他命令。

错误示例1

最常见的解锁代码就是直接使用jedis.del()方法删除锁,这种不先判断锁的拥有者而直接解锁的方式,会导致任何客户端都可以随时进行解锁,即使这把锁不是它的。

public static void wrongReleaseLock1(Jedis jedis, String lockKey) {
    jedis.del(lockKey);
}

错误示例2

这种解锁代码乍一看也是没问题,甚至我之前也差点这样实现,与正确姿势差不多,唯一区别的是分成两条命令去执行,代码如下:

public static void wrongReleaseLock2(Jedis jedis, String lockKey, String requestId) {
        
    // 判断加锁与解锁是不是同一个客户端
    if (requestId.equals(jedis.get(lockKey))) {
        // 若在此时,这把锁突然不是这个客户端的,则会误解锁
        jedis.del(lockKey);
    }

}

如代码注释,问题在于如果调用jedis.del()方法的时候,这把锁已经不属于当前客户端的时候会解除他人加的锁。那么是否真的有这种场景?答案是肯定的,比如客户端A加锁,一段时间之后客户端A解锁,在执行jedis.del()之前,锁突然过期了,此时客户端B尝试加锁成功,然后客户端A再执行del()方法,则将客户端B的锁给解除了。

更多redis知识请关注redis数据库教程栏目。

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