惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

Stack Overflow Blog
Stack Overflow Blog
L
LangChain Blog
人人都是产品经理
人人都是产品经理
酷 壳 – CoolShell
酷 壳 – CoolShell
T
Tailwind CSS Blog
N
Netflix TechBlog - Medium
Microsoft Security Blog
Microsoft Security Blog
J
Java Code Geeks
博客园 - 【当耐特】
量子位
有赞技术团队
有赞技术团队
Jina AI
Jina AI
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
博客园_首页
C
Check Point Blog
B
Blog RSS Feed
M
MIT News - Artificial intelligence
H
Help Net Security
雷峰网
雷峰网
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
博客园 - 聂微东
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
A
About on SuperTechFans
腾讯CDC

博客园 - buguge

交易成功还要记 3 笔账?我们直接砍到 1 笔—— 网络红包记账的一次自顶向下业务层重构 分享一次针对hardcode的小重构:不要把 package 名写死在代码里 聚合系统设计:如何为银行代付通道抽象出公共的代付接口能力 如何自定义 MyBatis枚举处理器(EnumTypeHandler):分享从 “同包同名 shadow 覆盖” 到 “官方扩展点” 的重构经历 这儿也改状态,那儿也改状态,连AI都理不清楚了… 不妨试试这个状态机good-practice 业务解耦的经典实践:从订单表解耦开票业务说起(以滴滴网约车开票为例) 从 `int` 到 `Duration`:一个缓存 API 的三次演进教会我的事 最差实践(bad-practice):开发者在方法里直接实例化线程池对象,然后...(应用gg了) 【HttpClient最差实践(bad-practice)】开发者在 http 工具方法中直接实例化 HttpClient,然后…… Crypto、Cipher与Password:Java加密开发的三个核心概念 知识VS技能:如何优雅判空? 20260604SR超时问题排查 推敲见文章:从 `try..catch` 看异常日志打印的正确姿势 #解决问题要彻底# 慢SQL治理完成后,如何防止同类问题“死灰复燃”? 从合同甲方是荒谬的“JD”谈起:软件开发不应遗忘的“常识” 别留小尾巴/尽快剪掉小尾巴:从一次“ABA”字段重命名,谈谈“解决问题要彻底” 常见的OOM错误 ( OutOfMemoryError全类型详解) 开发者暴露了一个无需授权访问的裸接口,我问:如果有人暴力请求怎么办? 【SQL性能优化篇】有了!治理慢SQL“WHERE create_time ORDER BY id”的良药---规避“Using filesort”性能杀手 高效查询商户日终余额:一个SQL的优化实践 Hutool 的 `TimedCache` 到期会自动清理吗? ——————hutool cache的"惰性清理"和"定期清理" Fastjson枚举反序列化:当字符串不是枚举常量名时,会发生什么? fastjson-EnumDeserializer类及源码分析 随笔20260309:我们都是围城里的人 认识2个单词:goal/target —————— 为什么Maven是 "goal" 而不是 "target"? 聚合系统设计:策略模式(Strategy Pattern)在银行通道对接场景中的应用 这样构建对象,太帅了!—— 阶梯式Builder模式与代码整洁之道 注意!字段数据类型不匹配,这个sql会很慢 还在用ArrayList?用HashSet吧!--性能对比 分页查询还在用create_time去做降序?
`UnexpectedRollbackException: Transaction rolled back bec...
buguge · 2026-03-09 · via 博客园 - buguge

🔍 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();如果业务相对简单,使用声明式事务+异常驱动更为简洁。