Redis应用与原理(4) Redis分布式锁实现II -- 上一章的实现竟然有这些BUG?

上次我们介绍了SETNX + GETSET的方式实现分布式锁,这是老版本Redis中最常用的实现分布式锁的方法,但该方法存在以下问题:

  1. 由于是客户端自己生成过期时间,所以需要强制要求分布式下每个客户端的时间必须同步。
  2. 当锁过期的时候,如果多个客户端同时执行jedis.getSet()方法,那么虽然最终只有一个客户端可以加锁,但是这个客户端的锁的过期时间可能被其他客户端覆盖。
  3. 锁不具备拥有者标识,即任何客户端都可以解锁。

对于锁拥有者标识的问题,我们首先想到可以使用ID来标识线程,在传入参数中携带ID。
首先获取锁对应的value值,检查是否与requestId相等,如果相等则删除锁(解锁)。但是,上述操作无法保证原子性!也就是无法做到线程安全的特性。
那么,如何传入线程ID同时又保证操作的原子性呢?答案是使用Lua脚本对传入参数进行处理。

public static boolean releaseDistributedLock(Jedis jedis, String lockKey, String requestId) {

        String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', 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代码传到jedis.eval()方法里,并使参数KEYS[1]赋值为lockKey,ARGV[1]赋值为requestId。eval()方法是将Lua代码交给Redis服务端执行。
eval命令执行Lua代码的时候,Lua代码将被当成一个命令去执行,并且直到eval命令执行完成,Redis才会执行其他命令。


对于上述锁强制要求分布式下每个客户端的时间必须同步的问题,Redis在其2.6.12版本中更新了SET命令,新版的SET命令可以传入多种参数,其中就包括锁过期时间!
SET key value [EX seconds] [PX milliseconds] [NX|XX]

参数解释:
EX seconds – 设置键 key 的过期时间,单位时秒
PX milliseconds – 设置键 key 的过期时间,单位时毫秒
NX – 只有键 key 不存在的时候才会设置 key 的值
XX – 只有键 key 存在的时候才会设置 key 的值
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;

    }

这样,在新版本的Redis中,使用SET + LUA脚本的方式,就能完美解决分布式锁问题了!

后来我还了解到Redisson这个开源项目。如果你的项目中Redis是多机部署的,那么可以尝试使用Redisson实现分布式锁,这是Redis官方提供的Java组件。

使用:直接操作Lock接口就可以了,没有学习成本!!

    RLock lock = redisson.getLock("anyLock");
    // 加锁以后10秒钟自动解锁
    // 无需调用unlock方法手动解锁
    lock.lock(10, TimeUnit.SECONDS);
    
    // 尝试加锁,最多等待100秒,上锁以后10秒自动解锁
    boolean res = lock.tryLock(100, 10, TimeUnit.SECONDS);
    if (res) {
       try {
         ...
       } finally {
           lock.unlock();
       }
    }

基于Redis的Redisson分布式可重入锁RLock Java对象实现了java.util.concurrent.locks.Lock接口
用他提供的锁接口即可实现分布式锁。Redisson采用了基于NIO的Netty框架,对Redis的常用功能进行了Java封装,实现各类Java常用的数据结构set,list,queue,map、多线程组件(各种锁),消息队列等。

你可能感兴趣的:(Redis应用与原理(4) Redis分布式锁实现II -- 上一章的实现竟然有这些BUG?)