加锁核心代码(RedissonLock.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,
"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; " +
"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脚本说明:
加锁成功 返回nil:
1. 如果key值不存在 设置key值:hset KEYS[1] ARGV[2] 1 设置过期时间: pexpire KEYS[1] ARGV[1]
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;
2. 如果key值存在并且是自己线程的 设置key值+1: hincrby KEYS[1] ARGV[2] 1 并且设置过期时间 pexpire KEYS[1] ARGV[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;
加锁失败 返回上一个锁的剩余过期时间:
3. 加锁失败,其他情况(如果key值存在并且不是自己线程的 )返回剩余过期时间
return redis.call('pttl', KEYS[1]);
KEYS[1] = 加锁key
ARGV[2] = 加锁值(这里是key对应的hash的field) = id + ":" + threadId; (初始化RedissonClient是会生成ConnectionManager属性(ConfigSupport.createConnectionManager)设置好 id = UUID.randomUUID();(为的是可重入,和避免解锁时解错其他线程的锁))
ARGV[1] = 看门狗过期时间 internalLockLeaseTime(Redisson内部提供了一个监控锁的看门狗,它的作用是在Redisson实例被关闭前,不断的延长锁的有效期)
解锁核心代码(RedissonLock.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; " +
"local counter = redis.call('hincrby', KEYS[1], ARGV[3], -1); " +
"if (counter > 0) then " +
"redis.call('pexpire', KEYS[1], ARGV[2]); " +
"return 0; " +
"else " +
"redis.call('del', KEYS[1]); " +
"redis.call('publish', KEYS[2], ARGV[1]); " +
"return 1; "+
"end; " +
"return nil;",
Arrays.<Object>asList(getName(), getChannelName()), LockPubSub.UNLOCK_MESSAGE, internalLockLeaseTime, getLockName(threadId));
}
解锁lua脚本说明:
解锁成功:
1. 如果key值不存在 返回nil
if (redis.call('hexists', KEYS[1], ARGV[3]) == 0) then return nil; end;
2. 如果key值存在 设置并获取key值field为ARGV[3](当前线程的值)的值-1: hincrby KEYS[1] ARGV[3] -1
local counter = redis.call('hincrby', KEYS[1], ARGV[3], -1);
2.1. 如果减完后任然有锁(当前线程任然占用) 更新key值过期时间(看门狗的续期) pexpire KEYS[1] ARGV[2] 返回0
if (counter > 0) then redis.call('pexpire', KEYS[1], ARGV[2]); return 0;
2.2. 如果减完后小于等于0 删除key值 (真正的解锁成功)发布解锁消息 返回1
else redis.call('del', KEYS[1]); redis.call('publish', KEYS[2], ARGV[1]); return 1;end;
3. 其他情况 返回nil
return nil;
理论上不抛出异常解锁都是认为成功了的,当等于ture时意味着线程完全解锁
KEYS[1] = 加锁key
KEYS[1] = 发布频道: getChannelName()
ARGV[1] = 解锁消息: LockPubSub.UNLOCK_MESSAGE
ARGV[2] = 看门狗过期时间 internalLockLeaseTime(Redisson内部提供了一个监控锁的看门狗,它的作用是在Redisson实例被关闭前,不断的延长锁的有效期)
ARGV[3] = 加锁值(这里是key对应的hash的field) = id + ":" + threadId; (初始化RedissonClient是会生成ConnectionManager属性(ConfigSupport.createConnectionManager)设置好 id = UUID.randomUUID();(为的是可重入,和避免解锁时解错其他线程的锁))
这里需要明白为什么使用lua脚本,因为redis的lua脚本是原子性(事务)的,加锁解锁整个过程必须保证原子性(事务),具体的原理可以参考:《Redis官方文档》用Redis构建分布式锁
安全和可靠性保证
在描述我们的设计之前,我们想先提出三个属性,这三个属性在我们看来,是实现高效分布式锁的基础。
一致性:互斥,不管任何时候,只有一个客户端能持有同一个锁。
分区可容忍性:不会死锁,最终一定会得到锁,就算一个持有锁的客户端宕掉或者发生网络分区。
可用性:只要大多数Redis节点正常工作,客户端应该都能获取和释放锁。(单例模式这点做不到)