PAYATHON 2026

Spring TransactionTemplate 为什么会加入来源不明的事务?

数据小许

仅凭 pg_stat_activity 中存在一个持续 4 小时的事务,以及终止会话后异常出现在 Task-Worker-* 线程上,不能认定 TransactionTemplate 加入了来源不明的事务,也不能认定 HikariCP 借出了事务未结束的连接。

从现有代码看,应先检查 commandBus.execute() 是否长时间未返回。任务领取和整批命令执行都在同一个事务中,任何一步阻塞都可能使事务持续数小时。

TransactionTemplate 什么时候会加入已有事务?

如果没有另行配置,TransactionTemplate 使用 PROPAGATION_REQUIRED:当前存在事务时参与该事务,不存在时创建事务。调用 executeWithoutResult() 并不意味着每次都强制开启独立事务。

对于常见的 Spring 本地 JDBC/JPA 事务,事务资源通常绑定在当前线程上,由对应的事务管理器识别。普通 ThreadPoolExecutor.submit() 不会自动把提交线程的 Spring 事务传递给工作线程。

因此,即使调用 runWorker() 的线程处于事务中,也不能据此认为工作线程会参与该事务。不过,独立线程池中仍可能出现以下情况:

  • taskExecutor 实际上是 Spring 代理,executeNextTask() 在进入方法体之前已经由 @Transactional 或其他事务拦截器开启事务。
  • 未展示的调用代码在工作线程中先开启了事务,再调用当前方法。
  • 自定义代码传播或绑定了事务资源,或者手动管理事务时没有正确清理。
  • 线程池复用了同一个工作线程,而此前执行的非标准事务代码遗留了线程状态。

正常通过 Spring 事务管理器完成的事务,结束时会清理相应资源;线程复用本身不会导致事务跨任务保留。现有材料没有展示事务配置、代理情况或手动资源操作,无法确定上述情况是否存在。

现有代码为什么可能产生长事务?

事务范围覆盖了以下整个过程:

transactionTemplate.executeWithoutResult(transactionStatus -> {
    List<DelayedTask> tasks =
        taskRepository.deleteWithSelectNextTasksToExecute(
            amountOfTasksToSelect, Instant.now());

    // 省略无任务时的处理

    for (DelayedTask task : tasks) {
        commandBus.execute(task.getCommand());
    }
});

只要回调没有结束,事务通常就尚未进入最终提交或回滚阶段。例如:

  • 领取任务的 SQL 正在等待数据库锁。
  • 某个命令中的 SQL 执行缓慢或被阻塞。
  • 某个命令等待 HTTP、RPC、消息响应、Java 锁或其他线程。
  • 一批任务数量较多,逐个执行累计耗时很长。

如果事务中的 SQL 已经执行完,而 Java 代码仍在等待外部操作,PostgreSQL 会话可能显示为 idle in transaction。这表示数据库事务仍然打开,不能直接说明连接已经归还连接池。

另外,workerReportCollector 在这里记录的是下次允许工作的时间,所示代码没有调用 sleep()。仅凭这个退避逻辑,无法解释一个持续 4 小时的数据库事务。

先确认模板是否真的参与了已有事务

可以在模板调用前和回调内部增加日志。以下代码可放入现有的 executeNextTask(),并保留原来的业务逻辑:

import org.springframework.transaction.support.TransactionSynchronizationManager;

log.info("Before template: thread={}, active={}, txName={}",
    Thread.currentThread().getName(),
    TransactionSynchronizationManager.isActualTransactionActive(),
    TransactionSynchronizationManager.getCurrentTransactionName());

transactionTemplate.executeWithoutResult(status -> {
    log.info("Inside template: thread={}, newTransaction={}, active={}, txName={}",
        Thread.currentThread().getName(),
        status.isNewTransaction(),
        TransactionSynchronizationManager.isActualTransactionActive(),
        TransactionSynchronizationManager.getCurrentTransactionName());

    // 原有任务领取和命令执行逻辑
});

对于常规的单事务管理器场景,可以这样理解结果:

  • 调用前 active=false,回调内 newTransaction=true:支持模板新建事务的判断。
  • 调用前 active=true,回调内 newTransaction=false:应追查进入模板之前的事务边界。

这些是 Spring 层面的信号,不能单独证明数据库连接的底层状态一定正常。多个事务管理器、自定义事务管理器或不同数据源并存时,还需核对模板实际使用的事务管理器,以及 Repository 使用的 DataSource 或 EntityManagerFactory。

同时检查 TransactionTemplate Bean 的创建位置和 propagationBehavior 配置。可以临时开启实际事务管理器对应包或类的 DEBUG 日志,观察创建事务、参与事务、提交和回滚的记录。

将 PostgreSQL 会话与具体任务关联起来

查询长事务时,除了事务时长,还应记录事务开始时间、当前状态、等待事件和阻塞者:

SELECT
    pid,
    application_name,
    state,
    xact_start,
    query_start,
    state_change,
    clock_timestamp() - xact_start AS transaction_age,
    wait_event_type,
    wait_event,
    pg_blocking_pids(pid) AS blocking_pids,
    query
FROM pg_stat_activity
WHERE datname = current_database()
  AND xact_start IS NOT NULL
ORDER BY xact_start;

查看其他会话的完整信息需要相应权限。重点关注:

  • xact_start 表示当前事务的开始时间;连接建立时间或连接存活时间不等于事务时长。
  • active 配合锁等待事件和 blocking_pids,可以帮助定位数据库阻塞。
  • idle in transaction 表示当前没有执行查询,但事务尚未结束。
  • 非 active 状态下,query 通常是最近执行的语句,不能据此断定该语句一直在运行。

为了建立可靠关联,可以在任务事务内,使用与 Repository 相同的事务连接执行:

SELECT pg_backend_pid();

将返回的 PID 与线程名称、任务标识、任务批次开始时间一起写入日志。如果另开独立连接查询 PID,得到的可能是另一个会话,无法用于关联当前任务事务。

发现长事务时,再采集对应工作线程的线程栈。如果数据库处于 idle in transaction,线程却正在等待 HTTP 响应或 Java 锁,就可以沿这个方向排查。

原问题中的函数名应为 pg_terminate_backend。终止会话后某个工作线程收到异常,说明该线程当时使用了被终止的连接;它不能证明该事务最初由其他线程开启。

HikariCP 会保留未结束事务的连接吗?

HikariCP 会复用物理连接,因此不同任务可能先后使用相同的 PostgreSQL backend PID。复用会话并不代表复用同一个事务。

正常通过 HikariCP 代理连接执行 JDBC 操作,并调用连接的 close() 归还连接时,连接池会进行状态清理;对于其检测到的未提交事务,会执行相应回滚处理。但当前任务一直持有连接、尚未退出事务回调时,这个连接可能根本没有归还。

如果确实怀疑连接状态泄漏,应检查:

  • 是否手动调用了 setAutoCommit(false),并遗漏 commit()、rollback() 或 close()。
  • 是否通过 unwrap() 操作底层连接,绕开了连接池对部分状态的跟踪。
  • 是否通过原生 SQL 操作事务边界,或混用了 Spring 事务管理和手动连接管理。
  • 是否存在多个数据源或连接获取路径,导致实际连接不受预期的事务管理器管理。

这些判断需要调用链、连接日志和复现结果支持,不能由一个长事务直接推导出来。

应该怎样调整事务边界?

如果业务要求模板始终创建独立事务,可以为这类任务配置专用模板:

TransactionTemplate taskTransactionTemplate =
    new TransactionTemplate(transactionManager);

taskTransactionTemplate.setPropagationBehavior(
    TransactionDefinition.PROPAGATION_REQUIRES_NEW);

其中 TransactionDefinition 来自 org.springframework.transaction。应在 Bean 初始化阶段完成配置,避免并发执行时修改共享模板。

REQUIRES_NEW 只能改变事务传播方式,不能解决回调内部的长时间阻塞。如果工作线程上已有事务,挂起外层事务再开启新事务还可能增加连接需求。

如果耗时集中在命令执行阶段,可以考虑缩短领取任务的事务,再单独执行命令。但原代码把删除任务与命令执行放在同一事务内,可能是为了让失败时任务记录随事务回滚恢复。拆分后必须补齐任务状态、失败重试和幂等处理,否则可能出现任务已删除而命令未完成的情况。

综合模板内外的事务日志、同一事务连接的 PostgreSQL PID 和长事务发生时的工作线程栈,才能判断事务来自外层调用、当前任务执行过久,还是连接管理异常。

备注:内容仅供参考。