ホームページ  >  記事  >  データベース  >  3 種類の Redis 分散ロックの比較

3 種類の Redis 分散ロックの比較

藏色散人
藏色散人転載
2020-11-04 14:48:092369ブラウズ

次のコラム Redis チュートリアル では、3 種類の Redis 分散ロックの比較を紹介します。

通常使用する同期ロックはスレッド ロックであり、同じ JVM プロセス内の複数のスレッドに対して有効です。ロックの本質は、ロックを取得したスレッドを記録するマークをメモリに保存することであるため、このマークはすべてのスレッドに表示されます。ただし、開始した複数の順序のサービスは複数の JVM です。メモリ内のロックは明らかに共有されていません。各 JVM プロセスは独自のロックを持っています。当然、スレッドの相互排他は保証されません。このとき、Distributed を使用する必要があります。ロック。一般的に使用されるソリューションは 3 つあります。 1. データベース ベースの実装 2. Zookeeper ベースの一時シリアル化ノードの実装 3. redis 実装。この記事ではredisの実装について紹介します。
分散ロックを実装するには、複数のプロセスに可視であること、相互に排他的であること、再入可能であることの 3 つの点を満たしている必要があります。

1) 複数のプロセスから参照可能

redis 自体は JVM の外部に基づいているため、複数のプロセスから参照できます。複数のプロセスが必要です。

2) 相互排除

つまり、同時に 1 つのプロセスだけがロック マークを取得できます。 redis setnx 実装では、最初の実行のみが成功して 1 が返され、それ以外の場合は 0 が返されます。

ロックを解除する
ロックを解除するには、ロックのキーを削除し、del xxx コマンドを使用するだけです。ただし、del を実行する前にサービスが突然ダウンした場合、ロックは削除されません。したがって、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) {
        }
    }
}

上記の分散ロックは実装されましたが、この時点で他に 2 つの問題が発生する可能性があります。
1 つ: ロックの取得時
Setnx はロックを正常に取得しましたが、setex サービスは問題は、この非アトミックな操作によりデッドロックが再び発生することです。実際、redis には nx および ex コマンドが用意されています。


2: ロックを解放する場合
1. A、B、Cの3つのプロセスがタスクを実行しており、ロックを競合しています。このとき、Aがロックを獲得し、自動有効期限は 10 秒です
2. A が業務の実行を開始します。何らかの理由で業務がブロックされ 10 秒以上かかります。このとき自動的にロックが解除されます
3. B が開始しますこの時点でロックを取得しようとしました。ロックは自動的に解放されているため、ロックは正常に取得されました。
4. A はこの時点で業務実行を完了し、ロック解放ロジック (キーの削除) を実行したため、B のロックはが解放されましたが、B は実際にはまだ業務を実行しています。
5. このとき、プロセス C はロックを取得しようとしましたが、A が B のロックを削除したため成功しました。
問題が発生します: B と C が同時にロックを取得し、相互排他性に違反しました。この問題を解決するにはどうすればよいでしょうか?ロックを削除する前に、そのロックが自分で設定したロックかどうかを確認する必要があります。そうでない場合 (たとえば、自分のロックが時間の経過とともに解放された場合)、削除しないでください。したがって、ロックを設定するときに現在のスレッドの一意の識別子を保存できます。ロックを削除する前に、内部の値が自分の識別リリースと一致しているかどうかを確認し、一致していない場合は、自分のロックではないことを意味しますので、削除しないでください。

/**
 * 第二种分布式锁 */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;
    }
}

上記の方法で分散ロックを実装すると、ほとんどの問題は簡単に解決できます。インターネット上の多くのブログでもこのメソッドが実装されていますが、メソッドがロックを取得した後、そのメソッド内でそのメソッドを呼び出すとロックを取得できないなど、まだ満足のいくシナリオがいくつかありません。現時点では、ロックを再入可能なロックに改良する必要があります。

3) リエントランシーロック

  也叫做递归锁,指的是在同一线程内,外层函数获得锁之后,内层递归函数仍然可以获取到该锁。换一种说法:同一个线程再次进入同步代码时,可以使用自己已获取到的锁。可重入锁可以避免因同一线程中多次获取锁而导致死锁发生。像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锁就开发完成了。

以上が3 種類の Redis 分散ロックの比較の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。

声明:
この記事はcnblogs.comで複製されています。侵害がある場合は、admin@php.cn までご連絡ください。