今天记录一下在项目上遇到的一个问题,和排查解决过程。
数据库死锁
我们这个项目是 springboot + hibernate + SQL server 基础架构的。
发现问题
在我们的一个地图配置功能页面,修改配置信息点击保存后,接口一直是处理中,被阻塞一直不能完成。
解决问题
- 首先查看服务是不是挂了,首先进服务器看服务状态和负载,发现都正常。所以排查不是服务整体挂了。
- 然后点点其他功能,发现也正常,发现只有跟这个配置相关的页面都阻塞打不开了。这就很明显了,不是代码的问题,可能是数据库的问题,于是连接数据库打开这个页面设计的表,发现数据库这个表也打不开,阻塞。ok这就确定了是数据库的问题,数据库锁表了。
- 赶快查chatgpt,搜索一下怎么查询数据库锁,然后就开始查询如下图:
截图很明显,pid 286 给表SESGIS_BASIC_INFO_SETS加了LX(排他锁),pid 273 给一共10张表加了LS(共享锁)其中就有SESGIS_BASIC_INFO_SETS这张表。
- 这原因已经找到了,是会话273一开始加了共享锁,然后会话286要加排他锁,一个表加了排他锁就不能加共享锁,加了共享锁就不能加排他锁,所以他们永远阻塞到这了。
- 行,到这里问题定位了,先解决在排查。很简单直接 kill 286 ;
- 回去查看发现,页面确实不阻塞了,爆出了错误,但是至少,其他关联这个表的页面都能正常展示了。
排查问题
先保证线上服务能正常使用了,才能后续再来排查问题。
- 这就根据这个保存接口看代码,可以测试环境断点调试。很快定位到第2430行代码被阻塞。下面截取这个方法的两个关键位置代码。
第一个截图是这个方法开始 先对SESGIS_BASIC_INFO_SETS表做了数据更新,第二个截图是在这个方法的后半部分,又调用了公司封装的一个工具类JSONUtil.generateObjectFromJson方法,传入了数据库的连接session工厂,所以这里面一定也有数据库操作。 - 在看这个方法里面
这里面调用了我们的session工厂的openSession,这是再创建一个新的会话,这就是关键,这整个大方法本来就在一个事务里的, 这是在一个事务里开了两个session访问数据库,刚刚说的第一张截图中方法开始就对这个表做了更新操作,这里就是要加排他锁的,然后在下面又要加共享锁去读取,那肯定要等待排他锁释放才能加共享锁,但是你们在一个事务里, 你读取数据不完成我事务也没法提交啊。所以这就形成了最终的死锁。
这就发现了死锁的原因是这个底层的JSONUtil工具类导致的。
根本解决问题
这就彻底解决这个问题吧。
这个JSONUtil工具类中openSession就开辟了新的会话,我们只要它不开启新的,我已经传递了工厂给你,你就用当前的就好了,直接调用getCurrentSession()就可以了,但是这是底层封装好的,而且是别的部门封装的,咱们也不能改,也不能改,后来询问了地图开发的同事,它们遇到过这个问题没?
结果它们其实已经又了解决办法,要把数据库的事务隔离级别升级成:**READ COMMITTED SNAPSHOT(读已提交快照)**这个事务隔离级别的查询语句不会加锁。
然后我查看原本数据库配置:
原来事务隔离级别isolation level:RC,而且还有一个重要配置lock_timeout:-1,这个是锁超时等待时间,-1就是一直等,导致我们页面一直阻塞,这个参数要设置一个合理时间,当发生死锁时,数据库也会在这个时间后自动报错释放锁。
最后关闭数据库服务器的防火墙,断开所有连接后,把事务隔离级别调整成READ COMMITTED SNAPSHOT 再打开防火墙,就都可以恢复正常了。
总结
最后有三种解决方案:
- 在使用sessionFactory时,不要新建session,使用当前的session就可以了。
- 其实可以把方法开始的更新方法放到最后,这样就会先加共享锁,等查询都完成后,最后再加排他锁更新,就不会死锁了。
- 修改事务隔离级别到READ COMMITTED SNAPSHOT ,这样查询就不会加锁了。
欢迎关注我的博客:http://he-bi.cn/#/