spring事务隔离导致数据库连接耗尽而死锁
记一次压测排查死锁
概述
应用背景
应用是公司内部的基础设施平台,会接收到多个内部平台的数据上报,考虑到后期可能接入平台增多,故对应用展开压力测试查看应用的在高并发情况下表现。压测接口主要是上报数据的接口,观察接口的稳定性。
环境
-
机器:2-core、4G DGRAM + 30G Disk
-
JDK1.8,-xms 2G -xmx2G
-
Tomcat
Tomcat接受请求过程:在accept队列中接收连接(当客户端向服务器发送请求时,如果客户端与OS完成三次握手建立了连接,则OS将该连接放入accept队列);在连接中获取请求的数据,生成request;调用servlet容器处理请求;返回response。
accept-count为socket请求连接队列数量,maxThreads为线程池最大数量,maxConnections为最大同时连接数
- 接受和处理的最大连接数maxConnections:500
- 请求处理最大线程数maxThreads:28
- 队列长度accpet-count:200(默认值为100)
-
druid
- 初始大小initial-size: 2
- 最大数据库连接数max-active:20
- 最小空闲数量min-idle:2
- 最大等待毫秒数max-wait:600
测试指标
- JVM内存运行稳定,无OOM,没有不合理大对象
- CPU、内存、网络、磁盘、文件句柄占用平稳
- 无频繁线程锁、线程数平稳
- 业务线程负载均衡
- 异常率小于0.1%
现象
TPS:Transaction Per Second 事务每秒
QPS: Query Per Second 请求每秒
当用户一次操作(一个连接)只请求一个接口,TPS和QPS没有任何区别
当TPS达到30左右,无论如何增添线程数(用户数),TPS不会再上升
排查
-
首先进行top,系统负载Load avg平稳,CPU使用率平稳(不高),一般计算密集型应用 CPU 使用率偏高 load 偏低,IO 密集型相反。内存占用在70%左右,相对平稳。
-
使用jstat -gcutil查看没有频繁fullGC youngGC。
至此,我开始怀疑是不是代码的质量写的有问题。
- 接着按惯例我还是再用jstack查看堆栈,结果发现好多个线程在Waiting状态,从下往上查看,主要是在申请数据库连接时候getConnection()。
在最开始,有多个线程在获取数据源时候卡住,导致数据库连接池连接被占满,而后所有线程全部处于等待状态,引发死锁。
最后定位到id生成器的一段代码上面去
image-20200619145716098然后向上定位,发现原来事务级别为3,REQUERIES-NEW,最后发现代码位于自定义的ID生成器上面
image-20200619145955568先概括一下死锁原因,Spring事务传播引发连接池死锁。
当服务需要分布式id时,会首先从数据库中获取一个start_id,然后将start_id更新成start_id+step。那么从start_id~start_id+step段内对的所有id,都属于当前这个服务了。如果start_id用完了,就会按照相同的流程重新申请一个start_id。
线程本身开启事务(每个事务占用一个数据库连接),然后使用id生成器申请Id,Id生成器发现Id不够用,于是再开启一个事务向数据库拿id,发现连接不够用了,于是等待连接池别的线程释放连接,而别的线程也在等待id生成器的id,形成互相等待局面——死锁。
Spring事务级别3,其实是TransactionDefinition.PROPAGATION_REQUIRES_NEW:创建一个新的事务,如果当前存在事务,则把当前事务挂起。也就是说无论如何都创建一个事务
解决
-
改变事务隔离级别,PROPAGATION_REQUIRES_NEW到PROPAGATION_REQUIRED
- PROPAGATION_REQUIRED(默认):如果当前存在事务,则加入该事务;如果当前没有事务,则创建一个新的事务。
-
增加连接池 getConnection 最大等待时间的配置。
如果没有获取到连接一定时间则会抛出异常,结束这个线程。至于如何配置,不同的连接池的配置项不同,具体可参考对应的连接池官方文档配置。如果防止部分连接执行时间太长或者数据源泄露,还可以加上Connection最大存活时间配置。
-
不使用同一个数据库连接池
正常来说,id生成的数据库实例应该单独配置实例
-
增加事务超时时间配置。(一般情况下不推荐,因为如果sql执行时间超过了超时时间,事务也会等待对应的sql执行完后结束,而在下一次执行sql时候报错)
通过spring事务注解时候,加上超时时间的属性配置。
@Transactional(timeout = 60) //代表事务60秒超时
本文纯粹是作者对工作中同小组遇到的压测排查记录,感谢同事wangxi