Spring事务连接复用解密:DataSourceTransactionManager详解
2025-04-09 10:37:01
解密 Spring 事务:DataSourceTransactionManager 何时复用数据库连接?
咱们在用 Spring 的 DataSourceTransactionManager
处理数据库事务时,可能会注意到一个现象:doBegin
方法并不总是去数据源 (DataSource
) 重新获取一个新的数据库连接 (Connection
)。有时候,它会选择复用当前线程上已经存在的连接。这跟咱们直观感觉上“每个新事务都应该有个全新连接”的想法似乎有点不一样。
代码里控制这个行为的关键判断是这样的:
// DataSourceTransactionManager.java -> doBegin(...)
if (!txObject.hasConnectionHolder() ||
txObject.getConnectionHolder().isSynchronizedWithTransaction()) {
// 条件成立:获取新连接
Connection newCon = this.dataSource.getConnection();
// ... (处理新连接)
txObject.setConnectionHolder(new ConnectionHolder(newCon), true);
} else {
// 条件不成立:复用 txObject 中已有的 ConnectionHolder 里的连接
// (代码块不存在,逻辑隐含在后续步骤中直接使用 txObject.getConnectionHolder().getConnection())
}
这行 if
语句的意思是:
- 如果
txObject
(代表当前事务状态的对象)里 没有 包含ConnectionHolder
(也就是当前线程还没绑定连接),或者 txObject
里 有ConnectionHolder
,但这个 Holder 已经被标记为isSynchronizedWithTransaction()
(表示这个连接已经和一个活动的 Spring 事务同步了)。
只要满足上面两个条件之一,doBegin
就会从 dataSource
获取一个 新 连接。
那么反过来,什么时候会跳过这个 if
语句块,不去获取新连接,而是复用 txObject
里已有的那个 ConnectionHolder
中的连接呢? 这就要求 if
的条件 不 满足,也就是说,必须 同时 满足以下两点:
txObject.hasConnectionHolder()
为true
:当前线程确实已经绑定了一个ConnectionHolder
。txObject.getConnectionHolder().isSynchronizedWithTransaction()
为false
:这个绑定的ConnectionHolder
没有 被标记为与一个(已启动的)Spring 管理的事务同步。
搞清楚这一点,咱们就能分析出具体是哪些场景导致了连接复用。
一、Spring 事务与线程绑定连接的基础
要理解连接复用的逻辑,得先明白 Spring 是怎么管理事务中的资源的,尤其是数据库连接。
核心机制是 TransactionSynchronizationManager
。这家伙负责管理事务同步,并且利用 ThreadLocal
变量来维护 当前线程 的事务状态和资源。
当一个事务启动(通常是通过 @Transactional
注解或者编程式事务),Spring 会:
- 获取资源: 对于数据库事务,就是从
DataSource
获取一个Connection
。 - 封装资源: 把这个
Connection
包装进一个ConnectionHolder
对象。ConnectionHolder
不仅仅持有连接,还包含一些状态信息,比如引用计数、是否与事务同步等。 - 线程绑定: 调用
TransactionSynchronizationManager.bindResource(dataSource, connectionHolder)
,把这个ConnectionHolder
和数据源 (dataSource
) 关联起来,并存储到当前线程的ThreadLocal
变量里。
这样一来,在这个事务的执行期间,同一个线程上的任何代码,只要通过 DataSourceUtils.getConnection(dataSource)
或者相关的 Spring 工具类去获取连接,都能拿到这个已经被绑定好的、同一个 Connection
实例,保证了事务内操作的一致性。
当事务结束(提交或回滚)时,TransactionSynchronizationManager.unbindResource(dataSource)
会被调用,解除连接与线程的绑定,并且 DataSourceUtils.releaseConnection(connection, dataSource)
会负责把连接还给连接池(如果使用了连接池的话)。
二、剖析 doBegin
的连接获取逻辑
现在回到 doBegin
方法里的那个 if
判断:
if (!txObject.hasConnectionHolder() ||
txObject.getConnectionHolder().isSynchronizedWithTransaction()) {
// 获取新连接...
}
// else 分支(隐含):复用现有连接
这个 txObject
(类型是 DataSourceTransactionObject
) 在 doBegin
被调用前,可能已经通过 TransactionSynchronizationManager.getResource(getDataSource())
拿到了当前线程绑定的 ConnectionHolder
(如果存在的话)。
-
!txObject.hasConnectionHolder()
为true
:说明当前线程根本没有绑定与该dataSource
相关的ConnectionHolder
。这通常是启动一个全新事务(没有外部事务)的入口情况。自然,需要从dataSource
获取一个新连接。 -
txObject.getConnectionHolder().isSynchronizedWithTransaction()
为true
:说明线程上虽然绑定了一个ConnectionHolder
,但这个ConnectionHolder
已经被明确标记为“正服务于一个 Spring 事务”。这种情况通常发生在需要启动一个 真正新 的物理事务时,比如事务传播级别是REQUIRES_NEW
。虽然线程上可能有来自外部事务的连接,但REQUIRES_NEW
要求挂起外部事务并开启一个完全独立的事务,所以必须获取一个新的连接。另一个场景是,即使是参与现有事务(如REQUIRED
),但在某些复杂的事务协调或恢复逻辑下,Spring 内部判断需要强制获取或验证连接状态,也可能走到这里。
结合起来看,获取新连接的场景覆盖了:
- 完全没有上下文,新起一个事务。
- 需要一个物理上隔离的新事务(如
REQUIRES_NEW
)。 - 内部逻辑判断需要刷新或使用全新连接资源。
那么,连接复用的条件:txObject.hasConnectionHolder() && !txObject.getConnectionHolder().isSynchronizedWithTransaction()
到底代表什么场景呢?
这意味着:线程上已经有一个 ConnectionHolder
了,但是它还没被正式“认领”并标记为服务于当前正在启动的这个 Spring 事务。
三、场景分析:何时复用连接?
下面分析几种具体场景,看看它们如何匹配上面推导出的复用条件。
场景一:事务传播(Transaction Propagation)- 特别是参与型传播
这是最常见的“看起来像复用”的情况,但机制稍微有点绕。
-
PROPAGATION_REQUIRED
(默认):- 如果外部已经存在一个事务 (
@Transactional
方法调用了另一个@Transactional
方法):AbstractPlatformTransactionManager.getTransaction()
方法会先检查当前线程是否已有活动事务。如果发现有,并且传播级别是REQUIRED
,它会执行handleExistingTransaction
逻辑。这个逻辑通常意味着“加入”现有事务,它会直接复用外部事务的ConnectionHolder
,并且通常不会再次调用doBegin
。内部方法的操作会直接在外部事务的那个连接上执行。所以,从效果上看是复用了连接,但不是通过doBegin
里那个else
分支实现的,而是在更早的getTransaction
层面就决定了参与现有事务。 - 如果外部没有事务:
getTransaction
会判断需要启动新事务,调用startTransaction
,进而调用doBegin
。此时,线程上没有ConnectionHolder
(!txObject.hasConnectionHolder()
为true
),所以doBegin
会获取一个 新 连接。
- 如果外部已经存在一个事务 (
-
PROPAGATION_SUPPORTS
:- 如果外部有事务:同
REQUIRED
,参与现有事务,复用连接(通过getTransaction
的早期判断)。 - 如果外部无事务:直接以非事务方式执行,根本不会调用
doBegin
获取连接(除非代码内部手动获取)。
- 如果外部有事务:同
-
PROPAGATION_MANDATORY
:- 如果外部有事务:同
REQUIRED
,参与现有事务,复用连接。 - 如果外部无事务:抛出异常,因为强制要求有事务存在。
- 如果外部有事务:同
-
PROPAGATION_REQUIRES_NEW
:- 无论外部是否有事务,它总要启动一个全新的物理事务。
getTransaction
会调用suspend
来挂起外部事务(如果存在,会临时解绑外部连接),然后调用startTransaction
->doBegin
。由于外部连接被挂起解绑了(或者本来就没有),doBegin
检查时会发现!txObject.hasConnectionHolder()
为true
,于是获取一个 新 连接。
- 无论外部是否有事务,它总要启动一个全新的物理事务。
-
PROPAGATION_NOT_SUPPORTED
:- 如果外部有事务,会挂起它,然后以非事务方式运行代码。
- 如果外部无事务,直接以非事务方式运行。通常不涉及
doBegin
。
-
PROPAGATION_NEVER
:- 如果外部有事务,抛异常。
- 如果外部无事务,以非事务方式运行。
-
PROPAGATION_NESTED
:- 如果外部有事务,它会在 同一个连接 上创建一个 JDBC Savepoint,模拟嵌套事务。
doBegin
会被调用,但因为它是在现有事务连接上操作,txObject.hasConnectionHolder()
为true
,并且isSynchronizedWithTransaction()
可能也是true
(表示已在事务中)。这时是否获取新连接取决于useSavepointForNestedTransaction()
等配置和 JDBC 驱动能力。通常目标是复用连接,但使用保存点。 - 如果外部无事务,行为类似
REQUIRED
,获取新连接。
- 如果外部有事务,它会在 同一个连接 上创建一个 JDBC Savepoint,模拟嵌套事务。
小结一下事务传播场景 :大多数连接复用(如 REQUIRED
加入事务)是在 getTransaction
层面实现的,并不走到 doBegin
的 else
分支。REQUIRES_NEW
则强制获取新连接。NESTED
试图在同一连接上创建保存点。
场景二:非 Spring 事务代码预绑定连接
这个场景就比较精确地命中了 doBegin
里复用连接的条件 (hasHolder && !isSynchronized
)。
设想一下这种情况:
- 你的代码(可能在某个拦截器、Filter,或者业务方法的前置逻辑里,而且 没有 开启 Spring 的
@Transactional
)手动调用了DataSourceUtils.getConnection(dataSource)
。DataSourceUtils.getConnection()
会尝试从TransactionSynchronizationManager
获取当前线程绑定的ConnectionHolder
。如果没找到,并且事务同步是激活的(通常是),它会从dataSource
获取一个新连接,创建一个ConnectionHolder(connection, false)
(注意第二个参数preventDisable
通常是 false),然后调用TransactionSynchronizationManager.bindResource(dataSource, holder)
将其绑定到线程。重点:此时ConnectionHolder
被绑定了,但它内部的isSynchronizedWithTransaction
标志很可能还是false
,因为它不是由一个完整的 Spring 事务启动流程 (doBegin
) 所管理的。
- 紧接着,在 同一个线程 上,你的代码调用了一个标记了
@Transactional
(比如默认的PROPAGATION_REQUIRED
) 的方法。 - Spring 的事务拦截器介入,调用
AbstractPlatformTransactionManager.getTransaction()
。 getTransaction()
发现当前没有活动的 Spring 事务 (isExistingTransaction
为 false),于是决定启动新事务,调用startTransaction
。startTransaction()
内部会检查TransactionSynchronizationManager.getResource(dataSource)
。这次,它 找到 了步骤 1 中手动绑定的那个ConnectionHolder
!它将这个找到的holder
设置到txObject
中,此时txObject.hasConnectionHolder()
为true
。- 调用
DataSourceTransactionManager.doBegin(txObject, definition)
。 - 进入
doBegin
,检查if
条件:txObject.hasConnectionHolder()
是true
。txObject.getConnectionHolder().isSynchronizedWithTransaction()
是false
(因为它是手动绑定,未经过doBegin
完整的事务同步标记)。- 所以,整个
if
条件(!txObject.hasConnectionHolder() || txObject.getConnectionHolder().isSynchronizedWithTransaction())
变成了(false || false)
,结果是false
!
if
块被跳过。后续代码会直接使用txObject.getConnectionHolder().getConnection()
,也就是复用了你在步骤 1 手动获取并绑定的那个连接。- 接下来,
doBegin
会对这个复用的连接进行事务相关的设置:- 设置事务隔离级别 (
DataSourceUtils.prepareConnectionForTransaction
)。 - 如果
con.getAutoCommit()
是true
,会调用con.setAutoCommit(false)
,并记录下来需要恢复 (txObject.setMustRestoreAutoCommit(true)
)。这正是提问者担心的——修改了“外部”连接的状态。 - 调用
prepareTransactionalConnection
(可能为空操作)。 - 将
ConnectionHolder
标记为事务激活:txObject.getConnectionHolder().setTransactionActive(true)
。 - 设置超时。
- 因为这个
ConnectionHolder
不是新创建的 (txObject.isNewConnectionHolder()
为 false),所以 不会 重新绑定到TransactionSynchronizationManager
(因为它本来就已经绑定了)。 - 最重要的,将这个 ConnectionHolder 标记为与事务同步:
txObject.getConnectionHolder().setSynchronizedWithTransaction(true)
。
- 设置事务隔离级别 (
这个场景解释了为什么 doBegin
有时不创建新连接,而是复用一个已存在但“非事务同步”的连接,并对其进行事务化改造(如设置 autoCommit=false
)。
代码示例(示意)
import org.springframework.jdbc.datasource.DataSourceUtils;
import org.springframework.transaction.support.TransactionSynchronizationManager;
import javax.sql.DataSource;
import java.sql.Connection;
public class ManualConnectionBindingExample {
private DataSource dataSource;
private MyTransactionalService transactionalService;
public void doWork() {
// 确保事务同步是激活的,这通常在 Web 环境或 Spring 应用上下文中默认是开启的
// TransactionSynchronizationManager.initSynchronization(); // 在某些环境下可能需要手动调用
Connection manualCon = null;
boolean mustRelease = false;
try {
// 1. 手动获取连接,这会隐式绑定到当前线程 (如果同步激活)
manualCon = DataSourceUtils.getConnection(dataSource);
// DataSourceUtils.getConnection 会查找或创建 ConnectionHolder 并绑定
// 假设此时绑定的 ConnectionHolder 的 isSynchronizedWithTransaction 是 false
System.out.println("Manually obtained connection: " + manualCon);
// 模拟一些非事务的操作...
// manualCon.prepareStatement(...);
// 2. 调用一个 @Transactional 方法
transactionalService.doSomethingTransactional();
} catch (Exception e) {
// Handle exception
} finally {
// 3. 手动释放连接
// 注意:如果 transactionalService.doSomethingTransactional() 成功启动并提交/回滚事务,
// 它内部的事务管理器可能已经处理了连接释放。
// DataSourceUtils.releaseConnection 需要正确处理这种情况,避免重复释放。
// 通常,更推荐的做法是让 Spring 完全管理连接生命周期。
// DataSourceUtils.releaseConnection(manualCon, dataSource); // 这里需要小心管理释放逻辑
// 如果手动初始化了同步,记得清理
// if (TransactionSynchronizationManager.isSynchronizationActive()) {
// TransactionSynchronizationManager.clearSynchronization();
// }
}
}
}
// @Service
// public class MyTransactionalService {
// @Transactional // 默认 PROPAGATION_REQUIRED
// public void doSomethingTransactional() {
// // 这个方法执行时,会尝试启动事务
// // 它会发现线程上已有一个 connection holder (来自 manualCon)
// // 且 isSynchronizedWithTransaction = false
// // 于是 DataSourceTransactionManager.doBegin 会复用 manualCon
// // 并设置 autoCommit=false 等
// System.out.println("Inside transactional method...");
// // ... 执行数据库操作 ...
// }
// }
安全与实践建议
- 避免手动管理连接与 Spring 事务混用: 上述场景二虽然解释了代码行为,但通常不推荐。手动获取连接并绑定,然后又依赖 Spring 事务管理同一个连接,容易导致连接状态混乱、难以追踪,还可能引起资源泄露(如果手动释放逻辑与 Spring 事务管理冲突)。
- 依赖 Spring 管理: 尽可能让 Spring 通过
@Transactional
或TransactionTemplate
来完整地管理事务和连接的生命周期。DataSourceUtils
主要用于在 Spring 管理的事务 内部 安全地获取当前事务连接,或者在明确知道自己在做什么的高级场景下使用。 - 理解
DataSourceUtils
的行为:DataSourceUtils.getConnection()
设计得足够智能,它会优先返回当前线程绑定的事务连接。只有在没有活动事务时,它才可能获取新连接并绑定(如果同步激活)。DataSourceUtils.releaseConnection()
也会检查连接是否参与了事务,以决定是真正关闭/归还连接池,还是仅仅减少引用计数。
四、总结要点
DataSourceTransactionManager.doBegin()
不创建新连接而复用现有连接的情况,主要发生在以下条件得到满足时:当前线程已经绑定了一个 ConnectionHolder
,但这个 Holder
还没有被标记为 isSynchronizedWithTransaction
。
- 主要复用场景(概念上): 是事务传播(如
REQUIRED
),但这通常发生在getTransaction
层面决定加入现有事务,可能根本不调用内部的doBegin
。 doBegin
内部直接复用的场景: 更可能是因为有非 Spring 事务的代码(如手动调用DataSourceUtils.getConnection()
)提前获取并绑定了一个连接到线程,随后同一个线程启动了一个 Spring 事务。doBegin
检测到这个“未同步”的连接,就将其接管过来,设置为事务模式(比如autoCommit=false
)并使用。
理解 TransactionSynchronizationManager
的线程绑定机制以及 ConnectionHolder
的 isSynchronizedWithTransaction
状态,是弄清楚 Spring 事务连接管理细节的关键。尽管场景二的行为符合代码逻辑,但在实践中应尽量避免,以保持事务管理的清晰和健壮。