Redission看门狗实现redis定期续期原理
文章目录
- 前言
- 前言问题解决思路
- 如何开启redission看门狗
- 看门狗核心代码
- 定时续期实现
- 解锁实现
- Redission的bug相关记录
- Redission依旧会产生需要考虑的问题
前言
本篇博客是介绍redission看门狗实现redis定期续期原理,若文章中出现相关问题,请指出!
所有博客文件目录索引:博客目录索引(持续更新)
通常我们在使用redis上锁时,会进行业务处理,通常上锁我们会采用setnx或者lua脚本来保证并发场景下,只有一个线程能够抢到锁。
大多数的业务代码为:
setnx lockKey,超时时间
业务代码
delete lockKey
一旦超时时间过去了,redis对应的key就过期掉了。
场景说明:如果说key过期了,但是其中的业务在超时时间里没有完成,那么就可能会出现并发执行我们业务的问题,导致出现各种并发问题。
前言问题解决思路
案例demo来源:https://developer.aliyun.com/article/1627823
锁过期问题解决:
- 预估业务执行时间,提前预判业务执行时长。(一旦预估不准,就会导致并发问题)
- 模拟CAS乐观锁的方式,增加版本号。(侵入代码)
- watch dog自动延期机制。(起一个后台线程,默认超时时长为30s,每隔10s检查一次,一旦客户端还持有锁,就会不断延长key的过期时间)
解决思路2:在每次写操作时加入一个 token。 token 可以是一个递增的数字(lock service 可以做到),每次有 client 申请锁就递增一次。
在每次写操作时加入一个 token。 token 可以是一个递增的数字(lock service 可以做到),每次有 client 申请锁就递增一次。
示例如下:
首先客户端1获取到了token值43,一旦客户端1出现STW或者业务时间较长情况,出现了redis key过期的情况,锁被释放。
此时客户端2获取到了token值44,执行的很快,业务完成后又将token=44写入db。
此时客户端1这时候活过来了,执行完业务再进行插入的时候,由于当前其token=43是<44,表示该业务出现了锁过期并发情况,那么该写入操作就会被拒绝。
缺点:入侵代码。
解决思路3:watch dog自动延期
客户端1进行加锁的同时,开启一个后台线程,这个线程每10s会检查下客户端1是否还持有锁,一旦还持有就会给它进行续期10s。
**本质:**在锁没有过期之前,不断的延长锁的有效期。
**优点:**不入侵业务代码,能够在真正业务执行完成之后锁释放。
**缺点:**无法避免STW情况,一旦出现STW无法执行续期代码逻辑。
如何开启redission看门狗
// 参数传递-1,开启看门狗,默认leaseTime超时时长为30s
boolean lock = rLock.tryLock(
lockWaitTime,
-1,
unit);
// 选择方法参数重不带leaseTime的,默认设置参数为-1 开启看门狗,默认leaseTime超时时长为30s
boolean lock = rLock.tryLock(
lockWaitTime,
unit);
一旦开启看门狗之后,会每隔leaseTime/3的时间进行续期leaseTime/3。
看门狗核心代码
下面只是指出最核心代码,并无细致逐行代码分析。
定时续期实现
业务代码:
RLock rLock = redissonClient.getLock(lockKey);
try {
boolean lock = rLock.tryLock(
lockWaitTime,
lockLeaseTime,
unit);
tryLock上锁
该方法为定时续期:
该段代码进行续期:主要判断就是 这个锁是否在redis中存在,如果存在就进行 pexpire 延期
protected CompletionStage<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));
}
解锁实现
Redission的bug相关记录
当前项目中使用的redission版本为:
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson-spring-boot-starter</artifactId>
<version>3.17.7</version>
</dependency>
Redission 版本3.16.0:
- 看门狗不生效问题 mr:https://github.com/redisson/redisson/issues/371。
Redisson 版本3.12.0:
- 看门狗不生效问题 mr:https://github.com/redisson/redisson/issues/2515
Redission 版本3.10.3:
- 看门狗死锁问题 mr:https://github.com/redisson/redisson/issues/1966
Redission依旧会产生需要考虑的问题
相关解决方案思路:
- 如何解决Redisson中的WatchDog因JVM GC SWT存在的失效风险?:https://segmentfault.com/q/1010000044673539
- redis分布式锁的思考:https://blog.csdn.net/weixin_43887958/article/details/118336001
下面列举可能遇到的场景:
- JVM STW导致的锁过期问题,也就是JVM整个层面的stw情况,stw导致所有的线程都无法进行正常工作,如果时间过长,导致锁过期了,那么就需要在释放锁的时候去判断当前锁是否存在,不存在的话就抛出异常,若存在且是别的线程拿到的锁,也要抛出异常。
- 另一种极端的情况:若是redis主从集群,那么有可能在异步复制时,主节点返回成功给线程1后,并未将数据同步到从节点就宕机了,之后从节点被升级为主节点,线程2也去获取锁A成功,线程1也获取成功。
**说明:**需要做一些补偿措施,一般的补偿措施就是将事务回滚。
**实现思路:**线程释放锁的时候应该判断当前锁是否存在,若不存在(1.key A过期,2.主节点未将key A同步到从节点就宕机了)就一定要回滚事务。在上述线程1和2都拿到锁的情况下,有没有问题呢?其实是没什么大问题的,因为线程A释放锁的时候会发现持有者不是自己,释放不掉,这个时候怎么办?只需要做一下补偿就行了,例如将增删改的数据还原即可。