buguge - Keep it simple,stupid

知识就是力量,但更重要的,是运用知识的能力why buguge?

导航

`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 的出现是因为:

  1. 矛盾的状态:事务被标记为回滚,但方法正常返回
  2. 设计原则:Spring 的事务管理是基于异常的

setRollbackOnly() 不仅适用于编程式事务,也适用于声明式事务,但:

  1. 在编程式事务中:是主要且推荐的回滚控制方式

    • 通过 TransactionStatus 参数直接访问
    • 代码清晰,事务边界明确
    • 不会产生 UnexpectedRollbackException
  2. 在声明式事务中:是备选方案。通常建议使用异常驱动回滚。

    • 需要通过 TransactionAspectSupport.currentTransactionStatus() 获取
    • 容易产生 UnexpectedRollbackException
    • 在声明式事务中,如果使用 setRollbackOnly(),应该立即返回或抛出异常。

推荐策略

  • 简单业务 → 声明式事务 + 异常驱动
  • 复杂业务 → 编程式事务 + setRollbackOnly()
  • 避免在声明式事务中混用正常返回和 setRollbackOnly()

我的实践建议:如果业务需要精细的事务控制,考虑使用编程式事务+ setRollbackOnly();如果业务相对简单,使用声明式事务+异常驱动更为简洁。

posted on 2026-03-09 20:42  buguge  阅读(1)  评论(0)    收藏  举报