spring事务隔离导致数据库连接耗尽而死锁

2020-06-19  本文已影响0人  SAikeilee

记一次压测排查死锁

概述

应用背景

应用是公司内部的基础设施平台,会接收到多个内部平台的数据上报,考虑到后期可能接入平台增多,故对应用展开压力测试查看应用的在高并发情况下表现。压测接口主要是上报数据的接口,观察接口的稳定性。

环境

测试指标

现象

TPS:Transaction Per Second 事务每秒

QPS: Query Per Second 请求每秒

当用户一次操作(一个连接)只请求一个接口,TPS和QPS没有任何区别

当TPS达到30左右,无论如何增添线程数(用户数),TPS不会再上升

排查

  1. 首先进行top,系统负载Load avg平稳,CPU使用率平稳(不高),一般计算密集型应用 CPU 使用率偏高 load 偏低,IO 密集型相反。内存占用在70%左右,相对平稳。

  2. 使用jstat -gcutil查看没有频繁fullGC youngGC。

至此,我开始怀疑是不是代码的质量写的有问题。

  1. 接着按惯例我还是再用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:创建一个新的事务,如果当前存在事务,则把当前事务挂起。也就是说无论如何都创建一个事务

解决

  1. 改变事务隔离级别,PROPAGATION_REQUIRES_NEWPROPAGATION_REQUIRED

    • PROPAGATION_REQUIRED(默认):如果当前存在事务,则加入该事务;如果当前没有事务,则创建一个新的事务。
  2. 增加连接池 getConnection 最大等待时间的配置。

    如果没有获取到连接一定时间则会抛出异常,结束这个线程。至于如何配置,不同的连接池的配置项不同,具体可参考对应的连接池官方文档配置。如果防止部分连接执行时间太长或者数据源泄露,还可以加上Connection最大存活时间配置。

  3. 不使用同一个数据库连接池

    正常来说,id生成的数据库实例应该单独配置实例

  4. 增加事务超时时间配置。(一般情况下不推荐,因为如果sql执行时间超过了超时时间,事务也会等待对应的sql执行完后结束,而在下一次执行sql时候报错)

    通过spring事务注解时候,加上超时时间的属性配置。

    @Transactional(timeout = 60)     //代表事务60秒超时
    

本文纯粹是作者对工作中同小组遇到的压测排查记录,感谢同事wangxi

上一篇下一篇

猜你喜欢

热点阅读