redisson实现分布式锁,是通过一个hash结构存储的,形式如下:
MY_LOCK 3444e697-8ab7-43ba-bfb5-28a38aeb1f02:1 1
MY_LOCK 是我获取分布式锁的时候,通过redisson.getLock(“MY_LOCK”)定义的,它作为hash结构的key
3444e697-8ab7-43ba-bfb5-28a38aeb1f02:1 作为hash结构的一个field,冒号后面的1是线程id。
1 是field的值,作为当前线程重入锁的次数。
每次通过判断所得key和当前线程对应的field是否存在来判断是否可以获取锁。以上这些内容都可以在调试源码的过程中看到具体细节。我是从org.redisson.RedissonLock#tryLock() 作为入口,罗列了比较关键的代码:
先整体看看加锁和给锁续期:
private RFuture<Boolean> tryAcquireOnceAsync(long leaseTime, TimeUnit unit, long threadId) {
// ......
// 加锁方法返回异步执行的结果,结果泛型为Boolean,这个结果是通过RedisCommands.EVAL_NULL_BOOLEAN转换的,如果结果为null, 返回true,否则返回false.
RFuture<Boolean> ttlRemainingFuture = tryLockInnerAsync(commandExecutor.getConnectionManager().getCfg().getLockWatchdogTimeout(), TimeUnit.MILLISECONDS, threadId, RedisCommands.EVAL_NULL_BOOLEAN);
// 给加锁返回的异步结果注册执行完成时的回调方法onComplete,是否需要给锁续期就是根据加锁结果判断的,
ttlRemainingFuture.onComplete((ttlRemaining, e) -> {
if (e != null) {
return;
}
// lock acquired
// 如果加锁方法返回true, 需要通过scheduleExpirationRenewal(threadId)方法给锁续期
if (ttlRemaining) {
scheduleExpirationRenewal(threadId);
}
});
return ttlRemainingFuture;
}
加锁方法内部实现
<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的参数
- KEYS[1]是MY_LOCK, 自定义的锁的key
- ARG[1] 是标识线程的哈希结构的field :81c39aea-b41f-4505-ae92-0f567fdc8651:1
- ARG[2] 是看门狗默认的超时时间
解释下这段lua的三个分支都做了什么事情:
如果MY_LOCK不存在,则创该建哈希结构,key为MY_LOCK,field为线程标记,value为1,过期时间30秒, 返回null, 这是首次加锁的情况。前面说到返回null的时候加锁方法会转为true, 此时回调方法中会去设置定时给锁续期的事情。
如果MY_LOCK这个key存在,并且MY_LOCK的线程field的存在,则给field增加1. 重新设置MY_LOCK的超时时间30s。返回null,这是锁重入时的情况,每次重入给线程field的值加1. 此时的返回结果也转为true, 并去续期锁。
否则,就是MY_LOCK存在,但是field不存在,表明当前线程已经不持有锁了,返回key的剩余生存时间. 此时结果不为null, 返回的RFuture 结果为false. 不会再去给锁续期。
补充一些redis知识:
exists 检查给定key是否存在
hexists 检查希表key
中,给定域field
是否存在
pexpire 设置过期时间,单位毫秒
expire 单位秒
hincrby 对hash的field的value增加,只能操作数值型
pttl 返回给定key
的剩余生存时间,单位毫秒
ttl 单位秒
EVAL script numkeys key [key …] arg [arg …]
- script: 参数是一段 Lua 5.1 脚本程序。脚本不必(也不应该)定义为一个 Lua 函数。
- numkeys: 用于指定键名参数的个数。
- key [key …]: 从 EVAL 的第三个参数开始算起,表示在脚本中所用到的那些 Redis 键(key),这些键名参数可以在 Lua 中通过全局变量 KEYS 数组,用 1 为基址的形式访问( KEYS[1] , KEYS[2] ,以此类推)。
- arg [arg …]: 附加参数,在 Lua 中通过全局变量 ARGV 数组访问,访问的形式和 KEYS 变量类似( ARGV[1] 、 ARGV[2] ,诸如此类)。
整体看看给锁续期
private void renewExpiration() {
ExpirationEntry ee = EXPIRATION_RENEWAL_MAP.get(getEntryName());
if (ee == null) {
return;
}
// 创建Timeout,这里创建的是HashedWheelTimeout,这是netty的HashedWheelTimer的一个内部类,HashedWheelTimer是一个优秀的定时任务实现方案,给锁续期就是定时任务去判断的。
Timeout task = commandExecutor.getConnectionManager().newTimeout(new TimerTask() {
@Override
public void run(Timeout timeout) throws Exception {
ExpirationEntry ent = EXPIRATION_RENEWAL_MAP.get(getEntryName());
if (ent == null) {
return;
}
Long threadId = ent.getFirstThreadId();
if (threadId == null) {
return;
}
// 这里真正的去更新锁的时间的操作
RFuture<Boolean> future = renewExpirationAsync(threadId);
// 根据更新锁返回的结果决定是否需要继续给锁续期
future.onComplete((res, e) -> {
if (e != null) {
log.error("Can't update lock " + getName() + " expiration", e);
return;
}
// 如果给锁续期返回成功,调用自己重新添加续期任务,因为之前的任务可能会过期
if (res) {
// reschedule itself
renewExpiration();
}
});
}
// 定时任务的执行时机是设置的看门狗超时时间的三分之一,默认看门狗超时时间30s的话那任务就是10s执行一次。
}, internalLockLeaseTime / 3, TimeUnit.MILLISECONDS);
ee.setTimeout(task);
}
看看给锁续期方法内部
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));
}
如果MY_LOCK的线程field存在,说明当前线程还持有锁,需要重新设置过期时间为看门狗设置的时间,返回1。否则返回0.
整体看看解锁
public RFuture<Void> unlockAsync(long threadId) {
RPromise<Void> result = new RedissonPromise<Void>();
// 解锁
RFuture<Boolean> future = unlockInnerAsync(threadId);
// 解锁完成后回调
future.onComplete((opStatus, e) -> {
// 如果发生了异常,取消自动续期,并组装失败结果,结束
if (e != null) {
cancelExpirationRenewal(threadId);
result.tryFailure(e);
return;
}
// 如果解锁返回null,也就是lua中,没有线程对应的锁存在,组装带有异常的失败信息,结束
if (opStatus == null) {
IllegalMonitorStateException cause = new IllegalMonitorStateException("attempt to unlock lock, not locked by current thread by node id: "
+ id + " thread-id: " + threadId);
result.tryFailure(cause);
return;
}
// 正常的解锁流程,在这里取消自动续期,返回成功结果
cancelExpirationRenewal(threadId);
result.trySuccess(null);
});
return result;
}
解锁内部实现
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的参数是
KEYS:
- MY_LOCK
- redisson_lock__channel:{MY_LOCK}
ARGV:
- 0
- 30000
- 81c39aea-b41f-4505-ae92-0f567fdc8651:1
解释下这段lua做了什么事情:
如果MY_LOCK的线程field不存在,返回null,结束
如果MY_LOCK的线程field存在,给value 减一,也就是重入锁退出一次。如果value还大于0,重新设置过期时间,返回0; 否则删除MY_LOCK, 往redisson_lock__channel:{MY_LOCK}通道发布一条消息0,返回1.