Spring Transaction导致Datasource连接池耗尽问题分析

目录

前言

背景

问题排查步骤

构造测试用例

分析

query语句的连接为什么没有释放?

在testWithTx方法中

无DB操作情况下Connection被占用

Spring-JdbcTemplate是否存在上面的问题?

结论


前言

由于本人对spring没有深入研究,这篇文章只是针对遇到的问题以及其产生原因进行简单记录。

背景

在使用spring-boot-2.4.2开发的项目中,有一个controller,需要调用RPC服务,而调用RPC所需要的数据都存放在DB中。

RPC调用,在非法输入的情况下,会阻塞整个controller reqeust,之后系统就会频繁出现can not aquire connection from the db pool的问题,从而带崩整个web服务。

项目中使用spring-data-jpa和spring jdbcTemplate进行数据库操作。

问题排查步骤

构造测试用例

首先需要了解,Datasource的connection是什么时候被占用,又是在什么时候释放的?

  • 增加一个bean,打印connection pool的使用情况
@Component
public class TestComponent {
    @Autowired
    private HikariDataSource dataSource;
    @PostConstruct
    public void init() {
        Thread thread = new Thread(()->{
            while(true) {
                System.out.println(dataSource.getHikariPoolMXBean().getTotalConnections()
                        +":::"+dataSource.getHikariPoolMXBean().getActiveConnections());
                try {
                    TimeUnit.SECONDS.sleep(3);
                } catch (InterruptedException interruptedException) {
                    interruptedException.printStackTrace();
                }
            }
        });
        thread.setDaemon(true);
        thread.start();
    }
}
  • 创建测试用的TestController、TestService
@RestController
@RequestMapping("/test/")
public class TestController {
    @Autowired
    private TestService testService;
    @GetMapping("test")
    public String test() {
        this.testService.testWithoutTx();
//        this.testService.testWithTx();
        return "{\"a\":1}";
    }
}
@Service
public class TestService {
    //jpa repository
    @Autowired
    private ClusterBasicRepository repository;
    /**
     * 无事务增强
     */
    public void testWithoutTx() {
        System.out.println("before query");
        repository.findAll();
        System.out.println("after query");
        try {
            TimeUnit.SECONDS.sleep(10);
        } catch (InterruptedException interruptedException) {
            interruptedException.printStackTrace();
        }
        System.out.println("end");
    }

    /**
     * 事务增强
     */
    @Transactional
    public void testWithTx() {
        ((TestService)AopContext.currentProxy()).testWithoutTx();
    }
}

(插一句:网上存在一个说法,spring的动态代理,如果target实现了接口,使用JDK动态代理,如果target没有实现接口,使用CGLIB;但是我在debug的过程中,就算TestService实现了接口,依然使用CGLIB,与之前的理论相悖,目前还没找出原因,当然,也不是这篇文章的重点。)

分析

分析前,先来说一下spring的事务增强原理:利用ThreadLocal,将Connection与Thread进行绑定,也就是说,有事务的情况下,至少在事务增强的方法中(超出事务增强依然有可能不释放connection,这个也是本次问题的一个原因),connection是不会返还给connection pool的。

首先测试testWithoutTx()方法,输出如下图(breakpoint设置在controller的return语句上):

表明:

  1. 在testWithoutTx的sleep过程中(after query与end之间的输出),connection并没有被释放.
  2. 完成TestService.testWithoutTx(),进入Controller后(end之后的输出),connection依然没有被释放。

还有一个有趣的现象,如下图:

表明:整个service确实没有事务,否则在service method进入之前,就被spring的事务增强处理了

query语句的连接为什么没有释放?

上面两张图,让我陷入了一脸懵逼的状态,这和理论知识对不上啊,那么,来看一下一张源码截图:

 没错,这个对象就是驱动我们定义的JpaRepository的执行类,它居然自己加了@Transactional。

但是,理论上事务只是增强了JPA的方法,在repository.findAll()执行结束后,事务增强就结束了,connection应该被归还了才对呀

这个问题的排查需要通过debug一遍一遍看源码,我就不罗嗦了,直接上结论:

就是这个属性引起的,默认值是true。

在启用open-in-view时,spring在整个controller的调用链路中,只要遇到一次Transactional增强逻辑,就会将connection绑定到ThreadLocal中,并在controller结束时才会释放,其实现原理时利用了springMVC的interceptor。

下面,我把open-in-view禁用,就会发现下面的现象了:

 

 此时,尽管findAll()方法依然被事务增强,但connection能够及时释放了。

在testWithTx方法中

到目前为止,我们可以保证testWithoutTx可以及时释放connection了,但如果我们希望在testWithTx方法中,调用testWithoutTx,此时又会是什么情况呢

 一个更神奇的现象出现了,我甚至都没有执行JPA的findAll(),connection就被占用了,而第二图,在end之后connection就释放了,说明前面设置的open-in-view还是起作用的。

那么,是什么原因导致我没有任何DB操作,connection依然被占用了呢?

无DB操作情况下Connection被占用

又是一遍debug读源码,导致这个问题的原因居然是TransactionMananger,下面是源码:

 

 也就是说,JpaTransactionMananger在启动事务的时候,会从datasource中占用connection,那么下面的情况就可能出现了:

 这里,我把testWithTx()修改了一下,不去触发任何DB操作但依然会占用connection资源,符合上面的分析结果。

Spring-JdbcTemplate是否存在上面的问题?

上面小节中,我们了解了JpaTransactionMananger会导致经过事务增强的方法,只要运行就会占用一个connection,那么非Jpa环境下的TransactionMananger,是不是还会有问题呢

我在去掉Jpa,只使用JdbcTemplate的情况下,使用的TransactionMananger是

 

来看一下它的doBegin()方法:

protected void doBegin(Object transaction, TransactionDefinition definition) {
		DataSourceTransactionObject txObject = (DataSourceTransactionObject) transaction;
		Connection con = null;

		try {
			if (!txObject.hasConnectionHolder() ||
					txObject.getConnectionHolder().isSynchronizedWithTransaction()) {
                //!!!!!!
				Connection newCon = obtainDataSource().getConnection();
				if (logger.isDebugEnabled()) {
					logger.debug("Acquired Connection [" + newCon + "] for JDBC transaction");
				}
				txObject.setConnectionHolder(new ConnectionHolder(newCon), true);
			}

			txObject.getConnectionHolder().setSynchronizedWithTransaction(true);
            //!!!!!!
			con = txObject.getConnectionHolder().getConnection();

			Integer previousIsolationLevel = DataSourceUtils.prepareConnectionForTransaction(con, definition);
			txObject.setPreviousIsolationLevel(previousIsolationLevel);
			txObject.setReadOnly(definition.isReadOnly());

			// Switch to manual commit if necessary. This is very expensive in some JDBC drivers,
			// so we don't want to do it unnecessarily (for example if we've explicitly
			// configured the connection pool to set it already).
			if (con.getAutoCommit()) {
				txObject.setMustRestoreAutoCommit(true);
				if (logger.isDebugEnabled()) {
					logger.debug("Switching JDBC Connection [" + con + "] to manual commit");
				}
				con.setAutoCommit(false);
			}

			prepareTransactionalConnection(con, definition);
			txObject.getConnectionHolder().setTransactionActive(true);

			int timeout = determineTimeout(definition);
			if (timeout != TransactionDefinition.TIMEOUT_DEFAULT) {
				txObject.getConnectionHolder().setTimeoutInSeconds(timeout);
			}

			// Bind the connection holder to the thread.
			if (txObject.isNewConnectionHolder()) {
				TransactionSynchronizationManager.bindResource(obtainDataSource(), txObject.getConnectionHolder());
			}
		}

 上面我用//!!!!标记了几句话,其他的也不用看了,还是和JpaTransactionManager一个逻辑,通过验证,也确实存在和JpaTransactionMananger一样的问题(称为问题可能不合适,只是对于我的使用场景来说不符合逻辑)。

结论

  1. Jpa环境下,建议禁用open-in-view。(当然,将connection绑定到controller也有其优势,不能强制约束)。
  2. @Transactional注解慎重添加,没有数据库操作的尽量不要加。

目前为止,还没有试过编程式事务情况下,是否存在上述问题。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值