Spring 事务:代理入口、资源绑定与失败后的事实
业务方法抛异常,表里却有记录;方法正常返回,表里却没有记录。两者都可能符合事务契约。定位时必须先问谁进入了代理、谁拥有连接、异常被哪层看见,以及物理提交有没有完成。固定基线 Framework v6.1.12 / Java 17 API,不是生产升级建议。本页限定命令式代理事务,以 DataSourceTransactionManager 管理一个 JDBC DataSource 为主;ReactiveTransactionManager、JTA、JPA 的资源协议需要另行分析。MyBatis 已拆到独立训练页,其中会解释 SqlSession.commit 与物理 Connection.commit 的区别。
先给事务边界画四条线
一次下单至少存在代理边界、线程边界、资源边界与业务边界。调用从代理进入,事务拦截器读取属性并找管理器;管理器把连接关联到当前线程;同一数据源的 Spring JDBC/MyBatis 访问复用这个连接;数据库只对这个物理事务内的操作提供提交回滚。远程 HTTP、Redis、消息发送及另一个独立数据源不会自动加入。
Transactional 是元数据,不是 Java 方法的内建语义。自调用、容器外 new、从 raw target 调用、选错事务管理器,都可能改变边界。方法可见性还受代理方式和属性源配置约束:JDK 要通过接口可调用方法;类代理在本基线下可支持符合条件的 protected/包可见事务方法,但 private/final 等仍受代理限制。不能用“只要 public 就一定生效”或“非 public 全部不支持”代替检查。
线程绑定还不等于所有 DataSource.getConnection 都复用事务。Spring 集成路径通过 DataSourceUtils 等获取资源;直接向未代理的底层数据源取连接,可能得到另一个连接。判断是否同一事务,应核对事务管理器选用的数据源、资源 key、实际连接和调用线程,不能仅比较方法上的注解。
TransactionInterceptor 怎样进入和退出
调用链是 TransactionInterceptor.invoke → TransactionAspectSupport.invokeWithinTransaction → 事务属性源 → determineTransactionManager → createTransactionIfNecessary → PlatformTransactionManager.getTransaction。进入业务前绑定 TransactionInfo;它保存当前拦截状态并能恢复外层信息,不等同于保存 JDBC 连接的资源 Map。
原码:TransactionAspectSupport.java,v6.1.12,连续 368–415 行,仅去除共同缩进。固定来源。
PlatformTransactionManager ptm = asPlatformTransactionManager(tm);
final String joinpointIdentification = methodIdentification(method, targetClass, txAttr);
if (txAttr == null || !(ptm instanceof CallbackPreferringPlatformTransactionManager cpptm)) {
// Standard transaction demarcation with getTransaction and commit/rollback calls.
TransactionInfo txInfo = createTransactionIfNecessary(ptm, txAttr, joinpointIdentification);
Object retVal;
try {
// This is an around advice: Invoke the next interceptor in the chain.
// This will normally result in a target object being invoked.
retVal = invocation.proceedWithInvocation();
}
catch (Throwable ex) {
// target invocation exception
completeTransactionAfterThrowing(txInfo, ex);
throw ex;
}
finally {
cleanupTransactionInfo(txInfo);
}
if (retVal != null && txAttr != null) {
TransactionStatus status = txInfo.getTransactionStatus();
if (status != null) {
if (retVal instanceof Future<?> future && future.isDone()) {
try {
future.get();
}
catch (ExecutionException ex) {
if (txAttr.rollbackOn(ex.getCause())) {
status.setRollbackOnly();
}
}
catch (InterruptedException ex) {
Thread.currentThread().interrupt();
}
}
else if (vavrPresent && VavrDelegate.isVavrTry(retVal)) {
// Set rollback-only in case of Vavr failure matching our rollback rules...
retVal = VavrDelegate.evaluateTryFailure(retVal, txAttr, status);
}
}
}
commitTransactionAfterReturning(txInfo);
return retVal;
}这段从取得命令式管理器到正常返回,排除 reactive 与 CallbackPreferring 的独立路径。创建事务位于 invocation 的 try 之前;目标抛错先执行 completeTransactionAfterThrowing,再重抛。finally 只恢复 TransactionInfo 的线程局部栈,不是释放 JDBC holder。正常路径在 finally 后才检查返回值和提交;删掉这些阶段会把“业务返回”错当成“事务完成”。创建或提交失败也不能说都由这个 invocation catch 处理。
异常规则、Future 与线程边界
正常返回也要调用 manager.commit,它可以因 rollback-only 而实际回滚。异常路径也不一定回滚:默认规则是 RuntimeException/Error 回滚,checked exception 通常提交,可用 rollbackFor/noRollbackFor 调整。匹配优先看具体规则的继承深度,类名模式规则容易过宽,应优先精确异常类型。RuleBasedTransactionAttribute 是实际判断入口。
6.1 的普通路径还检查返回时已经完成的 Future:若此刻已异常完成且符合规则,会标记回滚;这不表示稍后在其他线程发生的失败能逆转已经提交的事务。同步抛异常、返回一个已失败 Future、返回后才失败,是三个不同时间线。void 异步异常更不能由已返回的调用方事务捕获。
DefaultTransactionAttribute.rollbackOn给出默认规则;显式 rollbackFor 要先由规则匹配决定,未匹配才退回默认。异常在目标内部被吞掉时,这个拦截器可能只看到正常返回;资源自己标记失败则是另一条路径。
不要靠 sleep 制造 Future 竞争:实验先返回已经失败的 CompletableFuture,另一次返回未完成 Future,并在代理方法返回后才 completeExceptionally。前者由 isDone/get/rollbackOn 分支标记回滚;后者提交时尚未失败,失败不会倒流。Cancelled Future、Vavr 和异步注解组合未测,不从这两个输入外推。
新线程也不会继承命令式事务的资源 Map。实验让工作线程自动提交写 2,主线程写 1 后抛错,最后只剩 2。若工作线程重新调用事务代理,可获得自己的事务,却仍不成为调用方事务的一部分。不要把 JDBC Connection 或 holder 复制给另一线程来“传播事务”。
getTransaction:逻辑事务与物理事务分开
AbstractPlatformTransactionManager.getTransaction 先 doGetTransaction,再判断是否已有活动事务。无现有事务时 REQUIRED、REQUIRES_NEW、NESTED 通常开始新物理事务;MANDATORY 拒绝;SUPPORTS、NOT_SUPPORTED、NEVER 可以非事务执行,其中可能启用同步作用域,但不能据 isSynchronizationActive 就断言物理事务存在。
已有事务时的传播行为如下,条件是调用确实经过事务拦截:
| 传播 | 当前已有 JDBC 事务时 | 资源和失败边界 |
|---|---|---|
| REQUIRED | 参与同一物理事务 | 参与失败通常标记共享 rollback-only |
| REQUIRES_NEW | 暂停外层,再开独立物理事务 | 外层连接仍占用,内层另借连接 |
| NESTED | 允许且支持时建保存点 | 通常同连接,局部回滚,不能独立持久提交 |
| SUPPORTS | 参与现有事务 | 无事务时不强制开启 |
| NOT_SUPPORTED | 暂停事务,非事务执行 | 暂停不代表释放外层连接 |
| MANDATORY | 要求已有事务 | 无事务时报错 |
| NEVER | 拒绝现有事务 | 有事务时报错 |
分支见 AbstractPlatformTransactionManager.getTransaction/handleExistingTransaction。没有外层事务时的七种行为分支、MANDATORY/NEVER 的拒绝形态与 validateExistingTransaction 校验,见同步与传播矩阵页。REQUIRED 内层声明不同 isolation 不会自动把外层事务升级;可启用 validateExistingTransaction 检查部分不兼容请求。隔离还依赖数据库实际实现、快照和锁,注解名称不是跨数据库完全相同的异常保证。
NESTED 的保存点能力依赖事务管理器和驱动。DataSourceTransactionManager 的常见路径支持 JDBC 保存点,不能泛化给所有管理器。内层正常返回只是释放保存点;外层随后回滚,内层操作同样消失。内层保存点回滚也不自动恢复 Java 对象字段或已经发出的远程请求。
JDBC 连接怎样开始、暂停与恢复
DataSourceTransactionManager.doBegin 获取连接,按需要准备隔离和只读标志,若 autoCommit=true 则切成 false,记录需要恢复的状态,设置事务活动与超时,将 ConnectionHolder 绑定到 TransactionSynchronizationManager。资源 key 关联 DataSource,不是一个抽象“当前全局连接”。
DataSourceUtils.doGetConnection 先查线程上的 holder;已有连接就复用,并通过引用计数配合释放。事务中业务代码释放连接通常只是归还使用权,不能提前关闭由管理器持有的物理事务连接。事务结束后管理器才解绑、恢复自动提交及隔离/只读状态、releaseConnection;对连接池包装连接,close 通常表示归还池,而不是每次断开数据库 socket。
REQUIRES_NEW 的 suspend 会解绑外层 holder,并暂停同步器;外层连接仍被外层事务拥有,锁也未释放。新事务绑定内层连接,完成清理后恢复外层 holder 和同步状态。如果内层开启失败,管理器仍尝试恢复外层上下文,再把失败交给调用链。相关入口见 DataSourceTransactionManager 与 DataSourceUtils。
超时不是后台定时器到点就杀线程。管理器记录剩余预算,JDBC 工具和框架可把剩余时间用于 statement timeout;连接池借用、网络读取、锁等待和应用计算还可能有各自的超时。readOnly 默认主要是提示,特定管理器可选择强制执行数据库只读语句,不能把默认 readOnly 当作禁止写入的权限控制。
原码:DataSourceUtils.java,v6.1.12,连续 103–147 行,仅去除共同缩进。固定来源。
public static Connection doGetConnection(DataSource dataSource) throws SQLException {
Assert.notNull(dataSource, "No DataSource specified");
ConnectionHolder conHolder = (ConnectionHolder) TransactionSynchronizationManager.getResource(dataSource);
if (conHolder != null && (conHolder.hasConnection() || conHolder.isSynchronizedWithTransaction())) {
conHolder.requested();
if (!conHolder.hasConnection()) {
logger.debug("Fetching resumed JDBC Connection from DataSource");
conHolder.setConnection(fetchConnection(dataSource));
}
return conHolder.getConnection();
}
// Else we either got no holder or an empty thread-bound holder here.
logger.debug("Fetching JDBC Connection from DataSource");
Connection con = fetchConnection(dataSource);
if (TransactionSynchronizationManager.isSynchronizationActive()) {
try {
// Use same Connection for further JDBC actions within the transaction.
// Thread-bound object will get removed by synchronization at transaction completion.
ConnectionHolder holderToUse = conHolder;
if (holderToUse == null) {
holderToUse = new ConnectionHolder(con);
}
else {
holderToUse.setConnection(con);
}
holderToUse.requested();
TransactionSynchronizationManager.registerSynchronization(
new ConnectionSynchronization(holderToUse, dataSource));
holderToUse.setSynchronizedWithTransaction(true);
if (holderToUse != conHolder) {
TransactionSynchronizationManager.bindResource(dataSource, holderToUse);
}
}
catch (RuntimeException ex) {
// Unexpected exception from external delegation call -> close Connection and rethrow.
releaseConnection(con, dataSource);
throw ex;
}
}
return con;
}此处按有效 DataSource key 找 holder,已有连接优先返回;无 holder 才 fetch,且只有同步激活才登记同步与按条件绑定。key 会经有限代理解包,遵从 Map key 语义,不是按 URL 配对,也不是对所有 getConnection 的全局拦截。TransactionAwareDataSourceProxy 是显式适配的例外。isSynchronizationActive 与 isActualTransactionActive 分离,不能混作物理事务证据。
原码:DataSourceTransactionManager.java,v6.1.12,连续 262–315 行,仅去除共同缩进。固定来源。
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());
}
}
catch (Throwable ex) {
if (txObject.isNewConnectionHolder()) {
DataSourceUtils.releaseConnection(con, obtainDataSource());
txObject.setConnectionHolder(null, false);
}
throw new CannotCreateTransactionException("Could not open JDBC Connection for transaction", ex);
}
}只有需要新资源时才获取新连接句柄,autoCommit 原为 true 才切换并记录恢复标记;仅新 holder 绑定。初始化失败仅对新 holder 释放并清引用,再抛 CannotCreateTransactionException,不承诺任意外部副作用已回滚。每个逻辑事务方法并不都重新 doBegin。
原码:DataSourceTransactionManager.java,v6.1.12,连续 369–399 行,仅去除共同缩进。固定来源。
@Override
protected void doCleanupAfterCompletion(Object transaction) {
DataSourceTransactionObject txObject = (DataSourceTransactionObject) transaction;
// Remove the connection holder from the thread, if exposed.
if (txObject.isNewConnectionHolder()) {
TransactionSynchronizationManager.unbindResource(obtainDataSource());
}
// Reset connection.
Connection con = txObject.getConnectionHolder().getConnection();
try {
if (txObject.isMustRestoreAutoCommit()) {
con.setAutoCommit(true);
}
DataSourceUtils.resetConnectionAfterTransaction(
con, txObject.getPreviousIsolationLevel(), txObject.isReadOnly());
}
catch (Throwable ex) {
logger.debug("Could not reset JDBC Connection after transaction", ex);
}
if (txObject.isNewConnectionHolder()) {
if (logger.isDebugEnabled()) {
logger.debug("Releasing JDBC Connection [" + con + "] after transaction");
}
DataSourceUtils.releaseConnection(con, this.dataSource);
}
txObject.getConnectionHolder().clear();
}completion 清理与切面 finally 分开:新 holder 才解绑和释放,必须恢复自动提交才设回 true;恢复隔离/只读失败只记日志,不能证明属性已恢复。release 对池句柄通常是归还使用权,也有 SmartDataSource 等例外,不能把它一概说成物理断连。持有外层事务的暂停资源由管理器另行 resume。
commit 的完整分支:返回成功未必提交成功
公开 commit 先检查状态是否已完成、local rollback-only、global rollback-only,再决定回滚或进入 processCommit。普通新事务的核心顺序是 prepareForCommit → beforeCommit → beforeCompletion → doCommit → afterCommit → afterCompletion(COMMITTED) → cleanupAfterCompletion。保存点分支释放保存点,参与者分支不执行独立 JDBC commit。
beforeCommit 可 flush ORM/批量语句,抛异常仍可能使事务回滚;beforeCompletion 用于完成前清理,不应被当作成功通知。doCommit 才进入 DataSourceTransactionManager 的 Connection.commit。afterCommit 在物理提交成功之后;若它抛异常,异常可传播给调用方,但数据库仍已提交。finally 会调用 afterCompletion,最后清理线程与资源、恢复外层。
提交前失败 -> 根据类型/状态执行回滚与完成回调 -> 清理
物理提交成功 -> afterCommit 失败 -> 调用方见异常,数据库仍已提交
提交调用失败 -> 结果可能未知 -> afterCompletion 可报告 UNKNOWN -> 清理实现中的 TransactionException、RuntimeException/Error、UnexpectedRollbackException 分别处理;rollbackOnCommitFailure 的默认选择也影响是否尝试回滚。不能说“只要 commit 抛异常就保证数据库回滚”,连接断开时数据库可能已完成提交而响应丢失。应使用业务唯一键、状态查询与对账判断结果,盲目重试非幂等扣款会重复执行。
同步回调不是跨系统原子协议。afterCommit 发消息如果网络失败,数据库不会撤销;数据库提交后进程崩溃也可能根本没机会发送。需要可靠事件时,应在同一本地事务写 Outbox,由投递器重试,消费者幂等。afterCommit 若要写入必须持久提交的新数据,应开启明确的新事务,不能依赖尚未清理的旧连接自动再提交。回调时资源仍可能可访问,不代表原事务还能继续提供新的提交边界。
原码:commit入口可能转向rollback
原码连续摘录(仅统一公共缩进,截取方法局部):AbstractPlatformTransactionManager.java,v6.1.12,L742–L759。
if (defStatus.isLocalRollbackOnly()) {
if (defStatus.isDebug()) {
logger.debug("Transactional code has requested rollback");
}
processRollback(defStatus, false);
return;
}
if (!shouldCommitOnGlobalRollbackOnly() && defStatus.isGlobalRollbackOnly()) {
if (defStatus.isDebug()) {
logger.debug("Global transaction is marked as rollback-only but transactional code requested commit");
}
processRollback(defStatus, true);
return;
}
processCommit(defStatus);
}局部rollback-only先检查并processRollback(false)返回;全局标记且管理器不选择带标记提交时,processRollback(true)返回,正常分支才processCommit。catch异常不清除这里检查的状态。两处布尔参数还区分是否按unexpected处理,不能把主动setRollbackOnly与参与事务失败完全等同。
原码:afterCommit异常后仍安排完成通知
原码连续摘录(仅统一公共缩进,截取方法局部):AbstractPlatformTransactionManager.java,v6.1.12,L836–L843。
triggerAfterCommit(status);
}
finally {
triggerAfterCompletion(status, TransactionSynchronization.STATUS_COMMITTED);
if (commitListenerInvoked) {
this.transactionExecutionListeners.forEach(listener -> listener.afterCommit(status, null));
}
}这段位于物理提交成功之后;triggerAfterCommit包在try中,finally仍以STATUS_COMMITTED调用完成通知,并通知事务执行监听器。回调异常可影响调用方观察,却不会将已经完成的数据库commit逆转。提交后的可靠副作用应有持久意图与恢复任务,不能只靠一个回调。
rollback-only 与“catch 后仍回滚”
rollback 的 processRollback 先执行 beforeCompletion;有保存点就回滚到保存点,新物理事务则 doRollback,普通参与者在默认参与失败策略下标记已有事务 rollback-only。随后完成回调、清理,必要时抛 UnexpectedRollbackException。回滚本身失败也可能覆盖原业务异常,应记录应用异常与事务异常链。
输入:Outer.REQUIRED 插入 A,调用代理 Inner.REQUIRED 插入 B 后抛 RuntimeException;Outer catch 后插入 C 并正常返回。Inner 参与同一物理事务,退出时已经把共享事务标记回滚;catch 只改变 Java 控制流,不能取消 holder 的标记。最外层 commit 检测到标记,回滚 A/B/C 并报告 UnexpectedRollbackException,防止调用方误信已提交。
反例:如果异常在同一个方法内部被捕获,未离开任何事务拦截边界,且资源本身未失败、代码未 setRollbackOnly,则事务拦截器可能只看到正常返回,最终提交。另一个区别是当前新事务主动 setRollbackOnly 后返回,可按本地回滚标记正常结束而不必与参与者引起的“意外回滚”完全同异常。回答必须给清楚谁标记、哪个逻辑范围、谁负责最终提交。
把 Inner 改 REQUIRES_NEW:B 回滚,外层可继续提交 A/C;把 Inner 改 NESTED:回滚保存点内 B,外层可提交 A/C,但若外层再失败,A/C 仍全部回滚。两者结果表面相似,连接、锁、持久提交时点却不同。
实验:可验证结果与连接池耗尽
从仓库根运行 JAVA_HOME=$(/usr/libexec/java_home -v17) bash labs/spring-senior/run.sh tx,环境见实验说明。每模式独立 JVM/Context/内存库。H2 2.3.232 只验证此配置的框架边界,不证明 MySQL 隔离、死锁或网络恢复。
| 输入 | 控制条件及应观察的最终状态 |
|---|---|
| external / self | 相同 external 注解方法,代理外调活动事务且写 1 回滚;无注解 self 经 this 调用,事务不活动、写 1 自动提交 |
| checked / checked-rollback | 实际抛同一 checked exception;默认写 1 留存,rollbackFor=Exception.class 写 1 回滚;两次都活动事务 |
| future-failed / future-late | 前者返回已失败 Future,表空;后者在代理返回后失败,写 1 已提交 |
| thread | worker 无主线程事务,写 2 自动提交;主线程异常回滚写 1,最后仅 2 |
| required | 外层 1、内层 2 失败、catch 后 3;最终表空并观察 UnexpectedRollbackException |
| requires-new / nested | 内层独立事务或保存点回滚 2,外层提交 1、3;同结果不等于同资源协议 |
| after-commit | 回调真实抛错,调用方见异常而写 1 已持久提交 |
| pool | 池大小 1、有限 300ms 借用超时;外层占连接,内层 REQUIRES_NEW 无连接,外层 catch 后提交 1、3 |
本批12模式已实际运行通过,Liberica JDK17.0.19+11-LTS;预期表与实跑记录分别核查。注解主程序用真实 EnableTransactionManagement、事务管理器、AopUtils 与目标方法内的事务状态观测,退出代理后再读取提交结果。self/checked 旧占位入口已委托此程序,不再用正常提交替代异常实验。U18 的同步器、事件、编程式与保存点实验见同步与传播矩阵页,JDBC 层异常转换、批处理与生成键的 H2/MySQL 对照见本票 JDBC 页;本页模式作回归复跑不重复断言。
package study.spring;
import java.util.*;
import java.util.concurrent.*;
import org.h2.jdbcx.JdbcDataSource;
import org.springframework.context.annotation.*;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.annotation.*;
import org.springframework.transaction.support.TransactionSynchronizationManager;
import org.springframework.aop.support.AopUtils;
/** Public teaching command: real annotation infrastructure, H2 and observable rows. */
public class AnnotatedTxLab {
@Configuration(proxyBeanMethods=false)
@EnableTransactionManagement(proxyTargetClass=true)
static class Config {
@Bean JdbcDataSource dataSource() {
var ds = new JdbcDataSource(); ds.setURL("jdbc:h2:mem:annotated;DB_CLOSE_DELAY=-1"); return ds;
}
@Bean JdbcTemplate jdbc(JdbcDataSource ds) { return new JdbcTemplate(ds); }
@Bean DataSourceTransactionManager transactionManager(JdbcDataSource ds) { return new DataSourceTransactionManager(ds); }
@Bean Service service(JdbcTemplate jdbc) { return new Service(jdbc); }
}
public static class Service {
final JdbcTemplate jdbc;
boolean active;
Service(JdbcTemplate jdbc) { this.jdbc = jdbc; }
public boolean wasActive() { return active; }
public void self() { external(); }
@Transactional
public void external() {
active = TransactionSynchronizationManager.isActualTransactionActive();
jdbc.update("insert into item values(1)");
throw new IllegalStateException("expected business failure");
}
@Transactional
public void checkedDefault() throws Exception { write(); throw new Exception("expected checked failure"); }
@Transactional(rollbackFor=Exception.class)
public void checkedRollback() throws Exception { write(); throw new Exception("expected checked failure"); }
void write() {
active = TransactionSynchronizationManager.isActualTransactionActive();
jdbc.update("insert into item values(1)");
}
@Transactional
public CompletableFuture<String> future(boolean alreadyFailed) {
write();
return alreadyFailed ? CompletableFuture.failedFuture(new IllegalStateException("future failure"))
: new CompletableFuture<>();
}
@Transactional
public void thread() throws Exception {
write();
var executor = Executors.newSingleThreadExecutor();
try {
boolean workerActive = executor.submit(() -> {
boolean worker = TransactionSynchronizationManager.isActualTransactionActive();
jdbc.update("insert into item values(2)"); return worker;
}).get(3, TimeUnit.SECONDS);
if (workerActive) throw new AssertionError("transaction leaked to worker");
} finally { executor.shutdownNow(); }
throw new IllegalStateException("expected business failure");
}
}
public static void main(String[] args) {
String mode = args.length == 0 ? "external" : args[0];
if (!Set.of("external", "self", "checked", "checked-rollback", "future-failed", "future-late", "thread").contains(mode)) throw new IllegalArgumentException(mode);
try (var c = new AnnotationConfigApplicationContext(Config.class)) {
var jdbc = c.getBean(JdbcTemplate.class);
jdbc.execute("drop table if exists item"); jdbc.execute("create table item(id int primary key)");
var service = c.getBean(Service.class);
try {
if (mode.equals("self")) service.self();
else if (mode.equals("checked")) service.checkedDefault();
else if (mode.equals("checked-rollback")) service.checkedRollback();
else if (mode.equals("thread")) service.thread();
else if (mode.startsWith("future-")) {
var future = service.future(mode.equals("future-failed"));
if (mode.equals("future-late")) future.completeExceptionally(new IllegalStateException("future failure"));
if (!future.isCompletedExceptionally()) throw new AssertionError("future must fail");
}
else service.external();
if (!mode.startsWith("future-")) throw new AssertionError("missing business failure");
} catch (Exception expected) {
if (!Set.of("expected business failure", "expected checked failure").contains(expected.getMessage())) throw expected;
}
boolean active = service.wasActive();
var rows = jdbc.queryForList("select id from item order by id", Integer.class);
var expectedRows = mode.equals("thread") ? List.of(2)
: Set.of("external", "checked-rollback", "future-failed").contains(mode) ? List.<Integer>of() : List.of(1);
if (!AopUtils.isAopProxy(service) || active == mode.equals("self") || !rows.equals(expectedRows)
|| TransactionSynchronizationManager.isActualTransactionActive())
throw new AssertionError(mode + " active=" + active + " rows=" + rows);
System.out.println("PASS annotated " + mode + " active=" + active + " rows=" + rows);
} catch (Exception e) { throw new RuntimeException(e); }
}
}保留 TransactionTemplate 的传播、回调和池耗尽实验;它直接验证管理器,不伪称注解代理实验。
package study.spring;
import java.util.*;
import com.zaxxer.hikari.*;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.*;
import org.springframework.transaction.support.*;
public class TxLab {
public static void main(String[] args){
String mode=args.length==0?"required":args[0];
if(Set.of("external","self","checked","checked-rollback","future-failed","future-late","thread").contains(mode)){
AnnotatedTxLab.main(args);return;
}
if(!Set.of("required","requires-new","nested","pool","after-commit").contains(mode))throw new IllegalArgumentException(mode);
var cfg=new HikariConfig();cfg.setJdbcUrl("jdbc:h2:mem:tx;DB_CLOSE_DELAY=-1");cfg.setMaximumPoolSize(mode.equals("pool")?1:3);cfg.setConnectionTimeout(300);
try(var ds=new HikariDataSource(cfg)){
var jdbc=new JdbcTemplate(ds);jdbc.execute("create table item(id int primary key)");
var tm=new DataSourceTransactionManager(ds);var outer=new TransactionTemplate(tm);var inner=new TransactionTemplate(tm);
inner.setPropagationBehavior(mode.equals("nested")?TransactionDefinition.PROPAGATION_NESTED:mode.equals("requires-new")||mode.equals("pool")?TransactionDefinition.PROPAGATION_REQUIRES_NEW:TransactionDefinition.PROPAGATION_REQUIRED);
var innerFailure=new java.util.concurrent.atomic.AtomicReference<RuntimeException>();
boolean outerFailed=false;
try{
outer.executeWithoutResult(s->{
jdbc.update("insert into item values(1)");
if(mode.equals("after-commit"))TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization(){public void afterCommit(){throw new IllegalStateException("after commit");}});
else {
try {
inner.executeWithoutResult(i->{jdbc.update("insert into item values(2)");throw new IllegalArgumentException("inner failure");});
throw new AssertionError("expected inner failure: "+mode);
} catch(RuntimeException failure) {
if(mode.equals("pool")) {
Throwable root=failure;
while(root.getCause()!=null)root=root.getCause();
if(!(failure instanceof CannotCreateTransactionException)
||!(root instanceof java.sql.SQLTransientConnectionException))throw failure;
System.out.println("observed inner CannotCreateTransactionException root="+root.getClass().getSimpleName());
} else {
if(failure.getClass()!=IllegalArgumentException.class
||!"inner failure".equals(failure.getMessage()))throw failure;
System.out.println("observed inner IllegalArgumentException: inner failure");
}
innerFailure.set(failure);
}
jdbc.update("insert into item values(3)");
}
});
} catch(RuntimeException failure) {
if(mode.equals("required")) {
if(!(failure instanceof UnexpectedRollbackException))throw failure;
} else if(mode.equals("after-commit")) {
if(failure.getClass()!=IllegalStateException.class||!"after commit".equals(failure.getMessage()))throw failure;
} else throw failure;
outerFailed=true;
System.out.println("observed outer "+failure.getClass().getSimpleName()+": "+failure.getMessage());
}
if(outerFailed!=Set.of("required","after-commit").contains(mode)
||(innerFailure.get()!=null)==mode.equals("after-commit"))
throw new AssertionError("expected exception presence: "+mode);
List<Integer> rows=jdbc.queryForList("select id from item order by id",Integer.class);
List<Integer> expected=mode.equals("required")?List.of():mode.equals("after-commit")?List.of(1):List.of(1,3);
if(!rows.equals(expected))throw new AssertionError(mode+" "+rows);
System.out.println("PASS transaction "+mode+" rows="+rows);
}
}
}容量推演输入 T=8 个线程、P=8 个连接,每个外层已借 1 个,随后都同步进入一层 REQUIRES_NEW。此刻外层占满池,内层全等连接,无人能推进归还;有限超时能打断等待,不提升吞吐。对只争夺连接、每线程最多同时持有 C 个连接的简化模型,P=T×(C−1)+1 是避免池资源死锁的一个下界推导;C=2 时为 9。这个数不是性能推荐值,更不保证没有数据库锁死锁。若内层等待外层持有的数据库锁,加连接也不能解决。
高级答法与架构师变化条件
高级答案应贯通:“事务拦截器按属性获取逻辑事务,管理器决定加入、保存点或暂停重开,JDBC 连接由线程资源绑定复用。正常返回进入 commit 但 rollback-only 可转回滚,异常按规则选择完成路径。同步回调、物理提交与清理有顺序;提交阶段失败可能结果未知,异步与远程效果不自动加入。”
重试也要画嵌套:无外部事务且每次进入 REQUIRED 新事务时,Retry → Transaction → Target 可以在前次完成清理后重开;Transaction → Retry → Target 仍可能复用已失败的事务。若前者还参与更外层 REQUIRED,也不能声称每次获得新物理事务。比较每次事务范围、连接和最终持久结果;这组重试组合是未运行推演,AOP 篇只说明嵌套入口。
三轮追问:先问 catch 为何无效,答共享 rollback-only;再问 REQUIRES_NEW 为何耗尽池,答暂停未释放外层连接;最后问 afterCommit 报错为何不能直接重试整单,答数据库已提交且远程效果可能未知,应查询幂等结果与恢复投递。各 2 分,完整连接时序与最终结果各加 2 分。
架构变化:订单事务里同时调用支付 HTTP,平均事务持有连接 40ms 变为 800ms。若吞吐 200 次/秒,按平均占用估算连接需求从 8 增到 160,还未包含峰值;增加池会扩大数据库并发和故障耦合。候选一继续同步但缩短本地锁窗口,把外部调用和状态机分开;候选二事务写订单与 Outbox,支付异步执行、幂等回调,代价是中间状态和恢复复杂度。若用户必须立即看到支付结果,仍可同步等待状态,但不要在等待期间持有数据库事务。
验证应覆盖连接 pending、事务持续时间、数据库锁、超时预算、重复业务键和提交未知后的查询,不把一次单元实验当作跨网络可靠性证明。多数据源需显式选择管理器,跨库原子性要另立协议;读写路由必须在取连接前确定,事务中切租户上下文不会自动迁移已绑定连接。专家深化可研究 JTA 或响应式取消语义,主线不能混用其保证。
闭卷验收
本页 8 道闭卷题收进了下方自测面板:先回忆,再展开参考答案与评分点,最后给三档自评。