业务背景:
1、项目需要对其他业务系统提供同步业务状态接口,该业务状态更新在项目自身很多业务场景都会有更新操作,在所有业务接口上都是加redis并发锁。
2、项目使用3.17.0版本的redisson依赖包,其中redis使用lettuce客户端。
问题描述
测试同学反馈从APP上测试一直提示系统繁忙,请稍后重试,这错误不是我自己定义的吗?没有获取到并发锁返回错误信息。redis客户端界面查询对应业务锁,锁一直存在,如下截图。是以hash值存入redis的。看后续源码。hash里的row=1的key、value值看起来很奇怪!!!
疑问:
1、hash里的key值是指啥?线程id?截图中key前缀像是uuid和数字组成。
2、value是int类型数字,指是啥?怎来的?
单看redis里存的值,看的很懵逼!!!
源码:
1、业务接口:
业务接口写do-while循环三次来处理业务逻辑,为啥加循环,因为其他平台只是同步一次数据,有可能会出现其他平台同步数据同时,项目自身业务已经占有锁,避免数据丢失和死循环,所以加上循环等待获取并发锁,业务接口源码如下(小声逼逼:业务代码已被抠掉了):
截图代码问题有俩处:
第一点:获取到并发锁后,执行业务代码逻辑,并没有跳出循环;
第二点:循环执行4次,并非是3次。
2、Redisson获取并发锁
(1)业务dao层封装
备注:redissonClient是通过注解注入的,代码如下:
@Resource
private RedissonClient redissonClient;
(2)RedissLock类tryLock无参方法
(3)RedissLock类tryLockAsync无参方法,
(4)RedissLock类tryLockAsync(long threadId)方法,这方法没啥好说的,很简单赋值;
(5)RedissLock类tryAcquireOnceAsync方法,重点!重点!重点!
(6)RedissLock类tryLockInnerAsync方法,执行lua脚本,判断当前key在redis里是否存在,若存在,则value+1;
备注:getLockName方法得到字符串:87ffb241-1906-4468-9014-3fda98edb858:133;133是指当前执行业务线程id,冒号前面字符串是指连接redis线程id。
(6)RedissBaseLock类scheduleExpirationRenewal(long threadId)方法
备注:scheduleExpirationRenewal方法中try里finally执行判断有点意思。在本地断点调试时,过了很久才进入这个方法,最后执行完成后,redis并没有出现死锁情况。看这里才搞明白,finally会判断当前线程是否已中断,中断会自动释放锁
(6)RedissBaseLock类renewExpiration()是实现看门狗模式,重中之重!!!(想不到其他词来形容0^0)
3、Redisson释放并发锁
(1)业务dao层代码
isLocked()是判断当前锁是否已释放,isHeldByCurrentThread判断当前并发锁是否属于当前线程
(2)RedissonLock类的unlock无参方法
(3)RedissonBaseLock类的unlockAsync(long threadId)方法,其中unlockInnerAsync方法释放redis的锁(使用lua脚本,下一步详细解释),当释放成功后,会将刷新过期时间定时任务取消cancelExpirationRenewal(threadId)方法。
(4)RedissonLock类unlockInnerAsync(long threadId)方法,lua脚本释放并发锁,这里释放逻辑并不是直接删除,是将redis中hash数据里的key对应value减去1。
(5)RedissBaseLock类cancelExpirationRenewal方法,释放锁和取消定时任务
疑问回答
Redisson中看门狗存入redis数据格式是hash。hash名称是业务定义的redis的key,hash中的key是指客户端连redis线程id+冒号+业务线程id,value是一个计数,如果正常业务value值应该是1。按业务接口代码写代码逻辑:在同步一个线程里,循环获取锁,锁会正常获取,并不会返回false,根据上述获取锁的lua源码可以看出来,如果锁已存在,value会加1,释放锁是对应value减1,循环获取4次锁,finally里释放一次锁,最终业务线程结束后,redis里的锁并没有删除调,还是会有定时任务刷新redis锁的过期时间。这就是会出现死锁情况。此外,在断点调试时,业务线程执行完成后,Redis里不会有锁存在,大概因为断点调试停留时间过程,业务线程停留太长时间,线程中断,导致走了取消定时任务刷新逻辑(方法:cancelExpirationRenewal),只是猜测,并没有验证。
总结:
使用Redisson的看门狗模式要慎重,在业务代码使用循环获取并发锁,慎重慎重慎重!!以上存在问题的地方,欢迎各位大佬指出来。0.0