事务传播获取不到数据库连接的问题记录

20 篇文章 2 订阅

目录

背景

现状

问题描述

问题定位

总结

参考文献


背景

       项目上有一个业务场景,处理的流程非常长。 对于前端的操作来说,就是一个提交按钮,但是对于后端的逻辑来说,链路比较多,简单梳理下如下:

       ① 根据入参做相关数据查询

       ② 更新相关表的数据

       ③ 更新ES中对应记录的数据

       ④ 调用第三方接口回传数据

       ⑤ 数据回传成功后更新相关表数据

       其中④和⑤是一个整体,要么都成功,要么都失败,并且它们的失败不会影响①②③的数据状态。所以操作上,对于①②③是一个事务,对于④⑤是一个事务,两者之间不关联。 

现状

       目前的代码组织方式如下:

@Autowired
private TransactionService transactionService;

@Override
@Transaction(rollbackFor = Exception.class)
public RspBo funMain(ReqBo req){
   //① 根据入参做相关数据查询
   //② 更新相关表的数据
   //③ 更新ES中对应记录的数据
   transactionService.funcA();
}


@Service
public class TransactionServiceImpl {

    @Transactional(rollbackFor = Exception.class, propagation = Propagation.REQUIRES_NEW)
    public void funcA() {
         // ④ 请求第三方接口
         // ⑤ 相关表更新操作
    }
}

       上游的Controller中调用funMain。 从这个代码看的话,funMain中的前三步操作,独享事务,funcA方法独享事务,因为事务的传播机制是REQUIRES_NEW。

       看上去,代码也没啥毛病,也符合预期了。 

问题描述

       实际做测试的时候,发现偶发会出现整个系统不可用,全部加载等待的情况。 跟踪后发现是所有数据库连接都是用完了,新的请求进来,拿不到数据库连接,一直等待,所有页面都是加载等待。 

       直观上想,这不就是数据库连接太少了吗,加,加多点,直接加成2000。 好,问题暂时解决,但是压测的时候,还是会出现同样的问题,也就是说,高并发情况下,会出现这一批进来的请求全部失败,然后后续的也一直失败。 按常理说,如果是数据库连接不够,总会有先处理完成的释放了,后来的继续用,也不应该出现全部失败啊?

问题定位

       尝试将数据库最大连接数改成4,然后一次并发请求进来4个。

# 最大活动可用连接数
spring.datasource.druid.max-active=4
# 获取连接最大等待时长,单位秒
spring.datasource.druid.max-wait=5000

       想当然的认为,最大4个,请求进来4个,那这4个应该是可以成功的吧,但是实际情况却是,没有一个成功,所有请求报错都是如下:

org.springframework.transaction.CannotCreateTransactionException: Could not open JDBC Connection for transaction; nested exception is com.alibaba.druid.pool.GetConnectionTimeoutException: wait millis 5001, active 4, maxActive 4, creating 0
	at org.springframework.jdbc.datasource.DataSourceTransactionManager.doBegin(DataSourceTransactionManager.java:305)
	at org.springframework.transaction.support.AbstractPlatformTransactionManager.startTransaction(AbstractPlatformTransactionManager.java:400)
	at org.springframework.transaction.support.AbstractPlatformTransactionManager.handleExistingTransaction(AbstractPlatformTransactionManager.java:434)

       为什么?明明最大4个,请求也是4个,而且没有其他请求,这怎么全部都失败了?感觉不科学啊!

       问题的主要原因在于:

       funMain这个方法有事务注解,进入这个方法的时候,就会申请使用一个新的数据库连接,而funA的事务传播是开启新事物,对应的也会申请一个新的数据库连接。 因为同时进来4个请求,这四个请求上去就申请了4个链接,每个线程中再往后走funcA的时候,funcA获取不到数据库连接了,所以全部报异常。  

       就这个场景而言,数据库连接的最大数设置为5,最大并发为4的时候,就可以全部正常。

总结

       对于这个问题,改造优化的方式有以下几种:

       ① tomcat的最大工作线程数一定要小于连接池的最大连接数,即:server.tomcat.threads.max值要小于spring.datasource.druid.max-active 。

       ② 从代码方面优化,可以再往上抽一层,因为funMain本身是独立的,完全可以将funMain中前三步的内容也抽出来放到一个子方法中加上事务,这样funMain就不用带事务注解了,逻辑顺序执行,前三步方法执行完提交事务,继续走funcA方法,这样始终使用的都是一个数据库连接。 

       相对而言,②是更合理的方案,主要还是需要从代码层面注意,写代码的时候多思考。 

       之前一直怀疑是不是mysql连接不释放的问题,最后发现并不是,对于连接释放这块:

       (1)事务对应方法执行完毕,连接就会放回连接池。如果方法没有加事务,正常的查询结束后,连接也会直接放回连接池。

       (2)如果子方法没有设置事务传播机制,默认会开启一个新事物,如果父级有事务,就和父级使用同一个事物,如果父级没有,就新开一个事务。对于数据库的连接而言,只要在一个事务内,就只会使用一个链接。 但是对于PROPAGATION_NESTED嵌套事务,则会和父级公用一个连接,在一个事务内。 

       (3)就这个场景而言,前三步涉及到ES的变更,后两步涉及到http请求以及数据库数据变更,这看似简单,但是实际都是比较棘手的场景,因为都涉及到了分布式事务的问题。 对于①②③而言,相对容易些,讲ES的更新放到最后,并且加上try/catch,如果es更新失败,则抛异常,让事务回滚,这时候也不会再走funcA。 而对于funcA中的④⑤就要复杂些,因为业务要求必须是调用http请求成功后,才能做表数据的更新,如果调用http请求失败,比较简单,直接不往后走了,不涉及事务;如果调用http请求成功,但是⑤相关的mysql操作失败了,如何回退?这种也是一种典型的分布式事务场景。 一般来说,我个人建议有两种解决方式:

一、http接口提供回退的接口,⑤如果失败了,在catch中调用http接口的回退接口,将http接口业务回退,并且这里要设置自动重试。然后抛出异常,事务回滚。 

二、加事务补偿机制,可以是人工,可以是自动。在⑤的catch中,将异常记录,将相关数据记录记录到事务补偿表或者文件中,定期重新提交或者提供运维界面,支持手工提交和数据修复。至少在数据层面实现功能闭环。

参考文献

【1】Spring中事务嵌套使用一定得警惕这个问题了!! - 知乎 (zhihu.com)

【2】spring事务、数据库事务使用及原理详解 - 知乎 (zhihu.com)

  • 12
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值