加锁流程
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避免释放其他锁。