Redis分布式锁实现-Redission

107 阅读3分钟

加锁流程

RedissonClient-(getLock)->RedissonLock-(lock)->tryAcquire-(同步转异步)->tryAcquireAsync-(最终调用)->tryLockInnerAsync-(lua脚本)->lua(脚本原子性,发送)-->redis服务器接受请求,执行脚本

源码

<T> RFuture<T> tryLockInnerAsync(long leaseTime, TimeUnit unit, long threadId, RedisStrictCommand<T> command) {
        internalLockLeaseTime = unit.toMillis(leaseTime);
        
        return commandExecutor.evalWriteAsync(getName(), LongCodec.INSTANCE, command,
                  "if (redis.call('exists', KEYS[1]) == 0) then " +
                      "redis.call('hincrby', KEYS[1], ARGV[2], 1); " +
                      "redis.call('pexpire', KEYS[1], ARGV[1]); " +
                      "return nil; " +
                  "end; " +
                  "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; " +
                  "return redis.call('pttl', KEYS[1]);",
                    Collections.<Object>singletonList(getName()), internalLockLeaseTime, getLockName(threadId));
    }

解析

  • lua脚本的原子性

Redis的单线程模型保证了lua脚本的原子性,每个redis实例只有一个lua脚本解析器排队执行所有脚本,期间不会有其他的脚本或者其他redis命令执行。

但是redis的原子性不同于数据库的原子性,脚本中若有执行错误,那么已经执行过的脚本不会进行回滚

  • 参数
KEYS[1] 锁名称--Collections.<Object>singletonList(getName())
ARGV[1] 过期时间默认30s --internalLockLeaseTime
ARGV[2] 加锁的唯一标识, UUID + threadId---getLockName(threadId)
  • lua加锁逻辑

1)先调用redis的exists命令判断加锁的key存不存在,如果不存在的话,那么就进入if。不存在的意思就是还没有某个客户端的某个线程来加锁,第一次加锁肯定没有人来加锁,于是第一次if条件成立。

2)然后调用redis的hincrby的命令,设置加锁的key和加锁的某个客户端的某个线程,加锁次数设置为1,加锁次数很关键,是实现可重入锁特性的一个关键数据。用hash数据结构保存。hincrby命令完成后就形成如下的数据结构。 myLock:{"b983c153-7421-469a-addb-44fb92259a1b:1":1}

3)最后调用redis的pexpire的命令,将加锁的key过期时间设置为30s。

锁续约

  • 默认30秒,是为了防止死锁
  • 续约机制:
private <T> RFuture<Long> tryAcquireAsync(long leaseTime, TimeUnit unit, long threadId) {
        if (leaseTime != -1) {
            //加锁
            return tryLockInnerAsync(leaseTime, unit, threadId, RedisCommands.EVAL_LONG);
        }
        RFuture<Long> ttlRemainingFuture = tryLockInnerAsync(commandExecutor.getConnectionManager().getCfg().getLockWatchdogTimeout(), TimeUnit.MILLISECONDS, threadId, RedisCommands.EVAL_LONG);
        ttlRemainingFuture.onComplete((ttlRemaining, e) -> {
            if (e != null) {
                return;
            }

            // lock acquired
            if (ttlRemaining == null) {
                //定时任务,续约
                scheduleExpirationRenewal(threadId);
            }
        });
        return ttlRemainingFuture;
    }

//续约核心
protected RFuture<Boolean> renewExpirationAsync(long threadId) {
        return commandExecutor.evalWriteAsync(getName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN,
                "if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then " +
                    "redis.call('pexpire', KEYS[1], ARGV[1]); " +
                    "return 1; " +
                "end; " +
                "return 0;",
            Collections.<Object>singletonList(getName()), 
            internalLockLeaseTime, getLockName(threadId));
    }

可重入锁

                  "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对应的加锁线程跟要来加锁的线程是不是同一个,是就将这个线程对应的加锁次数+1,实现也可重入锁,同事返回nil回去。 //返回值
myLock:{
	"b983c153-7421-469a-addb-44fb92259a1b:1":2
}
  • 主动释放锁和避免其他线程释放自己的锁
  • 不手动释放的锁因为看门狗机制会造成死锁。
  • 指定超时自动释放的会造成锁浪费。
  • 释放锁会传入当前线程id避免释放其他锁。