Spring 事务:同步器、事务事件、编程式与传播矩阵
事务里的回调和事件为什么没执行、编程式回滚为什么和声明式不一样、MANDATORY 为什么报错、REQUIRES_NEW 借走的是哪个连接——这些都在"事务边界已经画对"之后发生。本页承接事务正文(代理入口、资源绑定、rollback-only 与提交分支的主归属在那页),负责同步器、事务事件、编程式事务、传播行为两侧完整矩阵、保存点、挂起恢复、隔离与只读、回滚规则匹配和 ORM 集成边界。固定基线 Framework v6.1.12 / Java 17 API。H2 2.3.232 只验证框架边界;隔离与只读的真实数据库差异由本票 JDBC 页在一次性隔离 MySQL 9.7.1 实例上实测,不把 H2 行为外推到生产库。ReactiveTransactionManager 归响应式轨道,JTA 只作边界说明。
事务同步器:把副作用挂到确定的完成时点
一次下单要在"订单已确认提交"之后发通知、在"确实回滚了"之后清补偿标记——这两个时点都不能靠业务代码自己调用,因为业务代码不知道事务最终提交还是回滚。TransactionSynchronization 是挂在当前线程事务上的回调集:beforeCommit、beforeCompletion、afterCommit、afterCompletion、suspend、resume、flush 七个方法(后三个有默认实现)。注册入口是 TransactionSynchronizationManager.registerSynchronization,前提是 isSynchronizationActive():显式开启事务会激活同步,SUPPORTS/NOT_SUPPORTED 在无物理事务时也可能只激活同步(getTransaction 的 prepareSynchronization 分支),所以 isSynchronizationActive()==true 不能当作"物理事务存在"的证据,那是 isActualTransactionActive 的职责。注册顺序就是触发顺序的基础:getSynchronizations() 按 Order 排序返回,多个同步器用 getOrder 声明先后。
回调的异常路由不对称,这是同步器最常被误用的地方。beforeCommit 抛出的异常会中止提交并使事务回滚(调用方看得到);beforeCompletion 和 afterCompletion 的异常只被记日志(afterCompletion 的兄弟回调仍会收到状态);afterCommit 的异常会传播给调用方,但物理提交已经完成、不会逆转。实测(sync-fail 模式):beforeCommit 抛 IllegalStateException 后 H2 数据行回滚为空,afterCompletion 注册的两个同步器都收到 STATUS_ROLLED_BACK,其中一个在 afterCompletion 里抛错也不影响另一个。
原码:TransactionSynchronizationUtils.java,v6.1.12,连续 105–137 行,仅去除共同缩进。固定来源。
public static void triggerBeforeCompletion() {
for (TransactionSynchronization synchronization : TransactionSynchronizationManager.getSynchronizations()) {
try {
synchronization.beforeCompletion();
}
catch (Throwable ex) {
logger.error("TransactionSynchronization.beforeCompletion threw exception", ex);
}
}
}
/**
* Trigger {@code afterCommit} callbacks on all currently registered synchronizations.
* @throws RuntimeException if thrown by a {@code afterCommit} callback
* @see TransactionSynchronizationManager#getSynchronizations()
* @see TransactionSynchronization#afterCommit()
*/
public static void triggerAfterCommit() {
invokeAfterCommit(TransactionSynchronizationManager.getSynchronizations());
}
/**
* Actually invoke the {@code afterCommit} methods of the
* given Spring TransactionSynchronization objects.
* @param synchronizations a List of TransactionSynchronization objects
* @see TransactionSynchronization#afterCommit()
*/
public static void invokeAfterCommit(@Nullable List<TransactionSynchronization> synchronizations) {
if (synchronizations != null) {
for (TransactionSynchronization synchronization : synchronizations) {
synchronization.afterCommit();
}
}triggerBeforeCompletion 把每个回调包进 try/catch 记 error 日志,所以完成前的清理回调不能承担"失败必须让调用方知道"的职责;triggerAfterCommit/invokeAfterCommit 没有任何 catch,afterCommit 的异常原样上抛给 processCommit 的调用方。afterCompletion 的 invokeAfterCompletion(L150 附近)同样吞错——状态通知的可靠性由列表遍历保证,单个回调的失败不改变别人收到的状态。beforeCompletion 抛错被吞还有一个后果:你无法通过它阻止提交,那不是它的设计位置。
事务事件:@TransactionalEventListener 的四个阶段
模块事件解耦的常规做法是发布领域事件。@TransactionalEventListener 把监听方法挂到当前事务的完成时点:phase 支持 BEFORE_COMMIT、AFTER_COMMIT(默认)、AFTER_ROLLBACK、AFTER_COMPLETION。发布入口仍然是 ApplicationEventPublisher(或 TransactionalApplicationEventPublisher),发布动作只是把"事件 + 监听器"注册成事务同步;真正执行在事务完成回调里。condition(SpEL)按事件内容过滤,fallbackExecution 决定无事务时是否照常执行。
阶段路由是白名单式的:BEFORE_COMMIT 只在 beforeCommit 回调执行;AFTER_COMMIT 只在 afterCompletion 收到 STATUS_COMMITTED 时执行;AFTER_ROLLBACK 只在 STATUS_ROLLED_BACK 时执行;AFTER_COMPLETION 两种状态都执行。实测(events 模式):提交事务里发布事件 → BEFORE_COMMIT、AFTER_COMMIT、AFTER_COMPLETION 依次到达,AFTER_ROLLBACK 监听器沉默;回滚事务里发布事件 → AFTER_COMMIT 相监听器(默认、condition、fallback)全部静默,只有 AFTER_ROLLBACK 和 AFTER_COMPLETION 到达;无事务发布 → 默认监听器整组不投递,只有 fallbackExecution=true 的照常执行。condition="#e.amount > 10" 对 amount=5 的事件全程静默、对 50 放行。
原码:TransactionalApplicationListenerMethodAdapter.java,v6.1.12,连续 88–106 行,仅去除共同缩进。固定来源。
public void onApplicationEvent(ApplicationEvent event) {
if (TransactionalApplicationListenerSynchronization.register(event, this, this.callbacks)) {
if (logger.isDebugEnabled()) {
logger.debug("Registered transaction synchronization for " + event);
}
}
else if (this.fallbackExecution) {
if (getTransactionPhase() == TransactionPhase.AFTER_ROLLBACK && logger.isWarnEnabled()) {
logger.warn("Processing " + event + " as a fallback execution on AFTER_ROLLBACK phase");
}
processEvent(event);
}
else {
// No transactional event execution at all
if (logger.isDebugEnabled()) {
logger.debug("No transaction is active - skipping " + event);
}
}
}onApplicationEvent 三分支:同步激活且真实事务存在时注册成同步;否则看 fallbackExecution——AFTER_ROLLBACK 相的 fallback 会打 warn(无事务却执行回滚相语义上可疑);否则整条跳过。注册判断是 isSynchronizationActive() && isActualTransactionActive() 两个条件(TransactionalApplicationListenerSynchronization.register L75–L94),比同步器注册更严。
原码:TransactionalApplicationListenerSynchronization.java,v6.1.12,连续 107–126 行,仅去除共同缩进。固定来源。
@Override
public void beforeCommit(boolean readOnly) {
if (getTransactionPhase() == TransactionPhase.BEFORE_COMMIT) {
processEventWithCallbacks();
}
}
@Override
public void afterCompletion(int status) {
TransactionPhase phase = getTransactionPhase();
if (phase == TransactionPhase.AFTER_COMMIT && status == STATUS_COMMITTED) {
processEventWithCallbacks();
}
else if (phase == TransactionPhase.AFTER_ROLLBACK && status == STATUS_ROLLED_BACK) {
processEventWithCallbacks();
}
else if (phase == TransactionPhase.AFTER_COMPLETION) {
processEventWithCallbacks();
}
}PlatformSynchronization 在 beforeCommit/afterCompletion 两个回调里按相位白名单分发,事件执行本身再走 processEventWithCallbacks 的回调包装。AFTER_COMMIT 阶段执行的监听器在物理提交之后,它抛出的异常会沿 afterCompletion 吞错吗——不会,它在 beforeCommit 之外的 afterCompletion 白名单分支里执行,异常被 invokeAfterCompletion 记日志,调用方无感知。所以"提交后事件的失败"既不回滚也不报给调用方。
两个边界必须写进评审清单。其一,@TransactionalEventListener 的处理由 TransactionalEventListenerFactory 注册,而这个 factory 随 @EnableTransactionManagement(或 Boot 的事务自动配置)装入;缺失时方法会被普通 @EventListener 机制接管、在发布点立即执行。这不是理论风险:本票实验首版就因为装配里没有事务开关,实测四个相位监听器在 publishEvent 调用栈内直接全跑了一遍。其二,同进程事件不跨进程:AFTER_COMMIT 里发 MQ 失败不会回滚数据库,可靠投递的主归属是Outbox 与事务消息,事务事件只解决"同 JVM 内在正确时点触发"。
编程式事务:TransactionTemplate 与纯管理器调用
不用代理也能开事务。TransactionTemplate(实现 TransactionOperations)把"getTransaction → 执行回调 → commit/rollback"封装为 execute;它和 @Transactional 用同一个 PlatformTransactionManager,回滚判定却不同:模板只看回调抛不抛 RuntimeException/Error,checked 异常要么在回调里包装要么被模板包装后回滚。
原码:TransactionTemplate.java,v6.1.12,连续 130–155 行,仅去除共同缩进。固定来源。
public <T> T execute(TransactionCallback<T> action) throws TransactionException {
Assert.state(this.transactionManager != null, "No PlatformTransactionManager set");
if (this.transactionManager instanceof CallbackPreferringPlatformTransactionManager cpptm) {
return cpptm.execute(this, action);
}
else {
TransactionStatus status = this.transactionManager.getTransaction(this);
T result;
try {
result = action.doInTransaction(status);
}
catch (RuntimeException | Error ex) {
// Transactional code threw application exception -> rollback
rollbackOnException(status, ex);
throw ex;
}
catch (Throwable ex) {
// Transactional code threw unexpected exception -> rollback
rollbackOnException(status, ex);
throw new UndeclaredThrowableException(ex, "TransactionCallback threw undeclared checked exception");
}
this.transactionManager.commit(status);
return result;
}
}三个分支:RuntimeException/Error → rollbackOnException 后原样重抛;其他 Throwable(checked 只能以不可声明的方式从无 throws 签名的回调里逃逸,例如反射调用或泛型擦除陷阱)→ 回滚后包装成 UndeclaredThrowableException;正常返回 → commit。rollbackOnException(L163–L179 就近引用)里回滚本身失败会覆盖应用异常并记录"Application exception overridden by rollback exception"——异常链上应用异常在 cause 里。实测(programmatic 模式):sneakyThrow 出的 IOException 回滚后包装为 UndeclaredThrowableException(cause 是原 IOException);IllegalStateException 原样重抛;回调里主动 status.setRollbackOnly() 后正常返回——commit 走 localRollbackOnly 分支静默回滚,调用方看不到任何异常,与参与型失败抛 UnexpectedRollbackException 形成对照。这也回答了一个常见误区:setRollbackOnly 不是"抛异常",是"把本地标记位翻上,让 commit 退化成 rollback"。
更底层的形式是直接持有 PlatformTransactionManager:getTransaction(definition) 拿 DefaultTransactionStatus,try 业务,catch 里 rollback(status),成功 commit(status)。责任全部自担:忘记 commit 就悬挂、完成后再 commit 抛 IllegalTransactionStateException("do not call commit or rollback more than once per transaction")。业务代码用模板,纯管理器调用留给工具类。
传播行为矩阵:无事务与有事务两侧的完整分支
既有事务页的表讲过"已有 JDBC 事务时"七种行为的资源边界,REQUIRED/REQUIRES_NEW/NESTED 的实测在那页。本节补齐另一侧:没有外层事务时 getTransaction 的分支,和两个"拒绝型"传播在错误方向上的失败形态。
原码:AbstractPlatformTransactionManager.java,v6.1.12,连续 430–492 行,仅去除共同缩进。固定来源。
if (definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_NEVER) {
throw new IllegalTransactionStateException(
"Existing transaction found for transaction marked with propagation 'never'");
}
if (definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_NOT_SUPPORTED) {
if (debugEnabled) {
logger.debug("Suspending current transaction");
}
Object suspendedResources = suspend(transaction);
boolean newSynchronization = (getTransactionSynchronization() == SYNCHRONIZATION_ALWAYS);
return prepareTransactionStatus(
definition, null, false, newSynchronization, debugEnabled, suspendedResources);
}
if (definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_REQUIRES_NEW) {
if (debugEnabled) {
logger.debug("Suspending current transaction, creating new transaction with name [" +
definition.getName() + "]");
}
SuspendedResourcesHolder suspendedResources = suspend(transaction);
try {
return startTransaction(definition, transaction, false, debugEnabled, suspendedResources);
}
catch (RuntimeException | Error beginEx) {
resumeAfterBeginException(transaction, suspendedResources, beginEx);
throw beginEx;
}
}
if (definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_NESTED) {
if (!isNestedTransactionAllowed()) {
throw new NestedTransactionNotSupportedException(
"Transaction manager does not allow nested transactions by default - " +
"specify 'nestedTransactionAllowed' property with value 'true'");
}
if (debugEnabled) {
logger.debug("Creating nested transaction with name [" + definition.getName() + "]");
}
if (useSavepointForNestedTransaction()) {
// Create savepoint within existing Spring-managed transaction,
// through the SavepointManager API implemented by TransactionStatus.
// Usually uses JDBC savepoints. Never activates Spring synchronization.
DefaultTransactionStatus status = newTransactionStatus(
definition, transaction, false, false, true, debugEnabled, null);
this.transactionExecutionListeners.forEach(listener -> listener.beforeBegin(status));
try {
status.createAndHoldSavepoint();
}
catch (RuntimeException | Error ex) {
this.transactionExecutionListeners.forEach(listener -> listener.afterBegin(status, ex));
throw ex;
}
this.transactionExecutionListeners.forEach(listener -> listener.afterBegin(status, null));
return status;
}
else {
// Nested transaction through nested begin and commit/rollback calls.
// Usually only for JTA: Spring synchronization might get activated here
// in case of a pre-existing JTA transaction.
return startTransaction(definition, transaction, true, debugEnabled, null);
}
}handleExistingTransaction 先处理三个"方向不对"的分支:NEVER 在有事务时抛 IllegalTransactionStateException(消息点名 propagation 'never');NOT_SUPPORTED 先 suspend 再以无事务状态继续;REQUIRES_NEW 先 suspend 再 startTransaction,开始失败时 resumeAfterBeginException 恢复外层再上抛——内层 begin 失败不拖死外层(实测 propagation 模式里 NEVER 的拒绝被 catch 后外层照常提交);NESTED 在 isNestedTransactionAllowed 且 useSavepointForNestedTransaction 时走保存点(DataSourceTransactionManager 的 nestedTransactionAllowed 默认 true)。MANDATORY 的镜像分支在 getTransaction L393–L396:无事务时抛 "No existing transaction found for transaction marked with propagation 'mandatory'"。
实测(propagation 模式)覆盖两侧:无事务 MANDATORY → IllegalTransactionStateException;无事务 NEVER → 非事务执行、行立即持久;无事务 SUPPORTS → isActualTransactionActive=false 但 isSynchronizationActive=true(同步域仍然可用);有事务 NEVER → 拒绝且外层 catch 后照常提交;有事务 SUPPORTS → 与外层同一连接对象参与。四个"参与/拒绝"的关键变量只有一个:调用是否真的经过代理(互链U17 事务叠加与 T14 self 模式)。
保存点:NESTED 与 createSavepoint 是同一机制的两种入口
NESTED 传播不是"嵌套事务",是保存点:同一物理事务内标记一个可回退的位置。除注解声明外,TransactionStatus 直接暴露 createSavepoint/rollbackToSavepoint/releaseSavepoint 三个方法,两入口背后都是 SavepointManager。
原码:DefaultTransactionStatus.java,v6.1.12,连续 190–204 行,仅去除共同缩进。固定来源。
/**
* This implementation exposes the {@link SavepointManager} interface
* of the underlying transaction object, if any.
* @throws NestedTransactionNotSupportedException if savepoints are not supported
* @see #isTransactionSavepointManager()
*/
@Override
protected SavepointManager getSavepointManager() {
Object transaction = this.transaction;
if (!(transaction instanceof SavepointManager savepointManager)) {
throw new NestedTransactionNotSupportedException(
"Transaction object [" + this.transaction + "] does not support savepoints");
}
return savepointManager;
}transaction 对象不实现 SavepointManager 就抛 NestedTransactionNotSupportedException——保存点能力是管理器属性不是数据库通用能力。DataSourceTransactionManager 的 DataSourceTransactionObject 实现了 SavepointManager(JDBC SAVEPOINT 语句)。实测(savepoint 模式):createSavepoint → insert 1 → rollbackToSavepoint → createSavepoint → insert 2 → releaseSavepoint → insert 3,提交后 rows=[2,3];另一事务里 createSavepoint、insert 4、releaseSavepoint 后外层失败,rows 仍是 [2,3]——已释放保存点的写入随外层回滚一起消失。内层回滚到保存点不会恢复 Java 对象字段、已发出的远程调用或 MyBatis 一级缓存(互链MyBatis 页的 NESTED 边界声明)。
挂起与恢复:REQUIRES_NEW 时刻的资源事实
REQUIRES_NEW 的 suspend 不是"释放外层资源",是"把外层的线程上下文打包存起来"。挂起期间外层连接仍被外层事务持有、数据库锁不释放——T14 的池耗尽实验(池 1、内层 REQUIRES_NEW 无连接可用)就是直接后果。
原码:AbstractPlatformTransactionManager.java,v6.1.12,连续 615–649 行,仅去除共同缩进。固定来源。
protected final SuspendedResourcesHolder suspend(@Nullable Object transaction) throws TransactionException {
if (TransactionSynchronizationManager.isSynchronizationActive()) {
List<TransactionSynchronization> suspendedSynchronizations = doSuspendSynchronization();
try {
Object suspendedResources = null;
if (transaction != null) {
suspendedResources = doSuspend(transaction);
}
String name = TransactionSynchronizationManager.getCurrentTransactionName();
TransactionSynchronizationManager.setCurrentTransactionName(null);
boolean readOnly = TransactionSynchronizationManager.isCurrentTransactionReadOnly();
TransactionSynchronizationManager.setCurrentTransactionReadOnly(false);
Integer isolationLevel = TransactionSynchronizationManager.getCurrentTransactionIsolationLevel();
TransactionSynchronizationManager.setCurrentTransactionIsolationLevel(null);
boolean wasActive = TransactionSynchronizationManager.isActualTransactionActive();
TransactionSynchronizationManager.setActualTransactionActive(false);
return new SuspendedResourcesHolder(
suspendedResources, suspendedSynchronizations, name, readOnly, isolationLevel, wasActive);
}
catch (RuntimeException | Error ex) {
// doSuspend failed - original transaction is still active...
doResumeSynchronization(suspendedSynchronizations);
throw ex;
}
}
else if (transaction != null) {
// Transaction active but no synchronization active.
Object suspendedResources = doSuspend(transaction);
return new SuspendedResourcesHolder(suspendedResources);
}
else {
// Neither transaction nor synchronization active.
return null;
}
}suspend 做五件事:挂起全部同步器列表、清 name/readOnly/isolation/wasActive 四个线程状态、doSuspend 交还资源句柄(DataSourceTransactionManager 的实现是解绑 holder 但不关连接)、打包成 SuspendedResourcesHolder;doSuspend 失败时恢复同步器列表再上抛,原事务不丢。resume 按相反顺序恢复。实测(suspension 模式,池 2):外层借连接 A(pool active=1),内层 REQUIRES_NEW 借到不同的连接 B(身份比较不相等,pool active=2),内层完成后 active 回落 1;外层同步器的 beforeCommit 在内层 afterCompletion(status=COMMITTED) 之后才触发——内层是完整独立的物理事务,外层回调只认外层边界。
隔离级别与只读:注解值到驱动行为的距离
@Transactional(isolation=...) 和 readOnly 的语义层级常被混为一谈:注解值只是"请求",真正生效要看管理器是否新开物理事务、驱动和数据库怎么解释。新物理事务时 doBegin 经 prepareConnectionForTransaction 把隔离级别和非只读标志应用到连接;参与现有事务时什么都不改(内层声明的隔离不会升级外层),默认静默接受。
原码:DataSourceUtils.java,v6.1.12,连续 178–221 行,仅去除共同缩进。固定来源。
public static Integer prepareConnectionForTransaction(Connection con, @Nullable TransactionDefinition definition)
throws SQLException {
Assert.notNull(con, "No Connection specified");
boolean debugEnabled = logger.isDebugEnabled();
// Set read-only flag.
if (definition != null && definition.isReadOnly()) {
try {
if (debugEnabled) {
logger.debug("Setting JDBC Connection [" + con + "] read-only");
}
con.setReadOnly(true);
}
catch (SQLException | RuntimeException ex) {
Throwable exToCheck = ex;
while (exToCheck != null) {
if (exToCheck.getClass().getSimpleName().contains("Timeout")) {
// Assume it's a connection timeout that would otherwise get lost: e.g. from JDBC 4.0
throw ex;
}
exToCheck = exToCheck.getCause();
}
// "read-only not supported" SQLException -> ignore, it's just a hint anyway
logger.debug("Could not set JDBC Connection read-only", ex);
}
}
// Apply specific isolation level, if any.
Integer previousIsolationLevel = null;
if (definition != null && definition.getIsolationLevel() != TransactionDefinition.ISOLATION_DEFAULT) {
if (debugEnabled) {
logger.debug("Changing isolation level of JDBC Connection [" + con + "] to " +
definition.getIsolationLevel());
}
int currentIsolation = con.getTransactionIsolation();
if (currentIsolation != definition.getIsolationLevel()) {
previousIsolationLevel = currentIsolation;
con.setTransactionIsolation(definition.getIsolationLevel());
}
}
return previousIsolationLevel;
}readOnly 分支 setReadOnly(true) 失败就忽略("it's just a hint anyway");隔离分支只在请求非 DEFAULT 且与当前不同时才改并记住旧值,供 cleanup 恢复。所以"readOnly 保证不可写"是错的:H2 2.3.232 对提示不阻止 INSERT(实测 readonly-h2 模式,写入照常落库);MySQL + Connector/J 9.2.0 则在驱动侧拒绝写语句(实测消息 "Connection is read-only. Queries leading to data modification are not allowed.",见本票 JDBC 页)。
隔离级别的真实差异是本票最重要的边界:H2 2.3.232 不拒绝 READ_UNCOMMITTED 请求(getTransactionIsolation 原样返回),但其引擎行为是 READ COMMITTED,脏读无法复现;MySQL 9.7.1 隔离实例实测 READ-UNCOMMITTED 会话读到写方未提交的行、写方回滚后行消失,且 Spring 的 ISOLATION_READ_UNCOMMITTED 确实下发为会话值 READ-UNCOMMITTED(isolation 与 dirtyread-mysql 模式)。"隔离级别名字相同"不是语义相同的证明。
原码:AbstractPlatformTransactionManager.java,v6.1.12,连续 499–518 行,仅去除共同缩进。固定来源。
if (isValidateExistingTransaction()) {
if (definition.getIsolationLevel() != TransactionDefinition.ISOLATION_DEFAULT) {
Integer currentIsolationLevel = TransactionSynchronizationManager.getCurrentTransactionIsolationLevel();
if (currentIsolationLevel == null || currentIsolationLevel != definition.getIsolationLevel()) {
throw new IllegalTransactionStateException("Participating transaction with definition [" +
definition + "] specifies isolation level which is incompatible with existing transaction: " +
(currentIsolationLevel != null ?
DefaultTransactionDefinition.getIsolationLevelName(currentIsolationLevel) :
"(unknown)"));
}
}
if (!definition.isReadOnly()) {
if (TransactionSynchronizationManager.isCurrentTransactionReadOnly()) {
throw new IllegalTransactionStateException("Participating transaction with definition [" +
definition + "] is not marked as read-only but existing transaction is");
}
}
}
boolean newSynchronization = (getTransactionSynchronization() != SYNCHRONIZATION_NEVER);
return prepareTransactionStatus(definition, transaction, false, newSynchronization, debugEnabled, null);静默接受会掩盖误配:内层请求的隔离和外层不一致时默认照常参与。validateExistingTransaction=true 把静默变成失败——isolation 不匹配或"外层只读而内层非只读"都抛 IllegalTransactionStateException(实测消息含 "isolation level which is incompatible")。这个开关建议在有跨团队复用的服务层打开:它把注解拼错从"运行时行为漂移"提前为"显式失败"。
回滚规则:rollbackFor 匹配深度与平局顺序
默认规则(RuntimeException/Error 回滚、checked 提交)之上,rollbackFor/noRollbackFor 的匹配不是模糊匹配而是"继承深度最近者胜"。RuleBasedTransactionAttribute.rollbackOn 遍历全部规则取深度最小者;无规则命中退回默认。
原码:RuleBasedTransactionAttribute.java,v6.1.12,连续 125–145 行,仅去除共同缩进。固定来源。
public boolean rollbackOn(Throwable ex) {
RollbackRuleAttribute winner = null;
int deepest = Integer.MAX_VALUE;
if (this.rollbackRules != null) {
for (RollbackRuleAttribute rule : this.rollbackRules) {
int depth = rule.getDepth(ex);
if (depth >= 0 && depth < deepest) {
deepest = depth;
winner = rule;
}
}
}
// User superclass behavior (rollback on unchecked) if no rule matches.
if (winner == null) {
return super.rollbackOn(ex);
}
return !(winner instanceof NoRollbackRuleAttribute);
}winner == null 才走 super.rollbackOn 的 unchecked 默认;winner 是 NoRollbackRuleAttribute 则提交。两个细节实测确认(rollback-rules 模式):rollbackFor=IOException 与 noRollbackFor=FileNotFoundException 同时存在时,FileNotFoundException 实例按 noRollback 提交——精确匹配深度 0 胜过父类匹配深度 1;两条规则命名同一个类时深度相同,depth < deepest 的严格小于让先注册的规则胜出——rollbackFor 与 noRollbackFor 指向同一异常时结果取决于规则列表顺序,这是评审时该禁写的组合。getDepth 沿父类链逐级 +1、到 Throwable 为止返回 -1(不匹配),不检查接口。
回滚规则解决"异常该不该回滚";U17 讲的切面 order 错位解决"异常到底有没有到达拦截器"——两层各自独立,评审清单要同时勾。方法内 catch 不经过任何边界时不触发任何规则(互链rollback-only 节的反例条件)。
与 ORM 集成的边界
Spring 事务抽象对 ORM 的承诺只有一条:同一 DataSource key 上的资源共享。DataSourceTransactionManager 把 ConnectionHolder 绑到线程,DataSourceUtils.getConnection 按同一 key 复用;ORM 层各自决定怎么借这个资源——MyBatis-Spring 的 SqlSessionTemplate 按 holder 复用 SqlSession、SpringManagedTransaction 识别"Spring 管理的连接"后把 commit/rollback 交给管理器(主归属在MyBatis 页,会话绑定细节归 U42);JPA 的 JpaTransactionManager 以 EntityManagerFactory 为资源 key 做同类绑定(归 U43)。本票边界:ORM 集成讲解到"资源 key 与复用协议"为止,会话/持久化上下文的内部协议是兄弟票的;选错事务管理器或 DataSource 包装层(互链 T14 的"四条线")会让注解存在但资源不共享。
实验:同步、事件、传播与挂起的可验证结果
从仓库根运行 JAVA_HOME=$(/usr/libexec/java_home -v17) bash labs/spring-senior/run.sh sync,环境见实验说明。每模式独立 JVM/内存库,断言失败非零退出。
| 输入 | 应观察的最终状态 |
|---|---|
| sync-phases | 八条回调按 @Order 序列:high/low 的 beforeCommit→beforeCompletion→afterCommit→afterCompletion(COMMITTED) |
| sync-fail | beforeCommit 抛错 → 表空;两个 afterCompletion 都收 STATUS_ROLLED_BACK,兄弟抛错不拦截 |
| events | 提交:BEFORE_COMMIT→AFTER_COMMIT 相→AFTER_COMPLETION;回滚:AFTER_COMMIT 相全静默;无事务:仅 fallback 监听器收到 |
| programmatic | checked → UndeclaredThrowableException+回滚;runtime → 原样重抛+回滚;setRollbackOnly → 无异常静默回滚 |
| propagation | MANDATORY 无事务拒绝;NEVER 两向(无事务非事务执行/有事务拒绝且外层存活);SUPPORTS 无事务仅激活同步、有事务同连接参与 |
| isolation | SERIALIZABLE 应用于连接;H2 原样回显 READ_UNCOMMITTED;参与型不升级;validateExistingTransaction 抛 IllegalTransactionStateException |
| savepoint | create/rollbackTo/release 序列 rows=[2,3];外层回滚清掉已释放保存点的写入 |
| suspension | REQUIRES_NEW 第二连接;池 active 1→2→1;内层完成先于外层 beforeCommit |
| rollback-rules | 深度 0 胜深度 1;同深度按注册顺序;无规则 checked 提交 |
本批 9 模式已实际运行通过(Oracle JDK 17.0.12,Framework 6.1.12);隔离与只读的 MySQL 对照在本票 JDBC 页的一次性隔离实例上运行。注解式代理的行为(external/self/rollbackFor/Future)回归在 T14 的 AnnotatedTxLab,不重复断言。
package study.spring;
import java.sql.Connection;
import java.util.*;
import java.lang.reflect.UndeclaredThrowableException;
import org.h2.jdbcx.JdbcDataSource;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.Ordered;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.jdbc.datasource.DataSourceUtils;
import org.springframework.stereotype.Component;
import org.springframework.transaction.*;
import org.springframework.transaction.annotation.EnableTransactionManagement;
import org.springframework.transaction.event.*;
import org.springframework.transaction.interceptor.NoRollbackRuleAttribute;
import org.springframework.transaction.interceptor.RollbackRuleAttribute;
import org.springframework.transaction.interceptor.RuleBasedTransactionAttribute;
import org.springframework.transaction.support.*;
/**
* U18 lab: synchronization lifecycle, transactional events, programmatic templates,
* propagation matrix (no-transaction side), savepoints, suspension, isolation and
* rollback rules. H2 only — it cannot demonstrate production lock or dirty-read
* semantics (see JdbcLab for the real-MySQL contrast).
*/
public class TransactionLab {
static final List<String> TRACE = new ArrayList<>();
static JdbcDataSource h2(String name) {
var ds = new JdbcDataSource();
ds.setURL("jdbc:h2:mem:" + name + ";DB_CLOSE_DELAY=-1");
return ds;
}
static String isolationName(int level) {
return switch (level) {
case Connection.TRANSACTION_READ_UNCOMMITTED -> "READ_UNCOMMITTED";
case Connection.TRANSACTION_READ_COMMITTED -> "READ_COMMITTED";
case Connection.TRANSACTION_REPEATABLE_READ -> "REPEATABLE_READ";
case Connection.TRANSACTION_SERIALIZABLE -> "SERIALIZABLE";
default -> "UNKNOWN(" + level + ")";
};
}
@SuppressWarnings("unchecked")
static <E extends Throwable> RuntimeException sneakyThrow(Throwable e) throws E { throw (E) e; }
/** Ordered synchronization recording every callback with its name and status. */
static final class RecordingSync implements TransactionSynchronization, Ordered {
final String name; final int order; final boolean failBeforeCommit; final boolean failAfterCompletion;
RecordingSync(String name, int order, boolean failBeforeCommit, boolean failAfterCompletion) {
this.name = name; this.order = order; this.failBeforeCommit = failBeforeCommit; this.failAfterCompletion = failAfterCompletion;
}
@Override public int getOrder() { return order; }
@Override public void beforeCommit(boolean readOnly) {
TRACE.add(name + ".beforeCommit(syncActive=" + TransactionSynchronizationManager.isSynchronizationActive()
+ ",txActive=" + TransactionSynchronizationManager.isActualTransactionActive() + ",readOnly=" + readOnly + ")");
if (failBeforeCommit) throw new IllegalStateException("beforeCommit failure");
}
@Override public void beforeCompletion() { TRACE.add(name + ".beforeCompletion"); }
@Override public void afterCommit() { TRACE.add(name + ".afterCommit"); }
@Override public void afterCompletion(int status) {
TRACE.add(name + ".afterCompletion(status=" + (status == STATUS_COMMITTED ? "COMMITTED" : status == STATUS_ROLLED_BACK ? "ROLLED_BACK" : String.valueOf(status)) + ")");
if (failAfterCompletion) throw new IllegalStateException("afterCompletion failure");
}
}
public static void main(String[] args) {
String mode = args.length == 0 ? "sync-phases" : args[0];
switch (mode) {
case "sync-phases" -> syncPhases();
case "sync-fail" -> syncFail();
case "events" -> events();
case "programmatic" -> programmatic();
case "propagation" -> propagation();
case "isolation" -> isolation();
case "savepoint" -> savepoint();
case "suspension" -> suspension();
case "rollback-rules" -> rollbackRules();
default -> throw new IllegalArgumentException(mode);
}
}
static void syncPhases() {
var ds = h2("u18sync");
var tm = new DataSourceTransactionManager(ds);
var jdbc = new JdbcTemplate(ds);
jdbc.execute("create table item(id int primary key)");
var tt = new TransactionTemplate(tm);
tt.execute(s -> {
TransactionSynchronizationManager.registerSynchronization(new RecordingSync("low", 1, false, false));
TransactionSynchronizationManager.registerSynchronization(new RecordingSync("high", 0, false, false));
jdbc.update("insert into item values(1)");
return null;
});
// Ordered: order 0 ("high") first; the same sorted list is reused for the completion callbacks.
if (!Objects.equals(TRACE, List.of(
"high.beforeCommit(syncActive=true,txActive=true,readOnly=false)",
"low.beforeCommit(syncActive=true,txActive=true,readOnly=false)",
"high.beforeCompletion", "low.beforeCompletion",
"high.afterCommit", "low.afterCommit",
"high.afterCompletion(status=COMMITTED)", "low.afterCompletion(status=COMMITTED)")))
throw new AssertionError("commit-phase trace mismatch: " + TRACE);
if (!jdbc.queryForList("select id from item", Integer.class).equals(List.of(1)))
throw new AssertionError("rows after commit");
System.out.println("PASS tx-u18 sync-phases callbacks=8 ordered=high-first status=COMMITTED");
}
static void syncFail() {
var ds = h2("u18syncfail");
var tm = new DataSourceTransactionManager(ds);
var jdbc = new JdbcTemplate(ds);
jdbc.execute("create table item(id int primary key)");
var tt = new TransactionTemplate(tm);
// beforeCommit throws -> commit path aborted -> physical rollback; beforeCompletion still runs.
try {
tt.execute(s -> {
TransactionSynchronizationManager.registerSynchronization(new RecordingSync("bad", 0, true, false));
TransactionSynchronizationManager.registerSynchronization(new RecordingSync("sibling", 1, false, true));
jdbc.update("insert into item values(1)");
return null;
});
throw new AssertionError("expected beforeCommit failure to propagate");
} catch (IllegalStateException expected) {
if (!"beforeCommit failure".equals(expected.getMessage())) throw expected;
}
// afterCompletion failures are logged, not propagated: "sibling" throwing must not stop
// or reverse the sequence, and "bad" still received STATUS_ROLLED_BACK first.
if (!TRACE.contains("bad.afterCompletion(status=ROLLED_BACK)")
|| !TRACE.contains("sibling.afterCompletion(status=ROLLED_BACK)")
|| TRACE.indexOf("bad.afterCompletion(status=ROLLED_BACK)") > TRACE.indexOf("sibling.afterCompletion(status=ROLLED_BACK)"))
throw new AssertionError("rollback trace mismatch: " + TRACE);
if (TRACE.stream().anyMatch(t -> t.startsWith("bad") && t.contains(".afterCommit")))
throw new AssertionError("no afterCommit callbacks on rollback");
if (!jdbc.queryForList("select id from item", Integer.class).isEmpty())
throw new AssertionError("beforeCommit failure must roll back");
System.out.println("PASS tx-u18 sync-fail beforeCommitThrow=rolledBack afterCompletionSwallowed=true rows=[]");
}
public record OrderEvent(int amount, String tag) {}
@Component
static class EventListeners {
@TransactionalEventListener(phase = TransactionPhase.BEFORE_COMMIT)
public void before(OrderEvent e) { TRACE.add("evt.beforeCommit:" + e.tag()); }
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
public void after(OrderEvent e) { TRACE.add("evt.afterCommit:" + e.tag()); }
@TransactionalEventListener(phase = TransactionPhase.AFTER_ROLLBACK)
public void rolled(OrderEvent e) { TRACE.add("evt.afterRollback:" + e.tag()); }
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMPLETION)
public void completed(OrderEvent e) { TRACE.add("evt.afterCompletion:" + e.tag()); }
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT, condition = "#e.amount > 10")
public void bigOnly(OrderEvent e) { TRACE.add("evt.condition:" + e.tag()); }
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT, fallbackExecution = true)
public void fallback(OrderEvent e) { TRACE.add("evt.fallback:" + e.tag()); }
}
@Configuration(proxyBeanMethods = false)
@EnableTransactionManagement
static class EventConfig {
@Bean JdbcDataSource dataSource() { return h2("u18evt"); }
@Bean DataSourceTransactionManager transactionManager(JdbcDataSource ds) { return new DataSourceTransactionManager(ds); }
@Bean JdbcTemplate jdbc(JdbcDataSource ds) { return new JdbcTemplate(ds); }
@Bean TransactionTemplate transactionTemplate(DataSourceTransactionManager tm) { return new TransactionTemplate(tm); }
@Bean EventListeners listeners() { return new EventListeners(); }
}
static void events() {
try (var ctx = new AnnotationConfigApplicationContext(EventConfig.class)) {
var tt = ctx.getBean(TransactionTemplate.class);
var jdbc = ctx.getBean(JdbcTemplate.class);
jdbc.execute("create table item(id int primary key, tag varchar(20))");
var order1 = new OrderEvent(5, "order-5");
tt.execute(s -> { ctx.publishEvent(order1); jdbc.update("insert into item values(1,?)", "order-5"); return null; });
// committed tx: BEFORE_COMMIT group -> AFTER_COMMIT group -> AFTER_COMPLETION group.
// Same-phase listener order between the two AFTER_COMMIT adapters is adapter enumeration
// order (declared-method order is undefined), so we assert phase groups, not intra-group order.
if (TRACE.size() != 4 || !TRACE.contains("evt.beforeCommit:order-5")
|| !TRACE.contains("evt.afterCommit:order-5") || !TRACE.contains("evt.fallback:order-5")
|| !TRACE.contains("evt.afterCompletion:order-5")
|| TRACE.indexOf("evt.beforeCommit:order-5") > TRACE.indexOf("evt.fallback:order-5")
|| Math.min(TRACE.indexOf("evt.fallback:order-5"), TRACE.indexOf("evt.afterCommit:order-5"))
> TRACE.indexOf("evt.afterCompletion:order-5")
|| TRACE.contains("evt.afterRollback:order-5") || TRACE.contains("evt.condition:order-5"))
throw new AssertionError("commit event trace mismatch: " + TRACE);
TRACE.clear();
var order2 = new OrderEvent(99, "order-99");
try {
tt.execute(s -> { ctx.publishEvent(order2); jdbc.update("insert into item values(2,?)", "order-99");
throw new IllegalStateException("business failure"); });
throw new AssertionError("expected rollback");
} catch (IllegalStateException expected) {
if (!"business failure".equals(expected.getMessage())) throw expected;
}
// rolled back tx: AFTER_COMMIT-phase listeners (default, condition, fallback) all silent;
// only AFTER_ROLLBACK and AFTER_COMPLETION fired (relative order of completion-phase
// listeners is adapter enumeration order, not asserted).
if (TRACE.size() != 2 || !TRACE.contains("evt.afterRollback:order-99")
|| !TRACE.contains("evt.afterCompletion:order-99")
|| TRACE.contains("evt.beforeCommit:order-99") || TRACE.contains("evt.afterCommit:order-99")
|| TRACE.contains("evt.fallback:order-99") || TRACE.contains("evt.condition:order-99"))
throw new AssertionError("rollback event trace mismatch: " + TRACE);
TRACE.clear();
ctx.publishEvent(new OrderEvent(7, "no-tx"));
// No transaction: default-phase listeners skipped entirely, fallbackExecution delivered inline.
if (!Objects.equals(TRACE, List.of("evt.fallback:no-tx")))
throw new AssertionError("no-tx event trace mismatch: " + TRACE);
TRACE.clear();
tt.execute(s -> { ctx.publishEvent(new OrderEvent(50, "big-50")); return null; });
if (!TRACE.contains("evt.condition:big-50")) throw new AssertionError("condition amount=50 must pass: " + TRACE);
if (!jdbc.queryForList("select id from item order by id", Integer.class).equals(List.of(1)))
throw new AssertionError("rolled-back row 2 must be gone");
System.out.println("PASS tx-u18 events phases=4 conditionFiltered=5 noTxDropped=true fallbackDelivered=true"
+ " rollbackSilencedAfterCommitPhase=true");
}
}
static void programmatic() {
var ds = h2("u18prog");
var tm = new DataSourceTransactionManager(ds);
var jdbc = new JdbcTemplate(ds);
jdbc.execute("create table item(id int primary key)");
var tt = new TransactionTemplate(tm);
// checked exception smuggled past the no-throws callback signature (sneakyThrow): the template's
// catch(Throwable) branch rolls back and wraps it in UndeclaredThrowableException.
try {
tt.execute(s -> { jdbc.update("insert into item values(1)");
return sneakyThrow(new java.io.IOException("checked inside callback")); });
throw new AssertionError("expected checked wrapper");
} catch (UndeclaredThrowableException expected) {
if (!(expected.getCause() instanceof java.io.IOException) || !"checked inside callback".equals(expected.getCause().getMessage()))
throw expected;
}
// runtime exception: rolled back and rethrown unchanged
try {
tt.execute(s -> { jdbc.update("insert into item values(2)"); throw new IllegalStateException("runtime inside callback"); });
throw new AssertionError("expected runtime");
} catch (IllegalStateException expected) {
if (!"runtime inside callback".equals(expected.getMessage())) throw expected;
}
// local setRollbackOnly: commit turns into a silent rollback, no exception reaches the caller
tt.executeWithoutResult(s -> { jdbc.update("insert into item values(3)"); s.setRollbackOnly(); });
var rows = jdbc.queryForList("select id from item order by id", Integer.class);
if (!rows.isEmpty()) throw new AssertionError("checked/runtime/setRollbackOnly all must roll back, rows=" + rows);
System.out.println("PASS tx-u18 programmatic checkedWrapped=UndeclaredThrowableException"
+ " runtimeRethrown=true localRollbackSilent=true rows=[]");
}
static void propagation() {
var ds = h2("u18prop");
var tm = new DataSourceTransactionManager(ds);
var jdbc = new JdbcTemplate(ds);
jdbc.execute("create table item(id int primary key)");
// no outer transaction: MANDATORY refuses, NEVER runs non-transactionally, SUPPORTS activates sync only
var mandatory = new TransactionTemplate(tm);
mandatory.setPropagationBehavior(TransactionDefinition.PROPAGATION_MANDATORY);
try {
mandatory.executeWithoutResult(s -> jdbc.update("insert into item values(1)"));
throw new AssertionError("expected MANDATORY rejection");
} catch (IllegalTransactionStateException expected) {
if (!String.valueOf(expected.getMessage()).contains("mandatory")) throw expected;
}
var never = new TransactionTemplate(tm);
never.setPropagationBehavior(TransactionDefinition.PROPAGATION_NEVER);
never.executeWithoutResult(s -> {
if (TransactionSynchronizationManager.isActualTransactionActive())
throw new AssertionError("NEVER without outer tx must not be transactional");
jdbc.update("insert into item values(1)");
});
var supports = new TransactionTemplate(tm);
supports.setPropagationBehavior(TransactionDefinition.PROPAGATION_SUPPORTS);
boolean[] syncActive = new boolean[1];
supports.executeWithoutResult(s -> {
syncActive[0] = TransactionSynchronizationManager.isSynchronizationActive();
if (TransactionSynchronizationManager.isActualTransactionActive())
throw new AssertionError("SUPPORTS without outer tx must have no physical transaction");
jdbc.update("insert into item values(2)");
});
if (!syncActive[0]) throw new AssertionError("SUPPORTS without tx must still activate synchronization");
if (!jdbc.queryForList("select id from item", Integer.class).equals(List.of(1, 2)))
throw new AssertionError("non-transactional rows must persist immediately");
// with an outer transaction: NEVER refuses and the outer survives to commit
var required = new TransactionTemplate(tm);
required.executeWithoutResult(s -> {
jdbc.update("insert into item values(3)");
try {
never.executeWithoutResult(inner -> jdbc.update("insert into item values(99)"));
throw new AssertionError("expected NEVER rejection inside tx");
} catch (IllegalTransactionStateException expected) {
if (!String.valueOf(expected.getMessage()).contains("never")) throw expected;
}
});
if (!jdbc.queryForList("select id from item order by id", Integer.class).equals(List.of(1, 2, 3)))
throw new AssertionError("outer must commit after inner NEVER rejection, rows=" + jdbc.queryForList("select id from item order by id", Integer.class));
// SUPPORTS joins the outer physical transaction (same connection identity)
required.executeWithoutResult(s -> {
Connection outer = DataSourceUtils.getConnection(ds);
supports.executeWithoutResult(inner -> {
if (DataSourceUtils.getConnection(ds) != outer)
throw new AssertionError("SUPPORTS must participate in outer connection");
jdbc.update("insert into item values(4)");
});
});
if (!jdbc.queryForList("select id from item order by id", Integer.class).equals(List.of(1, 2, 3, 4)))
throw new AssertionError("joined rows must commit with outer");
System.out.println("PASS tx-u18 propagation mandatoryNoTx=refused neverNoTx=autocommit"
+ " supportsNoTx=syncActiveNoTx neverInTx=refusedOuterSurvived supportsInTx=sameConnection");
}
static void savepoint() {
var ds = h2("u18sp");
var tm = new DataSourceTransactionManager(ds);
var jdbc = new JdbcTemplate(ds);
jdbc.execute("create table item(id int primary key)");
var tt = new TransactionTemplate(tm);
tt.executeWithoutResult(s -> {
Object sp1 = s.createSavepoint();
jdbc.update("insert into item values(1)");
s.rollbackToSavepoint(sp1);
Object sp2 = s.createSavepoint();
jdbc.update("insert into item values(2)");
s.releaseSavepoint(sp2);
jdbc.update("insert into item values(3)");
});
if (!jdbc.queryForList("select id from item order by id", Integer.class).equals(List.of(2, 3)))
throw new AssertionError("savepoint rollback must keep 2,3 only");
// everything inside the outer physical transaction dies with it, released savepoints included
try {
tt.executeWithoutResult(s -> {
Object sp = s.createSavepoint();
jdbc.update("insert into item values(4)");
s.releaseSavepoint(sp);
throw new IllegalStateException("outer failure after released savepoint");
});
throw new AssertionError("expected outer failure");
} catch (IllegalStateException expected) {
if (!"outer failure after released savepoint".equals(expected.getMessage())) throw expected;
}
if (!jdbc.queryForList("select id from item order by id", Integer.class).equals(List.of(2, 3)))
throw new AssertionError("outer rollback must erase savepoint writes");
System.out.println("PASS tx-u18 savepoint createRollbackToRelease=rows[2,3] outerRollbackErasesReleased=true");
}
static void suspension() {
var cfg = new com.zaxxer.hikari.HikariConfig();
cfg.setJdbcUrl("jdbc:h2:mem:u18susp;DB_CLOSE_DELAY=-1");
cfg.setMaximumPoolSize(2);
cfg.setConnectionTimeout(500);
try (var ds = new com.zaxxer.hikari.HikariDataSource(cfg)) {
var tm = new DataSourceTransactionManager(ds);
var jdbc = new JdbcTemplate(ds);
jdbc.execute("create table item(id int primary key)");
var tt = new TransactionTemplate(tm);
var inner = new TransactionTemplate(tm);
inner.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW);
tt.execute(s -> {
Connection outer = DataSourceUtils.getConnection(ds);
jdbc.update("insert into item values(1)");
TransactionSynchronizationManager.registerSynchronization(new RecordingSync("outer", 0, false, false));
if (active(ds) != 1) throw new AssertionError("outer must hold exactly one pooled connection");
inner.execute(i -> {
Connection innerConn = DataSourceUtils.getConnection(ds);
if (innerConn == outer) throw new AssertionError("REQUIRES_NEW must borrow a second connection");
if (!TransactionSynchronizationManager.isActualTransactionActive())
throw new AssertionError("inner must be transaction-active");
TransactionSynchronizationManager.registerSynchronization(new RecordingSync("inner", 0, false, false));
jdbc.update("insert into item values(2)");
if (active(ds) != 2) throw new AssertionError("both connections held while inner runs");
return null;
});
if (active(ds) != 1) throw new AssertionError("inner connection must be returned after inner completes");
return null;
});
// inner completed (and its afterCompletion fired) strictly before outer.beforeCommit fired at outer commit
int innerCompletion = TRACE.indexOf("inner.afterCompletion(status=COMMITTED)");
int outerBefore = TRACE.indexOf(firstContaining(TRACE, "outer.beforeCommit"));
if (innerCompletion < 0 || outerBefore < 0 || innerCompletion > outerBefore)
throw new AssertionError("suspension ordering mismatch: " + TRACE);
if (!jdbc.queryForList("select id from item order by id", Integer.class).equals(List.of(1, 2)))
throw new AssertionError("both rows must commit (inner independent)");
System.out.println("PASS tx-u18 suspension innerConnection=second innerBeforeOuterCommit=true"
+ " poolOuter=1 poolDuringInner=2 rows=[1,2]");
}
}
static String firstContaining(List<String> list, String part) {
for (String s : list) if (s.contains(part)) return s;
return null;
}
/** The isolation actually applied to the thread-bound transaction connection. */
static int boundIsolation(javax.sql.DataSource ds) {
try { return DataSourceUtils.getConnection(ds).getTransactionIsolation(); }
catch (java.sql.SQLException e) { throw new IllegalStateException(e); }
}
static int active(javax.sql.DataSource ds) {
var pool = ((com.zaxxer.hikari.HikariDataSource) ds).getHikariPoolMXBean();
return pool == null ? -1 : pool.getActiveConnections();
}
static void isolation() {
var ds = h2("u18iso");
var tm = new DataSourceTransactionManager(ds);
var jdbc = new JdbcTemplate(ds);
jdbc.execute("create table item(id int primary key)");
// new physical tx: requested isolation is applied to the connection
var serializable = new TransactionTemplate(tm);
serializable.setIsolationLevel(TransactionDefinition.ISOLATION_SERIALIZABLE);
serializable.executeWithoutResult(s -> {
int actual = boundIsolation(ds);
if (actual != Connection.TRANSACTION_SERIALIZABLE) throw new AssertionError("SERIALIZABLE must apply, got " + isolationName(actual));
});
// H2 accepts READ_UNCOMMITTED without rejection but its engine implements it as
// READ_COMMITTED (no dirty reads are possible); MySQL InnoDB really supports it (JdbcLab).
var readUncommitted = new TransactionTemplate(tm);
readUncommitted.setIsolationLevel(TransactionDefinition.ISOLATION_READ_UNCOMMITTED);
readUncommitted.executeWithoutResult(s -> {
int actual = boundIsolation(ds);
if (actual != Connection.TRANSACTION_READ_UNCOMMITTED)
throw new AssertionError("H2 accepts the request and reports it back, got " + isolationName(actual));
});
// inner REQUIRED with different isolation: no upgrade, no error by default
var outerSer = new TransactionTemplate(tm);
outerSer.setIsolationLevel(TransactionDefinition.ISOLATION_SERIALIZABLE);
var innerRC = new TransactionTemplate(tm);
innerRC.setIsolationLevel(TransactionDefinition.ISOLATION_READ_COMMITTED);
outerSer.executeWithoutResult(s -> {
innerRC.executeWithoutResult(i -> {
int actual = boundIsolation(ds);
if (actual != Connection.TRANSACTION_SERIALIZABLE)
throw new AssertionError("participating inner must not upgrade outer isolation, got " + isolationName(actual));
});
});
// validateExistingTransaction=true turns silent acceptance into IllegalTransactionStateException
var strictTm = new DataSourceTransactionManager(ds);
strictTm.setValidateExistingTransaction(true);
var strictOuter = new TransactionTemplate(strictTm);
strictOuter.setIsolationLevel(TransactionDefinition.ISOLATION_SERIALIZABLE);
var strictInner = new TransactionTemplate(strictTm);
strictInner.setIsolationLevel(TransactionDefinition.ISOLATION_READ_COMMITTED);
try {
strictOuter.executeWithoutResult(s -> strictInner.executeWithoutResult(i -> { }));
throw new AssertionError("expected validation failure");
} catch (IllegalTransactionStateException expected) {
if (!String.valueOf(expected.getMessage()).contains("isolation level which is incompatible")) throw expected;
}
System.out.println("PASS tx-u18 isolation serializableApplied=true h2ReadUncommitted=acceptedNoRejection"
+ " innerNoUpgrade=true validateExistingTransaction=IllegalTransactionStateException");
}
static void rollbackRules() {
// depth: exact match (0) beats superclass match (1..n); non-matching (-1) rules are ignored.
var a = new RuleBasedTransactionAttribute();
a.setRollbackRules(List.of(new RollbackRuleAttribute(java.io.IOException.class),
new NoRollbackRuleAttribute(java.io.FileNotFoundException.class)));
if (a.rollbackOn(new java.io.FileNotFoundException("x")))
throw new AssertionError("depth-0 noRollback(FileNotFoundException) must beat depth-1 rollback(IOException)");
if (!a.rollbackOn(new java.io.IOException("x"))) throw new AssertionError("rollback(IOException) must match itself");
if (!a.rollbackOn(new IllegalStateException("x")))
throw new AssertionError("no matching rule must fall back to unchecked default");
if (a.rollbackOn(new java.text.ParseException("x", 0)))
throw new AssertionError("checked exceptions must commit without a matching rule");
// same-depth tie: two rules naming the same class -> first registered wins (strict '<' in rollbackOn)
var tieRollbackFirst = new RuleBasedTransactionAttribute();
tieRollbackFirst.setRollbackRules(List.of(new RollbackRuleAttribute(java.io.FileNotFoundException.class),
new NoRollbackRuleAttribute(java.io.FileNotFoundException.class)));
var tieCommitFirst = new RuleBasedTransactionAttribute();
tieCommitFirst.setRollbackRules(List.of(new NoRollbackRuleAttribute(java.io.FileNotFoundException.class),
new RollbackRuleAttribute(java.io.FileNotFoundException.class)));
if (!tieRollbackFirst.rollbackOn(new java.io.FileNotFoundException("x")) || tieCommitFirst.rollbackOn(new java.io.FileNotFoundException("x")))
throw new AssertionError("same-depth tie must resolve by rule registration order");
System.out.println("PASS tx-u18 rollback-rules depthWins=tieByRegistrationOrder uncheckedDefault=true checkedDefault=commit");
}
}高级答法与架构师变化条件
高级答案应贯通:"同步器和事务事件都是同步域挂件,注册前提与异常路由不同——beforeCommit 上抛回滚、完成相吞错、afterCommit 上抛已提交;编程式模板与声明式共用管理器但回滚判定只看 RuntimeException/Error;传播矩阵两侧的关键分支在 getTransaction 与 handleExistingTransaction,拒绝型行为让误配显式失败;保存点与挂起是同一物理事务上的位置标记和上下文换入换出,不是两个事务。"
订单域要加"支付成功后发通知":候选一 AFTER_COMMIT 事件监听器发 MQ——同 JVM 内时点正确,但 MQ 失败既不回滚也不重试,需要接受丢失或另加恢复;候选二同事务写 Outbox 表由投递器轮询——多一次本地写和扫描,换来可重试的持久意图。事件适合"丢了可容忍"的场景(缓存失效、审计计数),Outbox 适合"丢了要追责"的场景(账务、履约)。评审产物:事件清单表——每列一个事件,写明 phase、condition、失败后语义(丢失/日志/补偿)、监听器执行时长预算(afterCommit 在连接释放前执行,长任务应转异步池)。若单库读写比 8:2 且 200 QPS,把报表查询从 REPEATABLE READ 降到 READ COMMITTED 可减少快照长度,但库存扣减路径必须保持可串行化校验或乐观锁——隔离降级的收益算例(锁等待下降)与代价(丢失更新概率)都要标注为推演非实测。挂起层次超过 2 层(A→B→C 连环 REQUIRES_NEW)时连接需求按每线程 3 个估算,池公式复用 T14 的 P≥T×(C−1)+1 推演。验证清单:同步回调时序断言、事件 phase 矩阵、传播拒绝两向、挂起连接身份——都要求 H2 行数或身份断言,不接受"看起来执行了"。
闭卷验收
本页 9 道闭卷题收进了下方自测面板:先回忆,再展开参考答案与评分点,最后给三档自评。