java实现Redisson看门狗机制
作者:雪顶猫的鳄
一、背景
网上redis分布式锁的工具方法,大都满足互斥、防止死锁的特性,有些工具方法会满足可重入特性。如果只满足上述3种特性会有哪些隐患呢?redis分布式锁无法自动续期,比如,一个锁设置了1分钟超时释放,如果拿到这个锁的线程在一分钟内没有执行完毕,那么这个锁就会被其他线程拿到,可能会导致严重的线上问题。
既然存在锁过期而任务未执行完毕的情况,那是否有一种可以在任务未完成时自动续期的机制呢,几年前在redisson中找到了看门狗的自动续期机制,就是解决这种分布式锁自动续期的问题的。
Redisson 锁的加锁机制如上图所示,线程去获取锁,获取成功则执行lua脚本,保存数据到redis数据库。如果获取失败: 一直通过while循环尝试获取锁(可自定义等待时间,超时后返回失败),获取成功后,执行lua脚本,保存数据到redis数据库。Redisson提供的分布式锁是支持锁自动续期的,也就是说,如果线程仍旧没有执行完,那么redisson会自动给redis中的目标key延长超时时间,这在Redisson中称之为 Watch Dog 机制
二、redisson 看门狗使用以及原理
1.redisson配置和初始化
pom.xml
<dependency> <groupId>org.redisson</groupId> <artifactId>redisson-spring-boot-starter</artifactId> <version>3.16.4</version> </dependency>
application.yaml
redis: host: xxxxxxx password: xxxxxx max-active: 8 max-idle: 500 max-wait: 1 min-idle: 0 port: 6379 timeout: 1000ms database: 0
redisson配置类
@Configuration public class RedisConfig { //最简单的redisson初始化配置 @Bean public RedissonClient getRedisson() { Config config = new Config(); config.useSingleServer().setAddress("redis://" + host + ":" + port).setPassword(password); return Redisson.create(config); } }
2.redisson看门狗使用
使用redisson分布式锁的目的主要是防止分布式应用产生的并发问题,所以一般会进行一下调整改为AOP形式去进行业务代码解耦。这里会加入自定义注解和AOP。
@Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface RedisLock { //锁的名称 String lockName(); //锁的失效时间 long leaseTime() default 3; //是否开启看门狗,默认开启,开启时锁的失效时间不执行。任务未完成时会自动续期锁时间 //使用看门狗,锁默认redis失效时间未30秒。失效时间剩余1/3时进行续期判断,是否需要续期 boolean watchdog() default true; }
public class RedisLockAspect { @Autowired private RedissonClient redissonClient; private static final String REDIS_PREFIX = "redisson_lock:"; @Around("@annotation(redisLock)") public Object around(ProceedingJoinPoint joinPoint, RedisLock redisLock) throws Throwable { String lockName = redisLock.lockName(); RLock rLock = redissonClient.getLock(REDIS_PREFIX + lockName); Object result = null; boolean isLock; if(redisLock.watchdog()){ isLock =rLock.tryLock(0, TimeUnit.SECONDS); }else { isLock =rLock.tryLock(0,redisLock.leaseTime(), TimeUnit.SECONDS); } if(isLock){ try { //执行方法 result = joinPoint.proceed(); } finally { if (rLock.isLocked() && rLock.isHeldByCurrentThread()) { rLock.unlock(); } } }else { log.warn("The lock has been taken:{}",REDIS_PREFIX + lockName); } return result; } }
@Scheduled(cron = "*/10 * * * * ?") //使用注解进行加锁 @RedisLock(lockName = "npa_lock_test",watchdog = true) public void redisLockTest() { System.out.println("get lock and perform a task"); try { Thread.sleep(20000L); } catch (InterruptedException e) { e.printStackTrace(); } }
这里使用定时任务进行模拟调用,10秒一次定时任务请求,线程执行睡眠20秒后完成。下面看一下执行结果。当获取锁后,第二次定时任务执行时。锁未被释放。所以失败,第三次获取时所已经释放,所以成功。
如果拿到分布式锁的节点宕机,且这个锁正好处于锁住的状态时,会出现锁死的状态,为了避免这种情况的发生,锁都会设置一个过期时间。这样也存在一个问题,加入一个线程拿到了锁设置了30s超时,在30s后这个线程还没有执行完毕,锁超时释放了,就会导致问题,Redisson给出了自己的答案,就是 watch dog 自动延期机制。
Redisson提供了一个监控锁的看门狗,它的作用是在Redisson实例被关闭前,不断的延长锁的有效期,也就是说,如果一个拿到锁的线程一直没有完成逻辑,那么看门狗会帮助线程不断的延长锁超时时间,锁不会因为超时而被释放。
默认情况下,看门狗的续期时间是30s,也可以通过修改Config.lockWatchdogTimeout来另行指定。另外Redisson 还提供了可以指定leaseTime参数的加锁方法来指定加锁的时间。超过这个时间后锁便自动解开了,不会延长锁的有效期。
3.redisson源码
Redisson的源码版本基于:3.16.4,同时需要注意的是:
watchDog 只有在未显示指定加锁时间(leaseTime)时才会生效。(这点很重要)
lockWatchdogTimeout设定的时间不要太小 ,比如我之前设置的是 100毫秒,由于网络直接导致加锁完后,watchdog去延期时,这个key在redis中已经被删除了。
在调用lock方法时,会最终调用到tryAcquireAsync。调用链为:lock()->tryAcquire->tryAcquireAsync,详细解释如下:
使用了RFuture(相关内容涉及Netty异步回调模式-Future和Promise剖析)去启动异步线程执行
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 { //没有指定加锁时间 会先进行加锁,并且默认时间就是 LockWatchdogTimeout的时间 //这个是异步操作 返回RFuture 类似netty中的future ttlRemainingFuture = tryLockInnerAsync(waitTime, internalLockLeaseTime, TimeUnit.MILLISECONDS, threadId, RedisCommands.EVAL_LONG); } //这里也是类似netty Future 的addListener,在future内容执行完成后执行 ttlRemainingFuture.onComplete((ttlRemaining, e) -> { if (e != null) { return; } // lock acquired if (ttlRemaining == null) { // leaseTime不为-1时,不会自动延期 if (leaseTime != -1) { internalLockLeaseTime = unit.toMillis(leaseTime); } else { //这里是定时执行 当前锁自动延期的动作,leaseTime为-1时,才会自动延期 scheduleExpirationRenewal(threadId); } } }); return ttlRemainingFuture; }
scheduleExpirationRenewal 中会调用renewExpiration。 这里我们可以看到是启用一个timeout定时,去执行延期动作,
private void renewExpiration() { ExpirationEntry ee = EXPIRATION_RENEWAL_MAP.get(getEntryName()); if (ee == null) { return; } 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 " + getRawName() + " expiration", e); EXPIRATION_RENEWAL_MAP.remove(getEntryName()); return; } if (res) { //如果 没有报错,就再次定时延期 // reschedule itself renewExpiration(); } else { cancelExpirationRenewal(null); } }); } // 这里我们可以看到定时任务 是 lockWatchdogTimeout 的1/3时间去执行 renewExpirationAsync }, internalLockLeaseTime / 3, TimeUnit.MILLISECONDS); ee.setTimeout(task); }
protected RFuture<Boolean> renewExpirationAsync(long threadId) { return this.evalWriteAsync(this.getRawName(), 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.getRawName()), this.internalLockLeaseTime, this.getLockName(threadId)); }
结论
- watch dog 在当前节点存活时每10s给分布式锁的key续期 30s;
- watch dog 机制启动,且代码中没有释放锁操作时,watch dog 会不断的给锁续期;
- 如果程序释放锁操作时因为异常没有被执行,那么锁无法被释放,所以释放锁操作一定要放到 finally {} 中;
- 要使 watchLog机制生效 ,lock时 不要设置 过期时间
- watchlog的延时时间 可以由 lockWatchdogTimeout指定默认延时时间,但是不要设置太小。如100
- watchdog 会每 lockWatchdogTimeout/3时间,去延时。
- watchdog 通过 类似netty的 Future功能来实现异步延时
- watchdog 最终还是通过 lua脚本来进行延时
到此这篇关于java实现Redisson看门狗机制的文章就介绍到这了,更多相关java Redisson看门狗内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!