目录
前言
背景
问题排查步骤
构造测试用例
分析
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是什么时候被占用,又是在什么时候释放的?
@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();
}
}
@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语句上):
表明:
还有一个有趣的现象,如下图:
表明:整个service确实没有事务,否则在service method进入之前,就被spring的事务增强处理了。
上面两张图,让我陷入了一脸懵逼的状态,这和理论知识对不上啊,那么,来看一下一张源码截图:
没错,这个对象就是驱动我们定义的JpaRepository的执行类,它居然自己加了@Transactional。
但是,理论上事务只是增强了JPA的方法,在repository.findAll()执行结束后,事务增强就结束了,connection应该被归还了才对呀!
这个问题的排查需要通过debug一遍一遍看源码,我就不罗嗦了,直接上结论:
就是这个属性引起的,默认值是true。
在启用open-in-view时,spring在整个controller的调用链路中,只要遇到一次Transactional增强逻辑,就会将connection绑定到ThreadLocal中,并在controller结束时才会释放,其实现原理时利用了springMVC的interceptor。
下面,我把open-in-view禁用,就会发现下面的现象了:
此时,尽管findAll()方法依然被事务增强,但connection能够及时释放了。
到目前为止,我们可以保证testWithoutTx可以及时释放connection了,但如果我们希望在testWithTx方法中,调用testWithoutTx,此时又会是什么情况呢?
一个更神奇的现象出现了,我甚至都没有执行JPA的findAll(),connection就被占用了,而第二图,在end之后connection就释放了,说明前面设置的open-in-view还是起作用的。
那么,是什么原因导致我没有任何DB操作,connection依然被占用了呢?
又是一遍debug读源码,导致这个问题的原因居然是TransactionMananger,下面是源码:
也就是说,JpaTransactionMananger在启动事务的时候,会从datasource中占用connection,那么下面的情况就可能出现了:
这里,我把testWithTx()修改了一下,不去触发任何DB操作,但依然会占用connection资源,符合上面的分析结果。
上面小节中,我们了解了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一样的问题(称为问题可能不合适,只是对于我的使用场景来说不符合逻辑)。
目前为止,还没有试过编程式事务情况下,是否存在上述问题。