公司项目开发时,遇到了一个死锁问题,项目会时不时的死掉,于是开始了对死锁问题的排查。
偶然间,项目再一次出现了死锁问题。
使用如下命令查看当前java进程堆栈
jstack -l 线程号 > 文件名
比如:
jstack -l 22194 > 1.txt
意思就是,打印22194
这个进程的堆栈内容到当前目录的1.txt
文件中。
注:如果不知道当前线程id可通过命令根据java端口号去查找或者在进程列表中查找。
通过打印,我们可以观察到阻塞的进程情况,他执行到哪一行代码卡住的,个人建议从下往上看,因为下面是最早出现的,当引发的源发生阻塞后,新来的资源就一定阻塞。
最后定位到问题所在地方:在最开始,有多个线程在获取数据源时候卡住,导致数据库连接池连接被占满,而后所有线程全部处于等待状态,引发死锁。
继续观察堆栈,定位到执行sql的代码,发现这块代码
这行代码其实是调用了一个存储过程,由于该存储过程代码的特殊性,导致他不能在事务中执行这个代码,故通过事务隔离机制将其排除,但排除后存在一个可能会引发死锁的问题:
假设连接池最大连接数为8个,同时有8个线程在事物内获取了数据连接,此时他们均需要调用这个存储过程,由于不在一个事务内,就需要新获取一个连接,此时都在相互等待,引发死锁!!
注意,在spring中,如果外面的8个线程不是在事务内调用是不会死锁的,因为不在事务内,每执行一次sql都是一个新的连接,但如果在同一事务内,直到事务提交,这个连接才会被释放。
也就是说,在spring中,事务内共用一个连接,不在事务内,每次执行sql会开启新的连接。
问题延伸:spring事务传机制是否在高并发下都有可能产生死锁呢?
经过本人测试,确实是有可能的!!!
解决办法:
1、增加连接池 getConnection 最大等待时间的配置。(简单,粗暴)
如果没有获取到连接一定时间则会抛出异常,结束这个线程。至于如何配置,不同的连接池的配置项不同,具体可参考对应的连接池官方文档配置。如果防止部分连接执行时间太长或者数据源泄露,还可以加上Connection最大存活时间配置。
2、还有一种解决方案就是,不使用同一个数据库连接池。(对于性能要求高的时候推荐此方案)
对于这个问题,博主采用的是这个方案。 就是执行这个存储过程使用另一个数据源,而不共用主数据源,这样也就可以避免死锁。至于数据源如何去切换,每个人可以有不同的选择,本人使用的是苞米多提供的一个动态数据源切换的框架解决的,但不限于一定使用这个框架,只要能达到数据源切换目的即可。
如果你想了解这个动态数据源切换框架,请查看:https://mp.baomidou.com/guide/dynamic-datasource.html
3、还有一个方案,增加事务超时时间配置。(一般情况下不推荐,因为如果sql执行时间超过了超时时间,事务也会等待对应的sql执行完后结束,而在下一次执行sql时候报错)
通过spring事务注解时候,加上超时时间的属性配置。
@Transactional(timeout = 60) //代表事务60秒超时