前言
本篇主要分享自己遇到以及了解的分布式锁,关于过期时间的坑,提醒自己和大家去正确使用它
一、背景
在微服务项目中,大家多多少少会接触使用到分布式锁,一般采取Redis实现巨多,Redis实现的方式也有挺多种的,比如,RedisTemplate、Redisson、RedisLockRegistry。
在公司的项目中,我们使用的是Redisson,一般你会怎么用?来看看下面的代码,是不是就是你的写法?
String lockKey = "forlan_lock_" + serviceId;
RLock lock = redissonClient.getLock(lockKey);
// 方式1
try {
lock.lock(5, TimeUnit.SECONDS);
// 执行业务
...
} catch (Exception e) {
e.printStackTrace();
} finally {
// 释放锁
lock.unlock();
}
// 方式2
try {
if (lock.tryLock(5, 5, TimeUnit.SECONDS)) {
// 获得锁执行业务
...
}
} catch (Exception e) {
e.printStackTrace();
} finally {
// 释放锁
lock.unlock();
}
上面这段代码有没有问题?下面我们一起来分析下~
二、分析
像上面的写法,符合我们的常规思维,因为对于缓存,基本都要设置一个过期时间,不能一直留在redis中,也是为了防止程序挂了,没有释放锁,所以都会设置一个过期时间
1、初步分析
好的,提到了过期时间,那么这个时间设置多长合适?
- 设置过短,会导致我们的业务还没有执行完,锁就释放了,其它线程拿到锁,重复执行业务
- 设置过长,如果程序挂了,需要等待比较长的时间,锁才释放,占用资源
这时,你可能会想,我们可以根据业务执行情况,设置个过期时间即可,对于部分执行久的业务,Redisson内部是有个看门狗机制,会帮我们去续期,那么是否就以为我们可以设置很短?频繁续期你觉得合理嘛~
简单来说,看门狗机制就是有个定时器,会去看我们的业务执行完没,没有就帮我们把过期时间延长,看似没有问题吧
2、源码分析
我们来看下源码,无论我们使用哪种方式,最终都会进到这个tryAcquireAsync方法,那就是看门狗机制的核心代码,如下:
private <T> RFuture<Long> tryAcquireAsync(long leaseTime, TimeUnit unit, final long threadId) {
if (leaseTime != -1L) {
// 前面我们指定了过期时间,会进到这里,直接加锁
return this.tryLockInnerAsync(leaseTime, unit, threadId, RedisCommands.EVAL_LONG);
} else {
// 没有指定过期时间的话,默认采用LockWatchdogTimeout,默认是30s
RFuture<Long> ttlRemainingFuture = this.tryLockInnerAsync(this.commandExecutor.getConnectionManager().getCfg().getLockWatchdogTimeout(), TimeUnit.MILLISECONDS, threadId, RedisCommands.EVAL_LONG);
// ttlRemainingFuture执行完,添加一个监听器,类似netty的时间轮
ttlRemainingFuture.addListener(new FutureListener<Long>() {
public void operationComplete(Future<Long> future) throws Exception {
if (future.isSuccess()) {
Long ttlRemaining = (Long)future.getNow();
if (ttlRemaining == null) {
RedissonLock.this.scheduleExpirationRenewal(threadId);
}
}
}
});
return ttlRemainingFuture;
}
从上面可以知道,如果我们指定了过期时间,会直接加锁,没有指定过期的话,默认是30s的时间,但会启动一个监听器,定期去续期,具体方法就是scheduleExpirationRenewal,源码如下:
private void scheduleExpirationRenewal(final long threadId) {
if (!expirationRenewalMap.containsKey(this.getEntryName())) {
Timeout task = this.commandExecutor.getConnectionManager().newTimeout(new TimerTask() {
public void run(Timeout timeout) throws Exception {
// renewExpirationAsync就是执行续期的方法
RFuture<Boolean> future = RedissonLock.this.renewExpirationAsync(threadId);
// 什么时候触发执行?
future.addListener(new FutureListener<Boolean>() {
public void operationComplete(Future<Boolean> future) throws Exception {
RedissonLock.expirationRenewalMap.remove(RedissonLock.this.getEntryName());
if (!future.isSuccess()) {
RedissonLock.log.error("Can't update lock " + RedissonLock.this.getName() + " expiration", future.cause());
} else {
if ((Boolean)future.getNow()) {
RedissonLock.this.scheduleExpirationRenewal(threadId);
}
}
}
});
}
}, this.internalLockLeaseTime / 3L, TimeUnit.MILLISECONDS); // 当业务执行了LockWatchdogTimeout的1/3时间,就会去执行续期
if (expirationRenewalMap.putIfAbsent(this.getEntryName(), new RedissonLock.ExpirationEntry(threadId, task)) != null) {
task.cancel();
}
}
public RedissonLock(CommandAsyncExecutor commandExecutor, String name) {
super(commandExecutor, name);
this.commandExecutor = commandExecutor;
this.id = commandExecutor.getConnectionManager().getId();
this.internalLockLeaseTime = commandExecutor.getConnectionManager().getCfg().getLockWatchdogTimeout();
this.entryName = this.id + ":" + name;
}
public Config() {
this.transportMode = TransportMode.NIO;
this.lockWatchdogTimeout = 30000L;
this.keepPubSubOrder = true;
this.addressResolverGroupFactory = new DnsAddressResolverGroupFactory();
}
从什么可以指定,当业务执行了LockWatchdogTimeout(30s)的1/3时间,就会去执行续期,也就是当时间超过10s后就会去续期,续期后的锁的时长重新变成 30 秒,源码如下:
protected RFuture<Boolean> renewExpirationAsync(long threadId) {
return this.commandExecutor.evalWriteAsync(this.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.singletonList(this.getName()), new Object[]{this.internalLockLeaseTime, this.getLockName(threadId)});
}
所以,结论是啥?就是以下两种写法都有问题
// 方式1
lock.lock(5, TimeUnit.SECONDS);
// 方式2
lock.tryLock(5, 5, TimeUnit.SECONDS)
这两种写法都会导致看门狗机制失效,假如业务执行超过5s,锁就释放了,会出现问题
三、解决
正确的写法,不指定过期时间,如下:
// 方式1
lock.lock();
// 方式2
lock.tryLock(5, -1, TimeUnit.SECONDS)
你可以会觉得不妥,不指定的话,会默认按照30s续期时间,然后每10s去看看有没有执行完,没有就续期,我们也可以指定续期时间,比如指定为15s
config.setLockWatchdogTimeout(15000L);
总结
- 在使用Redisson实现分布式锁,不应该设置过期时间
- 看门狗默认续期时间是30s,可以通过setLockWatchdogTimeout指定
- 看门狗每过 (过期时间 / 3)就去续期
- 看门狗机制底层实现,类似Netty的时间轮