Redisson分布式锁的源码分析

Redisson分布式锁的源码分析

Redisson 分布式锁实现思路

锁标识:Hash 数据结构,key 为锁的名字,filed 当前竞争锁成功线程的唯一标识,value 重入次数

队列:所有竞争锁失败的线程,会订阅当前锁的解锁事件,利用 Semaphore 实现线程的挂起和唤醒

源码分析

基于redisson3.11.5版本

加锁流程图

Redisson分布式锁的源码分析_第1张图片

加锁核心源码:tryLockInnerAsync
    <T> RFuture<T> tryLockInnerAsync(long leaseTime, TimeUnit unit, long threadId, RedisStrictCommand<T> command) {
        internalLockLeaseTime = unit.toMillis(leaseTime);

        return commandExecutor.evalWriteAsync(getName(), LongCodec.INSTANCE, command,
                   //如果锁不存在,则执行 hset 命令(hset key UUID+threadId 1),然后通过 pexpire 命令设置锁的过期时间(即锁的租约时间)
                  // 返回空值 nil ,表示获取锁成功
                  "if (redis.call('exists', KEYS[1]) == 0) then " +
                      "redis.call('hset', KEYS[1], ARGV[2], 1); " +
                      "redis.call('pexpire', KEYS[1], ARGV[1]); " +
                      "return nil; " +
                  "end; " +
                  //如果锁存在,且value匹配,说明是当前线程持有的锁,则执行 hincrby 命令,重入次数加1,并且设置失效时间,返回空
                  "if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then " +
                      "redis.call('hincrby', KEYS[1], ARGV[2], 1); " +
                      "redis.call('pexpire', KEYS[1], ARGV[1]); " +
                      "return nil; " +
                  "end; " +
                  //如果key已经存在,但是value不匹配,说明锁已经被其他线程持有,通过 pttl 命令获取锁的剩余存活时间并返回,至此获取锁失败
                  "return redis.call('pttl', KEYS[1]);",
                    // 下面三个参数分别为 KEYS[1], ARGV[1], ARGV[2]
                    // 即锁的name,锁释放时间,当前线程唯一标识
                    Collections.<Object>singletonList(getName()), internalLockLeaseTime, getLockName(threadId));
    }

参数说明:

  • KEYS[1]:Collections.singletonList(getName()),表示分布式锁的key;
  • ARGV[1]:internalLockLeaseTime,即锁的租约时间(持有锁的有效时间),默认30s;
  • ARGV[2]:getLockName(threadId),是获取锁时set的唯一值 value,即UUID+threadId。
解锁流程图

Redisson分布式锁的源码分析_第2张图片

解锁核心源码:unlockInnerAsync
protected RFuture<Boolean> unlockInnerAsync(long threadId) {
        return commandExecutor.evalWriteAsync(getName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN,
                //如果锁存在,但值不匹配,说明锁不是自己的,无权释放,直接返回空
                "if (redis.call('hexists', KEYS[1], ARGV[3]) == 0) then " +
                    "return nil;" +
                "end; " +
                //锁存在,且值匹配,将增量(重入次数)-1,如果重入次数大于0,更新锁的过期时间,不能释放,返回 0 
                "local counter = redis.call('hincrby', KEYS[1], ARGV[3], -1); " +
                "if (counter > 0) then " +
                    "redis.call('pexpire', KEYS[1], ARGV[2]); " +
                    "return 0; " +
                "else " +
                    //重入次数减1后的值如果为0,删除key,释放锁,返回 1 ;
                    "redis.call('del', KEYS[1]); " +
                    //发布锁释放消息
                    "redis.call('publish', KEYS[2], ARGV[1]); " +
                    "return 1; "+
                "end; " +
                "return nil;",
                //这5个参数分别对应KEYS[1],KEYS[2],ARGV[1],ARGV[2]和ARGV[3]
                Arrays.<Object>asList(getName(), getChannelName()), LockPubSub.UNLOCK_MESSAGE, internalLockLeaseTime, getLockName(threadId));

    }

参数说明:

  • KEYS[1]:getName(),表示分布式锁的key;
  • KEYS[2]:getChannelName(),redis消息的ChannelName,一个分布式锁对应唯一的一个 channelName;
  • ARGV[1]:LockPubSub.UNLOCK_MESSAGE,redis消息体,这里只需要一个字节的标记就可以,主要标记redis的key已经解锁,再结合redis的Subscribe,能唤醒其他订阅解锁消息的客户端线程申请锁;
  • ARGV[2]:internalLockLeaseTime,即锁的租约时间(持有锁的有效时间),默认30s;
  • ARGV[3]:getLockName(threadId),是获取锁时set的唯一值 value,即UUID+threadId。

总结

通过 Redisson 实现分布式可重入锁,比纯自己通过set key value px milliseconds nx +lua 实现的效果更好些,虽然基本原理都一样,因为通过分析源码可知,RedissonLock是可重入的,并且考虑了失败重试,可以设置锁的最大等待时间, 在实现上也做了一些优化,减少了无效的锁申请,提升了资源的利用率。

需要特别注意的是,RedissonLock 同样没有解决redis节点挂掉的时候,存在丢失锁的风险的问题。而现实情况是有一些场景无法容忍的,所以 Redisson 提供了实现了redlock算法的 RedissonRedLock,RedissonRedLock 真正解决了单点失败的问题,代价是需要额外的为 RedissonRedLock 搭建Redis环境。

所以,如果业务场景可以容忍这种小概率的错误,则推荐使用 RedissonLock, 如果无法容忍,则推荐使用 RedissonRedLock。

你可能感兴趣的:(redis)