`UnexpectedRollbackException: Transaction rolled back because it has been marked as rollback-only` 异常解析
🔍 transactionStatus.setRollbackOnly()引起UnexpectedRollbackException异常
1. 先看下面代码
@Transactional
public Result<Void> updateChannelLevy(ChannelLevyDTO dto) {
// ... 前置操作
boolean updated = update(dto);
if (!updated) { // 假设 update 返回失败
// 1. 手动将事务标记为回滚
TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
// 2. 返回错误结果(注意:没有抛出异常!)
return Result.error("操作失败");
}
return Result.success();
// 3. 方法正常结束,Spring 会尝试提交事务
}
执行上面代码,在update为false的情况下,程序会抛出异常:UnexpectedRollbackException: Transaction rolled back because it has been marked as rollback-only
stacktrace:
org.springframework.transaction.UnexpectedRollbackException: Transaction rolled back because it has been marked as rollback-only
at org.springframework.transaction.support.AbstractPlatformTransactionManager.processRollback(AbstractPlatformTransactionManager.java:870)
at org.springframework.transaction.support.AbstractPlatformTransactionManager.commit(AbstractPlatformTransactionManager.java:707)
at org.springframework.transaction.interceptor.TransactionAspectSupport.commitTransactionAfterReturning(TransactionAspectSupport.java:632)
at org.springframework.transaction.interceptor.TransactionAspectSupport.invokeWithinTransaction(TransactionAspectSupport.java:386)
at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:118)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)
at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:749)
at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:691)
at com.levy.backendapi.levy.LevyService$$EnhancerBySpringCGLIB$$80203775.updateChannelLevy(<generated>)
症结在于其中所调用的setRollbackOnly()方法。
🔧 Spring 事务管理的设计哲学
1. 基于异常的回滚机制
Spring 的事务管理是基于异常的:
- 抛出
RuntimeException→ 自动回滚 - 正常返回 → 自动提交
- 这是一个约定优于配置的设计
2. setRollbackOnly() 的作用
transactionStatus.setRollbackOnly();这个方法的真正含义是:
标记事务必须回滚(无论之后发生什么)。
3. Spring 事务的处理流程
Spring事务管理的源码,可查看 TransactionAspectSupport 类中的 invokeWithinTransaction() 方法,其中检查回滚标记并抛出 UnexpectedRollbackException的逻辑在 AbstractPlatformTransactionManager.commit() 方法中。示意代码见下方:
// Spring 事务管理的伪代码逻辑
try {
// 开启事务
// 执行业务方法
Object result = method.invoke(...);
// 如果事务被标记为回滚,抛出 UnexpectedRollbackException
if (transactionStatus.isRollbackOnly()) {
throw new UnexpectedRollbackException("Transaction rolled back...");
}
// 否则提交事务
commitTransaction();
} catch (RuntimeException e) {
// 如果抛出 RuntimeException,回滚事务
rollbackTransaction();
}
从源码可知,当方法正常返回(没有抛出异常)时,Spring 会尝试提交事务,如果发现事务已被标记为回滚,则会导致 UnexpectedRollbackException。
上面的Java方法向 Spring 发送了两个矛盾的信号:
| 信号 | 含义 | 代码行为 |
|---|---|---|
| 返回正常结果 | 告诉 Spring:"业务执行成功,可以提交事务" | 返回 Result 对象(没有异常) |
| 标记回滚 | 告诉 Spring:"需要回滚事务" | 调用 setRollbackOnly() |
而Spring 的困惑是:
- 方法正常返回(没有抛出异常)→ 应该提交事务
- 但事务被标记为回滚 → 不能提交事务
于是,Spring 选择抛出一个 UnexpectedRollbackException 来报告这个矛盾。
💡 正确的解决方案
方案1:抛出异常,让 Spring 自动回滚。由调用方处理异常(推荐)
@Transactional
public Result<Void> updateChannelLevy(ChannelLevyDTO dto) {
boolean updated = update(dto);
if (!updated) {
// 抛出异常,Spring 自动回滚
throw new BusinessException("操作失败: " + result.getMsg());
}
return Result.success();
}
外层调用方:
try {
Result<Void> result = levyService.updateChannelLevy(dto);
return result;
} catch (BusinessException e) {
// 转换异常为业务结果
return Result.error(e.getMessage());
}
方案2:使用编程式事务管理
public Result<Void> updateChannelLevy(ChannelLevyDTO dto) {
return transactionTemplate.execute(status -> {
boolean updated = update(dto);
if (!updated) {
status.setRollbackOnly(); // 标记回滚
return Result.error("操作失败"); // 返回错误结果
}
return Result.success();
});
}
** 两种事务管理方式对比:编程式事务(TransactionTemplate 或 PlatformTransactionManager) vs 声明式事务(@Transactional 注解)
复杂业务,需要细粒度控制事务的,可采用编程式事务。 简单业务操作,直接使用声明式事务即可,代码更简洁。
@Service
public class TransactionServiceDemo {
@Autowired
private TransactionTemplate transactionTemplate;
public Result<Void> processing() {
return transactionTemplate.execute(status -> {
// 步骤1
if (!performStep1()) {
status.setRollbackOnly();
return Result.error("步骤1失败");
}
// 步骤2
try {
performStep2();
} catch (BusinessException e) {
status.setRollbackOnly();
throw e; // 重新抛出异常
}
return Result.success();
});
}
}
🎯 总结:声明式事务管理中应慎用setRollbackOnly(),避免UnexpectedRollbackException异常
UnexpectedRollbackException: Transaction rolled back because it has been marked as rollback-only 的出现是因为:
- 矛盾的状态:事务被标记为回滚,但方法正常返回
- 设计原则:Spring 的事务管理是基于异常的
setRollbackOnly() 不仅适用于编程式事务,也适用于声明式事务,但:
-
在编程式事务中:是主要且推荐的回滚控制方式
- 通过
TransactionStatus参数直接访问 - 代码清晰,事务边界明确
- 不会产生
UnexpectedRollbackException
- 通过
-
在声明式事务中:是备选方案。通常建议使用异常驱动回滚。
- 需要通过
TransactionAspectSupport.currentTransactionStatus()获取 - 容易产生
UnexpectedRollbackException - 在声明式事务中,如果使用 setRollbackOnly(),应该立即返回或抛出异常。
- 需要通过
推荐策略:
- 简单业务 → 声明式事务 + 异常驱动
- 复杂业务 → 编程式事务 +
setRollbackOnly() - 避免在声明式事务中混用正常返回和
setRollbackOnly()
我的实践建议:如果业务需要精细的事务控制,考虑使用编程式事务+ setRollbackOnly();如果业务相对简单,使用声明式事务+异常驱动更为简洁。
当看到一些不好的代码时,会发现我还算优秀;当看到优秀的代码时,也才意识到持续学习的重要!--buguge
本文来自博客园,转载请注明原文链接:https://www.cnblogs.com/buguge/p/19694025
浙公网安备 33010602011771号