一、前言
前几天同事用redission的lock功能,耗时一下午,也没实现抢不到锁立即返回的效果,然后退回使用redis template的setnx方法。review全部代码后发现,老哥这是没有理解redission lock的参数含义,还有个其他C端场景模块代码毫无作用的等待2s,导致并发度始终上不去。当时有点血压飙升,抄起水杯,喝两口压压惊(自己年龄大了要稳重,业务上可能确实着急同事没有仔细研究这块)。今天简单说下这俩参数。
二、业务场景
这个业务存在很多任务队列,每个队列里有很多任务,串行执行,且队列中的任务一直增加。线程需要抢队列id锁,然后执行该队列的任务。执行任务的时候,执行失败,其他线程可以抢锁重试。队列加入新任务后会唤醒线程消费队列任务。以下是模拟当时抢锁的代码
RLock rLock = redissonClient.getLock("queue-lock" + queue.getId());
boolean islock = false;
try {
//islock = rLock.tryLock(4, 5, TimeUnit.SECONDS);
islock = rLock.tryLock(5, TimeUnit.SECONDS);
if (!islock) {
return ResponseEntity.ok("该任务在执行。。。");
}
// 业务代码,耗时操作
return result;
} catch (Exception e) {
log.info("\n{} --> 获取锁失败:{}", Thread.currentThread().getName(), e);
} finally {
rLock.unlock();
}
实际效果是:本来应该2s完成的任务,经常5秒都执行不完,研发自测始终不通过,被迫换redistemplate的原生setnx作为锁,然后达到了要求
三、血压飙升,我来分析原因
第一步:RLock rLock = redissonClient.getLock("queue-lock" + queue.getId()),这步源码如下:
public RedissonLock(CommandAsyncExecutor commandExecutor, String name) {
super(commandExecutor, name);
this.commandExecutor = commandExecutor;
this.internalLockLeaseTime = getServiceManager().getCfg().getLockWatchdogTimeout();
this.pubSub = commandExecutor.getConnectionManager().getSubscribeService().getLockPubSub();
}
每个方法都查看过+埋点查看,没有耗时操作
第二步:islock = rLock.tryLock(4, 5, TimeUnit.SECONDS);。这步看着就很可疑。waittime是啥含义,leaseTime怎么用?
官方解释如下:
Tries to acquire the lock with defined leaseTime. Waits up to defined waitTime if necessary until the lock became available. Lock will be released automatically after defined leaseTime interval.
Params:
waitTime – the maximum time to acquire the lock
leaseTime – lease time
unit – time unit
Returns:
true if lock is successfully acquired, otherwise false if lock is already set.
waitTime和unit还算好理解,没有抢到锁,则最长等待多少单位的时间。leaseTime有点懵,那就看下源码
第三步、leaseTime源码
设置leaseTime,tryAcquire核心代码如下
<T> RFuture<T> tryLockInnerAsync(long waitTime, long leaseTime, TimeUnit unit, long threadId, RedisStrictCommand<T> command) {
return evalWriteAsync(getRawName(), LongCodec.INSTANCE, command,
"if ((redis.call('exists', KEYS[1]) == 0) " +
"or (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.singletonList(getRawName()), unit.toMillis(leaseTime), getLockName(threadId));
}
单步过程跳过,走到了这里:redis.call('pexpire', KEYS[1], ARGV[1]),也就是说,这里的lua脚本会设置锁的释放时间为leaseTime,然后返回了一个过期时间。没有设置走如下逻辑,会启动看门狗机制
private <T> RFuture<Long> tryAcquireAsync(long waitTime, long leaseTime, TimeUnit unit, long threadId) {
RFuture<Long> ttlRemainingFuture;
if (leaseTime != -1) {
ttlRemainingFuture = tryLockInnerAsync(waitTime, leaseTime, unit, threadId, RedisCommands.EVAL_LONG);
} else {
// 这里需要注意的是leaseTime==-1,会触发redisson看门狗机制
ttlRemainingFuture = tryLockInnerAsync(waitTime, internalLockLeaseTime,
TimeUnit.MILLISECONDS, threadId, RedisCommands.EVAL_LONG);
}
ttlRemainingFuture.onComplete((ttlRemaining, e) -> {
if (e != null) {
return;
}
// 获取锁成功
if (ttlRemaining == null) {
if (leaseTime != -1) {
internalLockLeaseTime = unit.toMillis(leaseTime);
} else {
// 锁自动续时(看门狗机制)触发条件leaseTime == -1
scheduleExpirationRenewal(threadId);
}
}
});
return ttlRemainingFuture;
}
四、总结
根据上面的源码分析,如果实现setnx立即返回的效果,可以如下设置
1、固定超时时间,则可以这样设置(30为超时时间):
islock = rLock.tryLock(0, 30, TimeUnit.SECONDS);
2、启动看门狗机制:
islock = rLock.tryLock(0, TimeUnit.SECONDS);
注:年龄大了,一定要想的开,公司本身制度混乱,业务激进,能实现就可以了,业务真的其实不关心咋实现的,哪怕Excel手动管理; 谁没年轻过,有时间在优化,问题都是外部的,😁