Maison  >  Article  >  base de données  >  Comparaison de trois types de verrous distribués Redis

Comparaison de trois types de verrous distribués Redis

藏色散人
藏色散人avant
2020-11-04 14:48:092319parcourir

La colonne suivante du Tutoriel Redis vous présentera la comparaison de trois types de verrous distribués Redis. J'espère qu'elle sera utile aux amis dans le besoin !

Les synchronisés ou les verrous que nous utilisons habituellement sont des verrous de thread, qui sont valables pour plusieurs threads dans le même processus JVM. Étant donné que l'essence du verrou est de stocker une marque dans la mémoire, enregistrant qui est le thread qui a acquis le verrou, cette marque est visible par chaque thread. Cependant, les services de commandes multiples que nous avons démarrés sont plusieurs JVM. Les verrous en mémoire ne sont évidemment pas partagés. Chaque processus JVM a naturellement son propre verrou. L'exclusion mutuelle des threads ne peut pas être garantie pour le moment. verrouillage. Il existe trois solutions couramment utilisées : 1. Implémentation basée sur une base de données 2. Implémentation du nœud de sérialisation temporaire basée sur Zookeeper 3. Implémentation de Redis. Dans cet article, nous présentons l'implémentation de redis.
 Pour mettre en œuvre des verrous distribués, trois points doivent être respectés : visibles par plusieurs processus, mutuellement exclusifs et réentrants.

1) Visible par plusieurs processus

Redis lui-même est basé en dehors de la JVM, il est donc visible par plusieurs processus Exiger.

2) Exclusion mutuelle

Autrement dit, un seul processus peut obtenir la marque de verrouillage en même temps. Nous pouvons réussir. redis Dans l'implémentation setnx, seule la première exécution réussira et renverra 1, et 0 sera renvoyé dans les autres cas.

 

Libérez le verrou
Pour déverrouiller le verrou, il vous suffit de supprimer la clé du verrou et d'utiliser la commande del xxx. Cependant, si le service tombe soudainement en panne avant que nous exécutions del, le verrou ne sera jamais supprimé. Nous pouvons donc définir le délai d'expiration via la commande setex.

import java.util.UUID;import org.slf4j.Logger;import org.slf4j.LoggerFactory;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.stereotype.Component;import redis.clients.jedis.Jedis;import redis.clients.jedis.JedisPool;/**
 * 第一种分布式锁 */@Componentpublic class RedisService {private final Logger log = LoggerFactory.getLogger(this.getClass());
    
    @Autowired
    JedisPool jedisPool;     
    // 获取锁之前的超时时间(获取锁的等待重试时间)
    private long acquireTimeout = 5000;    // 获取锁之后的超时时间(防止死锁)
    private int timeOut = 10000;    
    /**
     * 获取分布式锁
     * @return 锁标识     */
    public boolean getRedisLock(String lockName,String val) {
        Jedis jedis = null;        try {
            jedis = jedisPool.getResource();            // 1.计算获取锁的时间
            Long endTime = System.currentTimeMillis() + acquireTimeout;            // 2.尝试获取锁
            while (System.currentTimeMillis() < endTime) {                // 3. 获取锁成功就设置过期时间
                if (jedis.setnx(lockName, val) == 1) {
                    jedis.expire(lockName, timeOut/1000);                    return true;
                }
            }
        } catch (Exception e) {
            log.error(e.getMessage());
        } finally {
            returnResource(jedis);
        }        return false;
    }    /**
     * 释放分布式锁
     * @param lockName 锁名称     */
    public void unRedisLock(String lockName) {
        Jedis jedis = null;        try {
            jedis = jedisPool.getResource();            // 释放锁            jedis.del(lockName);
        } catch (Exception e) {
            log.error(e.getMessage());
        } finally {
            returnResource(jedis);
        }
    }// ===============================================      
        public String get(String key) {
        Jedis jedis = null;
        String value = null;        try {
            jedis = jedisPool.getResource();
            value = jedis.get(key);
            log.info(value);
        } catch (Exception e) {
            log.error(e.getMessage());
        } finally {
            returnResource(jedis);
        }        return value;
    }    
    
    public void set(String key, String value) {
        Jedis jedis = null;        try {
            jedis = jedisPool.getResource();
            jedis.set(key, value);
        } catch (Exception e) {
            log.error(e.getMessage());
        } finally {
            returnResource(jedis);
        }
    }    /**
     * 关闭连接     */
    public void returnResource(Jedis jedis) {        try {            if(jedis!=null) jedis.close();
        } catch (Exception e) {
        }
    }
}

Le verrou distribué ci-dessus a été implémenté, mais il y a deux autres problèmes qui peuvent survenir à ce moment-là :
1 : Lors de l'acquisition du verrou
Setnx a acquis avec succès le verrou, mais le service setex a échoué avant qu'il ne puisse le faire. Le problème est qu'un blocage se produit à nouveau en raison de cette opération non atomique. En fait, redis fournit les commandes nx et ex.

 
Deux : Lors de la libération du verrou
1. Trois processus : A, B et C, exécutent des tâches et sont en compétition pour le verrou. À ce moment, A acquiert le verrou et. définit Le délai d'expiration automatique est de 10 s
2. A a commencé à exécuter l'entreprise Pour une raison quelconque, l'entreprise a été bloquée et a pris plus de 10 secondes. À ce moment, le verrou a été automatiquement libéré
3. B a démarré. essayant d'acquérir le verrou à ce moment-là. Comme le verrou a été automatiquement libéré, le verrou a été acquis avec succès
4. A a terminé l'exécution commerciale à ce moment-là et a exécuté la logique de déverrouillage (suppression de la clé), donc le verrou de B. a été libéré et B est en fait toujours en train d'exécuter l'entreprise
5. À ce moment-là, le processus C a tenté d'acquérir le verrou et a réussi car A a supprimé le verrou de B.
Un problème se pose : B et C ont acquis la serrure en même temps, violant ainsi l'exclusivité mutuelle ! Comment résoudre ce problème ? Avant de supprimer le verrou, nous devons déterminer si le verrou est un verrou défini par nous-mêmes. Si ce n'est pas le cas (par exemple, notre propre verrou a été libéré au fil du temps), ne le supprimez pas. Nous pouvons donc stocker l'identifiant unique du thread actuel lors du réglage du verrou ! Avant de supprimer le verrou, vérifiez si la valeur à l'intérieur est cohérente avec votre propre version d'identification. Si elle est incohérente, cela signifie qu'il ne s'agit pas de votre propre verrou, donc ne le supprimez pas.

/**
 * 第二种分布式锁 */public class RedisTool {    private static final String LOCK_SUCCESS = "OK";    
    private static final Long RELEASE_SUCCESS = 1L;    /**
     * 尝试获取分布式锁
     * @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, "NX", "PX", expireTime);        if (LOCK_SUCCESS.equals(result)) {            return true;
        }        return false;
    }    /**
     * 释放分布式锁
     * @param jedis Redis客户端
     * @param lockKey 锁
     * @param requestId 请求标识
     * @return 是否释放成功     */
    public static boolean releaseDistributedLock(Jedis jedis, String lockKey, String requestId) {        if (jedis.get(lockKey).equals(requestId)) {
            System.out.println("释放锁..." + Thread.currentThread().getName() + ",identifierValue:" + requestId);
            jedis.del(lockKey);            return true;
        }        return false;
    }
}

Après avoir implémenté les verrous distribués de la manière ci-dessus, la plupart des problèmes peuvent être facilement résolus. De nombreux blogs sur Internet implémentent également cette méthode, mais certains scénarios ne sont toujours pas satisfaisants. Par exemple, une fois qu'une méthode a obtenu un verrou, vous ne pourrez peut-être pas obtenir le verrou si vous appelez la méthode dans la méthode. À l’heure actuelle, nous devons améliorer le verrou pour en faire un verrou réentrant.

 

3) Serrure de réentrée :

  也叫做递归锁,指的是在同一线程内,外层函数获得锁之后,内层递归函数仍然可以获取到该锁。换一种说法:同一个线程再次进入同步代码时,可以使用自己已获取到的锁。可重入锁可以避免因同一线程中多次获取锁而导致死锁发生。像synchronized就是一个重入锁,它是通过moniter函数记录当前线程信息来实现的。实现可重入锁需要考虑两点:
   获取锁:首先尝试获取锁,如果获取失败,判断这个锁是否是自己的,如果是则允许再次获取, 而且必须记录重复获取锁的次数。
   释放锁:释放锁不能直接删除了,因为锁是可重入的,如果锁进入了多次,在内层直接删除锁, 导致外部的业务在没有锁的情况下执行,会有安全问题。因此必须获取锁时累计重入的次数,释放时则减去重入次数,如果减到0,则可以删除锁。

下面我们假设锁的key为“ lock ”,hashKey是当前线程的id:“ threadId ”,锁自动释放时间假设为20
获取锁的步骤:    1、判断lock是否存在 EXISTS lock 
        2、不存在,则自己获取锁,记录重入层数为1.        2、存在,说明有人获取锁了,下面判断是不是自己的锁,即判断当前线程id作为hashKey是否存在:HEXISTS lock threadId 
            3、不存在,说明锁已经有了,且不是自己获取的,锁获取失败.            3、存在,说明是自己获取的锁,重入次数+1: HINCRBY lock threadId 1 ,最后更新锁自动释放时间, EXPIRE lock 20
        释放锁的步骤:    1、判断当前线程id作为hashKey是否存在: HEXISTS lock threadId 
        2、不存在,说明锁已经失效,不用管了 
        2、存在,说明锁还在,重入次数减1: HINCRBY lock threadId -1 ,
          3、获取新的重入次数,判断重入次数是否为0,为0说明锁全部释放,删除key: DEL lock

因此,存储在锁中的信息就必须包含:key、线程标识、重入次数。不能再使用简单的key-value结构, 这里推荐使用hash结构。
获取锁的脚本(注释删掉,不然运行报错)

local key = KEYS[1]; -- 第1个参数,锁的keylocal threadId = ARGV[1]; -- 第2个参数,线程唯一标识local releaseTime = ARGV[2]; -- 第3个参数,锁的自动释放时间if(redis.call(&#39;exists&#39;, key) == 0) then -- 判断锁是否已存在
    redis.call(&#39;hset&#39;, key, threadId, &#39;1&#39;); -- 不存在, 则获取锁
    redis.call(&#39;expire&#39;, key, releaseTime); -- 设置有效期
    return 1; -- 返回结果end;if(redis.call(&#39;hexists&#39;, key, threadId) == 1) then -- 锁已经存在,判断threadId是否是自己    
    redis.call(&#39;hincrby&#39;, key, threadId, &#39;1&#39;); -- 如果是自己,则重入次数+1
    redis.call(&#39;expire&#39;, key, releaseTime); -- 设置有效期
    return 1; -- 返回结果end;return 0; -- 代码走到这里,说明获取锁的不是自己,获取锁失败

释放锁的脚本(注释删掉,不然运行报错)

local key = KEYS[1]; -- 第1个参数,锁的keylocal threadId = ARGV[1]; -- 第2个参数,线程唯一标识if (redis.call(&#39;HEXISTS&#39;, key, threadId) == 0) then -- 判断当前锁是否还是被自己持有
    return nil; -- 如果已经不是自己,则直接返回end;local count = redis.call(&#39;HINCRBY&#39;, key, threadId, -1); -- 是自己的锁,则重入次数-1if (count == 0) then -- 判断是否重入次数是否已经为0
    redis.call(&#39;DEL&#39;, key); -- 等于0说明可以释放锁,直接删除
    return nil;    
end;

完整代码

import java.util.Collections;import java.util.UUID;import org.springframework.core.io.ClassPathResource;import org.springframework.data.redis.core.StringRedisTemplate;import org.springframework.data.redis.core.script.DefaultRedisScript;import org.springframework.scripting.support.ResourceScriptSource;/**
 * Redis可重入锁 */public class RedisLock {    private static final StringRedisTemplate redisTemplate = SpringUtil.getBean(StringRedisTemplate.class);    private static final DefaultRedisScript<Long> LOCK_SCRIPT;    private static final DefaultRedisScript<Object> UNLOCK_SCRIPT;    static {        // 加载释放锁的脚本
        LOCK_SCRIPT = new DefaultRedisScript<>();
        LOCK_SCRIPT.setScriptSource(new ResourceScriptSource(new ClassPathResource("lock.lua")));
        LOCK_SCRIPT.setResultType(Long.class);        // 加载释放锁的脚本
        UNLOCK_SCRIPT = new DefaultRedisScript<>();
        UNLOCK_SCRIPT.setScriptSource(new ResourceScriptSource(new ClassPathResource("unlock.lua")));
    }    /**
     * 获取锁
     * @param lockName 锁名称
     * @param releaseTime 超时时间(单位:秒)
     * @return key 解锁标识     */
    public static String tryLock(String lockName,String releaseTime) {        // 存入的线程信息的前缀,防止与其它JVM中线程信息冲突
        String key = UUID.randomUUID().toString();        // 执行脚本
        Long result = redisTemplate.execute(
                LOCK_SCRIPT,
                Collections.singletonList(lockName),
                key + Thread.currentThread().getId(), releaseTime);        // 判断结果
        if(result != null && result.intValue() == 1) {            return key;
        }else {            return null;
        }
    }    /**
     * 释放锁
     * @param lockName 锁名称
     * @param key 解锁标识     */
    public static void unlock(String lockName,String key) {        // 执行脚本        redisTemplate.execute(
                UNLOCK_SCRIPT,
                Collections.singletonList(lockName),
                key + Thread.currentThread().getId(), null);
    }
}

 至此,一个比较完善的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