项目内有一个api方法(称之为 ServiceA.methodA() )需要调用一个共通service里的方法(称之为 CommonService.methodB() ),然后由于业务需求需要catch住 CommonService.methodB() 的异常并返回response。

由于项目在config里通过切面配置了对于所有的 @Service 都设置了默认事务为REQUIRED,并且设置了对所有异常都回滚(通过 TransactionInterceptor 和 NameMatchTransactionAttributeSource ),导致如果直接使用 CommonService.methodB() 的话就会因为spring的机制导致抛出UnexpectedRollbackException。

这个机制具体来说,抛出异常时spring会检测到并将当前事务内部的 rollbackOnly 这样一个flag设为true。但是又因为catch住了异常,导致最后结束方法提交事务时spring发现这个flag为true却仍然要提交,于是兜底性的回滚了并抛出UnexpectedRollbackException。

于是我想到了新建事务执行,而项目内正好有一个空壳方法 Helper.execute(Suppiler) 上面加了REQUIRES_NEW,只需代入方法即可。

1
2
3
4
5
try {
result = Helper.execute(() -> commonService.methodB());
catch (Exception e) {
return response;
}

但测试后发现仍然还是会抛出UnexpectedRollbackException。这就要具体分析整个流程了:

  1. Api (ServiceA) 调用 Helper.execute(),此时环境是 Tx1,对应拦截器(指 TransactionInterceptor )是拦截器1(用的是 NameMatchTransactionAttributeSource )。
  2. Helper 上有 @Service ,拦截器2介入(用的是 NameMatchTransactionAttributeSource ):
    1. 它看到ServiceA有Tx1,
    2. 它配置默认使用 REQUIRED。
    3. 所以它让 Helper 加入了 Tx1。
  3. Helper.execute 上有 @Transactional(propagation = Propagation.REQUIRES_NEW) ,拦截器3介入(用的是 AnnotationTransactionAttributeSource ):
    1. 它看到 REQUIRES_NEW。
    2. 它调用 tm.suspend(Tx1) ,挂起 Tx1。
    3. 它调用 tm.begin() ,开启 Tx2。
  4. 执行业务:
    1. CommonService 报错,抛出 BusinessRuntimeException。
  5. 拦截器3捕获异常:
    1. 它持有 Tx2。
    2. 它执行 tm.rollback(Tx2) ,Tx2干净地回滚了。
    3. 它执行 tm.resume(Tx1),Tx1恢复了。
    4. 关键动作:它必须把异常继续往外抛(因为 AOP 默认行为就是处理完事务抛出异常)。
    5. 抛出 MyBusinessRuntimeException。
  6. 拦截器2捕获异常:
    1. 它接到了注解拦截器抛出来的异常。
    2. 它持有 Tx1 (因为它之前加入了 Tx1)。
    3. 它检查回滚规则:RuntimeException -> 需要回滚。
    4. 它执行 tm.rollback(Tx1) 这里的逻辑。
    5. 但是因为它只是一个“参与者”(Propgation.REQUIRED),它没有资格直接物理回滚 Tx1,它只能通知上级(也就是拦截器1),通过 status.setRollbackOnly()。
    6. 结果:Tx1 被打上了必须回滚的标签。
    7. 它继续把异常抛给 Api。
  7. 回到 Api (ServiceA):
    1. Catch 住了异常。
    2. Api 觉得“没问题了”,方法正常结束。
    3. Spring 试图 tm.commit(Tx1)。
    4. 发现 Tx1 身上有全局拦截器贴的 RollbackOnly 标签。
    5. 抛出异常 UnexpectedRollbackException。

关于参与者,是因为Spring 事务管理遵循一个基本原则:“谁开启,谁负责(物理)提交/回滚”。只有最外层开启事务的方法,才持有真正的数据库连接(Connection),才有资格调用 JDBC 的 con.rollback() 或 con.commit()。具体在spring源码中可以看到:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
// org.springframework.transaction.support.AbstractPlatformTransactionManager

private void processRollback(DefaultTransactionStatus status, boolean unexpected) {
try {
boolean unexpectedRollback = unexpected;

try {
triggerBeforeCompletion(status);

// 1. 【我是老大吗?】
// 检查当前是不是"新事务"(即我是不是这个事务的开启者)
if (status.hasSavepoint()) {
// 如果有保存点,回滚到保存点
status.rollbackToHeldSavepoint();
}
else if (status.isNewTransaction()) {
// 【是的,我是老大!】
// 只有在这里,才会执行真正的物理回滚!
// 对应 JDBC 的 connection.rollback()
doRollback(status);
}
else {
// 【不是,我只是个小弟(参与者)!】
// 对应 PROPAGATION_REQUIRED 加入了别人的事务
if (status.hasTransaction()) {
// 2. 【打小报告逻辑】
// 如果配置了"参与者失败要全局回滚"(默认是 true)
if (status.isLocalRollbackOnly() || isGlobalRollbackOnParticipationFailure()) {
// 执行"标记回滚"
// 这只是把内存里的一个 boolean 变量设为 true
doSetRollbackOnly(status);
}
}
}
}
catch (RuntimeException | Error ex) {
triggerAfterCompletion(status, TransactionSynchronization.STATUS_UNKNOWN);
throw ex;
}

triggerAfterCompletion(status, TransactionSynchronization.STATUS_ROLLED_BACK);

// ...
}
}

那如果把catch放进supplier里呢?

1
2
3
4
5
6
7
result = Helper.execute(() -> {
try {
return commonService.methodB();
catch (Exception e) {
return response;
}
});

遗憾的是还是一样的结果。再分析下过程:

  1. Api (ServiceA) 调用 Helper.execute(),此时环境是 Tx1,对应拦截器是拦截器1(用的是 NameMatchTransactionAttributeSource )。
  2. Helper 上有 @Service ,拦截器2介入(用的是 NameMatchTransactionAttributeSource ):
    1. 它默认使用 REQUIRED。
    2. 它看到 ServiceA 有Tx1。
    3. 所以它让 Helper 加入了 Tx1。
  3. Helper.execute 上有 @Transactional(propagation = Propagation.REQUIRES_NEW) ,拦截器3介入(用的是 AnnotationTransactionAttributeSource ):
    1. 它看到 REQUIRES_NEW。
    2. 它调用 tm.suspend(Tx1) ,挂起 Tx1。
    3. 它调用 tm.begin() ,开启 Tx2。
  4. CommonService 也有 @Service,拦截器4介入(用的是 NameMatchTransactionAttributeSource )
    1. 它默认使用 REQUIRED。
    2. 它看到上一层有个Tx2。
    3. 所以它让 CommonService 加入了Tx2。
  5. CommonService 报错,抛出 BusinessRuntimeException。
    1. 拦截器4捕获到异常。
    2. 拦截器4发现自己是“参与者”(Joined Tx2)。
    3. 所以它执行 setRollbackOnly() 。
    4. Tx2 虽然还活着,但被打上了“必须回滚”标签。
  6. lambda代码里catch住异常,返回结果。
  7. 回到 Helper.execute 所在的拦截器3:
    1. 它看到 Lambda 执行结束了。
    2. 它看到没有抛出异常(被lambda吞了)。
    3. 它判断:“嗯,业务执行成功,没有异常。”
    4. 于是它决定执行 提交(Commit)Tx2。
  8. 事务管理器:
    1. 收到 Commit 指令。
    2. 检查 Tx2 状态。
    3. 发现:isRollbackOnly == true(步骤 5 里被打的标签)。
    4. 判定:“你让我提交一个明明标记了回滚的事务?”
    5. 结果:抛出 UnexpectedRollbackException。

如果要验证也很简单,在外层再套一个try catch然后打印堆栈即可发现Helper.execute()会抛出个UnexpectedRollbackException。


解决办法其实很简单:

1
2
3
4
5
6
7
8
result = Helper.execute(() -> {
try {
return commonService.methodB();
catch (Exception e) {
TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
return response;
}
});

这个方法设的rollback-only并非和事务管理器自动设的rollback-only是同一个,前者在源码里称为localRollbackOnly,后者是globalRollbackOnly。而源码中会先判断如果localRollbackOnly为true的话就直接回滚,不会抛UnexpectedRollbackException。

这个代码在 AbstractPlatformTransactionManager.commit() 里:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public final void commit(TransactionStatus status) throws TransactionException {  
if (status.isCompleted()) {
throw new IllegalTransactionStateException("Transaction is already completed - do not call commit or rollback more than once per transaction");
}

DefaultTransactionStatus defStatus = (DefaultTransactionStatus) status;
// 校验是否需要回滚,这个是我们手动设或者正常抛出异常的
if (defStatus.isLocalRollbackOnly()) {
if (defStatus.isDebug()) {
logger.debug("Transactional code has requested rollback");
}
processRollback(defStatus, false);
return;
}
// 判断事务管理器自动设的rollback only
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;
}
// 2、提交事务
processCommit(defStatus);
}

当然另外一种解决办法就是使用编程式事务:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
private final TransactionTemplate transactionTemplate;  // 构造器注入,此处略

public <T> T Result<T> execute(Supplier<T> supplier) {
transactionTemplate.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW);
try {
T data = transactionTemplate.execute(status -> supplier.get());
return new Result<>(data, null);
} catch (Exception e) {
return new Result<>(null, e);
}

// Result.java
public class Result<T> {
private final T data;
private final Exception exception;

public boolean hasException() {
return exception != null;
}
}

// 调用
Result<String> result = Helper.execute(() -> commonService.methodB());
if (result.hasException()) {
return response;
}
String data = result.getData();

但是要注意的是,不能把catch写在 transactionTemplate.execute 里面,否则内部的事务仍然会和前面一样把rollbackOnly设成true然后正常结束试图commit,结果导致UnexpectedRollbackException。

为什么这个catch写在 transactionTemplate.execute 外面了却没有之前那样抛UnexpectedRollbackException呢?主要还是因为它没有前面那么多层拦截器切面干涉:

  1. Api (ServiceA) 调用 Helper.execute(),此时环境是 Tx1,对应拦截器是拦截器1(用的是 NameMatchTransactionAttributeSource )。
  2. Helper.execute 上有 @Service ,拦截器2介入(用的是 NameMatchTransactionAttributeSource ),同时加入 Tx1。(此时它在监控 Tx1)
  3. 进入目标方法。
  4. 执行 transactionTemplate.execute :
    1. 代码内部 挂起 Tx1,开启 Tx2。
    2. 异常抛出。
    3. transactionTemplate 内部 回滚 Tx2。
    4. transactionTemplate 内部 恢复 Tx1。
    5. transactionTemplate 重新抛出异常。
  5. try-catch :
    1. 在异常碰到拦截器 2 之前,就把它吞了!
    2. 转换成了一个正常的返回值对象。
  6. 方法返回。
  7. 拦截器 2:
    1. 看到方法正常返回了对象,没有抛出异常。
    2. 它就不会去执行 setRollbackOnly。
    3. Tx1 安全存活。

也就是说编程式事务能够正常将Tx1挂起(suspend)再恢复(resume),这使得在Tx1醒过来之前我们就已经把异常处理掉了,Tx1醒来过后就看不到异常了。