返回

Spring事务连接复用解密:DataSourceTransactionManager详解

java

解密 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 语句的意思是:

  1. 如果 txObject (代表当前事务状态的对象)里 没有 包含 ConnectionHolder(也就是当前线程还没绑定连接),或者
  2. txObject ConnectionHolder,但这个 Holder 已经被标记为 isSynchronizedWithTransaction()(表示这个连接已经和一个活动的 Spring 事务同步了)。

只要满足上面两个条件之一,doBegin 就会从 dataSource 获取一个 连接。

那么反过来,什么时候会跳过这个 if 语句块,不去获取新连接,而是复用 txObject 里已有的那个 ConnectionHolder 中的连接呢? 这就要求 if 的条件 满足,也就是说,必须 同时 满足以下两点:

  1. txObject.hasConnectionHolder()true:当前线程确实已经绑定了一个 ConnectionHolder
  2. txObject.getConnectionHolder().isSynchronizedWithTransaction()false:这个绑定的 ConnectionHolder 没有 被标记为与一个(已启动的)Spring 管理的事务同步。

搞清楚这一点,咱们就能分析出具体是哪些场景导致了连接复用。

一、Spring 事务与线程绑定连接的基础

要理解连接复用的逻辑,得先明白 Spring 是怎么管理事务中的资源的,尤其是数据库连接。

核心机制是 TransactionSynchronizationManager。这家伙负责管理事务同步,并且利用 ThreadLocal 变量来维护 当前线程 的事务状态和资源。

当一个事务启动(通常是通过 @Transactional 注解或者编程式事务),Spring 会:

  1. 获取资源: 对于数据库事务,就是从 DataSource 获取一个 Connection
  2. 封装资源: 把这个 Connection 包装进一个 ConnectionHolder 对象。ConnectionHolder 不仅仅持有连接,还包含一些状态信息,比如引用计数、是否与事务同步等。
  3. 线程绑定: 调用 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 内部判断需要强制获取或验证连接状态,也可能走到这里。

结合起来看,获取新连接的场景覆盖了:

  1. 完全没有上下文,新起一个事务。
  2. 需要一个物理上隔离的新事务(如 REQUIRES_NEW)。
  3. 内部逻辑判断需要刷新或使用全新连接资源。

那么,连接复用的条件: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,获取新连接。

小结一下事务传播场景 :大多数连接复用(如 REQUIRED 加入事务)是在 getTransaction 层面实现的,并不走到 doBeginelse 分支。REQUIRES_NEW 则强制获取新连接。NESTED 试图在同一连接上创建保存点。

场景二:非 Spring 事务代码预绑定连接

这个场景就比较精确地命中了 doBegin 里复用连接的条件 (hasHolder && !isSynchronized)。

设想一下这种情况:

  1. 你的代码(可能在某个拦截器、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) 所管理的。
  2. 紧接着,在 同一个线程 上,你的代码调用了一个标记了 @Transactional (比如默认的 PROPAGATION_REQUIRED) 的方法。
  3. Spring 的事务拦截器介入,调用 AbstractPlatformTransactionManager.getTransaction()
  4. getTransaction() 发现当前没有活动的 Spring 事务 (isExistingTransaction 为 false),于是决定启动新事务,调用 startTransaction
  5. startTransaction() 内部会检查 TransactionSynchronizationManager.getResource(dataSource)。这次,它 找到 了步骤 1 中手动绑定的那个 ConnectionHolder!它将这个找到的 holder 设置到 txObject 中,此时 txObject.hasConnectionHolder()true
  6. 调用 DataSourceTransactionManager.doBegin(txObject, definition)
  7. 进入 doBegin,检查 if 条件:
    • txObject.hasConnectionHolder()true
    • txObject.getConnectionHolder().isSynchronizedWithTransaction()false(因为它是手动绑定,未经过 doBegin 完整的事务同步标记)。
    • 所以,整个 if 条件 (!txObject.hasConnectionHolder() || txObject.getConnectionHolder().isSynchronizedWithTransaction()) 变成了 (false || false),结果是 false
  8. if 块被跳过。后续代码会直接使用 txObject.getConnectionHolder().getConnection(),也就是复用了你在步骤 1 手动获取并绑定的那个连接。
  9. 接下来,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 通过 @TransactionalTransactionTemplate 来完整地管理事务和连接的生命周期。DataSourceUtils 主要用于在 Spring 管理的事务 内部 安全地获取当前事务连接,或者在明确知道自己在做什么的高级场景下使用。
  • 理解 DataSourceUtils 的行为: DataSourceUtils.getConnection() 设计得足够智能,它会优先返回当前线程绑定的事务连接。只有在没有活动事务时,它才可能获取新连接并绑定(如果同步激活)。DataSourceUtils.releaseConnection() 也会检查连接是否参与了事务,以决定是真正关闭/归还连接池,还是仅仅减少引用计数。

四、总结要点

DataSourceTransactionManager.doBegin() 不创建新连接而复用现有连接的情况,主要发生在以下条件得到满足时:当前线程已经绑定了一个 ConnectionHolder,但这个 Holder 还没有被标记为 isSynchronizedWithTransaction

  1. 主要复用场景(概念上): 是事务传播(如 REQUIRED),但这通常发生在 getTransaction 层面决定加入现有事务,可能根本不调用内部的 doBegin
  2. doBegin 内部直接复用的场景: 更可能是因为有非 Spring 事务的代码(如手动调用 DataSourceUtils.getConnection())提前获取并绑定了一个连接到线程,随后同一个线程启动了一个 Spring 事务。doBegin 检测到这个“未同步”的连接,就将其接管过来,设置为事务模式(比如 autoCommit=false)并使用。

理解 TransactionSynchronizationManager 的线程绑定机制以及 ConnectionHolderisSynchronizedWithTransaction 状态,是弄清楚 Spring 事务连接管理细节的关键。尽管场景二的行为符合代码逻辑,但在实践中应尽量避免,以保持事务管理的清晰和健壮。