记一次 Redisson 线上问题 → 你怎么能释放别人的锁

开心一刻

今天,我的又一个好哥们脱单了,只剩下我自己单身了

我向一个我喜欢的女生吐苦水

我:我这辈子是找不到女朋友了

她:怎么可能,你很优秀的,会有很多女孩子愿意当你女朋友的

我内心窃喜,问道:那你愿意当我女朋友吗

她:我都在开导你了,你不要恩将仇报!

线上问题

生产环境突然告警,告警信息:

attempt to unlock lock, not locked by current thread by node id: b9df1975-5595-42eb-beae-bdc5d67bce49 thread-id: 52

查看日志,找到对应的堆栈信息

java 复制代码
Exception in thread "thread0" java.lang.IllegalMonitorStateException: attempt to unlock lock, not locked by current thread by node id: b9df1975-5595-42eb-beae-bdc5d67bce49 thread-id: 52
	at org.redisson.RedissonLock.lambda$unlockAsync$4(RedissonLock.java:616)
	at org.redisson.misc.RedissonPromise.lambda$onComplete$0(RedissonPromise.java:187)
	at io.netty.util.concurrent.DefaultPromise.notifyListener0(DefaultPromise.java:578)
	at io.netty.util.concurrent.DefaultPromise.notifyListenersNow(DefaultPromise.java:552)
	at io.netty.util.concurrent.DefaultPromise.notifyListeners(DefaultPromise.java:491)
	at io.netty.util.concurrent.DefaultPromise.addListener(DefaultPromise.java:184)
	at org.redisson.misc.RedissonPromise.onComplete(RedissonPromise.java:181)
	at org.redisson.RedissonLock.unlockAsync(RedissonLock.java:607)
	at org.redisson.RedissonLock.unlock(RedissonLock.java:492)
	at com.qsl.ResissonTest.testLock(ResissonTest.java:41)
	at java.lang.Thread.run(Thread.java:748)

翻译过来就是

企图去释放锁,不被当前线程(node id: b9df1975-5595-42eb-beae-bdc5d67bce49 thread-id: 52)锁住

也就是:当前线程企图去释放别的线程的锁

怎么能释放别人的锁?

基础回顾

在排查问题之前,我们先弄清楚

node id: b9df1975-5595-42eb-beae-bdc5d67bce49 thread-id: 52

node idthread-id 是什么

关于 thread-id,我相信大家都理解,就是抛异常的线程的 id,没问题吧?那 node id 呢?

我用八股文引导下你们

问:redisson 用的 redis 的什么数据类型来实现锁的

答:hash

问:那 hash 中的 keyfieldvalue 的值分别是什么

答:key 的值是锁名,field 的值是 线程idvalue 的值是重入次数

问:如果多个服务同时去获取一把锁,field 的值是不是有可能相同,比如服务A获取锁的线程的 thread-id 是 52,服务B获取锁的线程的的 thread-id 也是 52

此时你是不是有点慌了,但依旧嘴硬的回答:有可能相同

问:那没问题吗,A服务的线程(thread-id=52)拿到锁后,正在执行业务处理,B服务的线程(thread-id=52)也能拿到锁,这不是锁了个寂寞?

答:呃...嗯...

很显然漏了个细节,那就是 field,其值不是 线程id,而是 node id:thread-id,例如:b9df1975-5595-42eb-beae-bdc5d67bce49:52 ,而这个 node id 就是 redisson实例id,用以区分分布式下的 redisson 实例

Redisson 分布式锁实现之源码篇 → 为什么推荐用 Redisson 客户端 有很详细的介绍,值得你们看看

释放别人的锁

talk is sheap show me the code

这代码,我相信大家都能看懂,但我还是说明下

  1. 构造锁
  2. 尝试获取锁,等待时间1s,持锁3s
  3. 如果获取到锁,则进行业务处理,没获取到锁,则打印 锁获取失败
  4. finally 保证异常和非异常情况下,锁都能释放

是不是很正常,但真的没 bug

我们调整下代码

运行 multiThreadLock,异常就来了

从打印信息,我们应该能分析出问题出在哪

  1. 线程52获取到锁,执行业务中
  2. 线程53尝试获取锁,但锁被线程52持有
  3. 线程53 1s内获取锁失败
  4. 线程53 来到 finally,判断锁是否被持有,发现是被持有的,释放锁
  5. redisson 释放锁的时候,发现锁的持有线程并非当前线程,抛出异常

线程53,你怎么回事,怎么能释放别人的锁?可不能怪线程53,代码可是我们写的,看看提交记录,非得把这个二臂揪出来!!!

算了算了,还是别揪了,我们继续看如何修复

问题修复

既然找到问题了,修复问题就很简单了,方式有以下几种

提高等待时长

将获取锁的等待时长提高,但这种方式只能减少异常,并不是完全修复异常;因为会有多个线程同时竞争锁,等待时长设置成多少都不合适,除非设置成不超时,但是设置成不超时,可能会导致等待的线程太多,造成线程不够用的情况。不推荐该方式

自动释放

去掉 finally,相当于把产生异常的源头给干掉了,那肯定就不会有异常了嘛,这不就是我们常提到的

解决不了问题,那就把提出问题的人解决掉

不主动释放锁,让锁自动到期释放,因为我们设置了锁持有时长是 3s,3s 后就自动到期释放了。但在实际业务中,我们往往会把锁持有时长设置的比较大(远大于业务执行的平均时长),保证业务不会并发执行,如果业务执行完了不主动释放锁,就会导致很长时间内锁被无效占用,后面的线程获取锁也只能白白等待。不推荐该方式

记录获取状态

直接看代码,你们就懂了

如果业务执行时间超过 3s,会怎么样,我们把睡眠时间改成 5s,执行下 testLock,你会发现同样的异常又出现了!!!

我们来分析下,锁持有时长是 3s,而业务执行时长是 5s,也就说业务还没执行完,锁已到期,redis 自动释放了,业务执行完之后我们再去释放锁,锁都没了,怎么释放?所以 redisson 抛出异常了;所以释放锁的时候,还需要加一个条件

if (acquired && lock.isLocked())

acquired 表示当前线程是否获取到锁了,而 lock.isLocked() 表示是否有线程持有锁,如果都为 true,那就说明是当前线程持有锁,释放就没问题了。可以用,但不推荐,因为有更优雅的处理方式

判断持有者

这种写法更优雅

就直接判断锁是不是当前线程持有,是就可以释放;就不用去管锁是别的线程持有,还是到期自动释放了。推荐该方式

总结

  1. 示例代码地址:redisson-spring-boot-demo
  2. 加锁的目的就是为了保证业务单线程执行,所以锁的持有时长一定要设置大一点,不然极端情况下,业务还在执行中,锁却到期了,就违背了加锁的初衷
  3. 锁一定要主动释放、一定要主动释放、一定要主动释放,与业务无关
  4. 释放锁的时候,要判断是否是当前线程持有,都不是你的锁,你凭什么释放
相关推荐
一直要努力哦2 小时前
Redis最终篇分布式锁以及数据一致性
数据库·redis·缓存
跳动的梦想家h5 小时前
黑马点评 秒杀下单出现的问题:服务器异常---java.lang.NullPointerException: null(已解决)
java·开发语言·redis
YiSLWLL6 小时前
Django+Nginx+uwsgi网站使用Channels+redis+daphne实现简单的多人在线聊天及消息存储功能
服务器·数据库·redis·python·nginx·django
清心歌7 小时前
Redis入门(九)
数据库·redis
烟雨长虹,孤鹜齐飞7 小时前
【分布式锁解决超卖问题】setnx实现
redis·分布式·学习·缓存·java-ee
Mephisto.java7 小时前
【大数据学习 | Spark-Core】关于distinct算子
大数据·hive·hadoop·redis·spark·hbase
吃着火锅x唱着歌9 小时前
Redis设计与实现 学习笔记 第二十章 Lua脚本
redis·笔记·学习
冷瞳9 小时前
Redis基本的全局命令
数据库·redis·缓存
Hacker_Fuchen10 小时前
Redis密码设置与访问限制(网络安全)
redis·web安全·bootstrap
乄bluefox14 小时前
SpringBoot中使用Sharding-JDBC实战(实战+版本兼容+Bug解决)
java·数据库·spring boot·redis·后端·缓存·bug