StackTraces
Spring Boot已验证

Spring Boot 报 UnexpectedRollbackException

@debug_master27 天前更新阅读 6 min00

UnexpectedRollbackException: Transaction rolled back because it has been marked as rollback-only 通常不是业务根因,而是哨兵信号。在默认 REQUIRED 传播机制下,内层方法抛出异常时 Spring 已将底层共享物理事务标记为 rollback-only;即便外层方法将该异常 try-catch 吞掉并尝试提交,事务管理器在提交阶段也会校验该标记并强制回滚,最终抛出哨兵异常。排查该问题的关键在于向上追溯日志中标记 rollback-only 的前一帧调用链。

Issue & Env(案发现场与环境)

环境配置

  • Spring Boot: 2.7.x / 3.x
  • Spring Framework: 5.3.x / 6.x
  • 事务管理: DataSourceTransactionManager / JpaTransactionManager

业务场景

业务层通常包含多步骤协同逻辑(例如创建订单并赠送积分、发放优惠券)。开发人员为了提高主链路可用性,常常对外围次要业务进行“容错降级”处理,期望即便次要逻辑失败,主逻辑仍然能够正常提交:

外层 @Transactional(REQUIRED)
  → 开启物理事务,绑定底层连接 Connection A
  → 执行主业务逻辑(插入订单)
  → 调用内层 @Transactional(REQUIRED,默认传播行为)
      → 加入同一物理事务(复用 Connection A)
      → 内层逻辑异常(如积分服务不可用)
      → TransactionInterceptor 捕获异常,将 Connection A 标记为 rollback-only,并向外抛出
  → 外层使用 try-catch 拦截并吞掉内层异常,方法正常返回
  → 事务拦截器完成调用,触发 Commit 操作
  → 事务管理器检测到 rollback-only 标记
      → 无法提交,强制回滚并抛出 UnexpectedRollbackException

在默认的 REQUIRED 传播机制下,内外层逻辑共享同一个底层物理数据库连接。只要任何内层逻辑触发了回滚条件,整个物理事务就被“污染”,外层的单纯 catch 无法撤销底层连接已设置的只回滚状态。

StackTrace(堆栈日志)

运行时报错并非内层抛出的具体业务异常,而是事务管理器在提交时抛出的异常:

org.springframework.transaction.UnexpectedRollbackException: Transaction rolled back because it has been marked as rollback-only
	at org.springframework.transaction.support.AbstractPlatformTransactionManager.processCommit(AbstractPlatformTransactionManager.java:797)
	at org.springframework.transaction.support.AbstractPlatformTransactionManager.commit(AbstractPlatformTransactionManager.java:734)
	at org.springframework.transaction.interceptor.TransactionAspectSupport.commitTransactionAfterReturning(TransactionAspectSupport.java:654)
	at org.springframework.transaction.interceptor.TransactionAspectSupport.invokeWithinTransaction(TransactionAspectSupport.java:407)
	at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:119)
	at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)
	at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:753)
	at com.example.service.OrderService$$EnhancerBySpringCGLIB$$8b6b249a.createOrder(<generated>)

开启 Spring 事务调试日志后,可以观察到关键的状态流转时序证据:

o.s.t.i.TransactionInterceptor : Completing transaction for [com.example.service.PointService.gift] after throwing: java.lang.RuntimeException: point service unavailable
o.s.t.i.TransactionInterceptor : Participating transaction failed - marking existing transaction as rollback-only
o.s.t.i.TransactionInterceptor : Completing transaction for [com.example.service.OrderService.createOrder] after return: null
o.s.jdbc.datasource.DataSourceTransactionManager : Committing transaction
o.s.t.i.TransactionInterceptor : Transaction rolled back because it has been marked as rollback-only

日志明确显示:内层 PointService.gift 抛出异常后触发了 Participating transaction failed - marking existing transaction as rollback-only;而外层 OrderService.createOrder 吞掉异常后以 after return: null 正常返回,驱动事务提交,从而在提交阶段遭遇冲突。

Reproduce(复现代码)

以下是典型的内外层嵌套事务错误容错写法:

package com.example.service;

import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Slf4j
@Service
public class OrderService {

    @Autowired
    private OrderMapper orderMapper;
    @Autowired
    private PointService pointService;

    @Transactional
    public void createOrder(Order order) {
        orderMapper.insert(order);

        try {
            // 内层方法使用默认的 Propagation.REQUIRED,加入当前物理事务
            pointService.gift(order.getUserId(), 100);
        } catch (Exception e) {
            // 误区:试图在此处吞掉异常,继续向下执行并期望订单成功提交
            log.warn("积分赠送失败,降级忽略并继续下单流程", e);
        }

        orderMapper.updateOrderStatus(order.getId(), "CREATED");
    }
}

@Service
public class PointService {

    @Autowired
    private PointMapper pointMapper;

    @Transactional
    public void gift(Long userId, int point) {
        pointMapper.insertPointRecord(userId, point);
        // 模拟内层抛出运行时异常
        throw new RuntimeException("point service unavailable");
    }
}

Root Cause(根因分析)

1. Spring 嵌套事务机制设计原理

在 AbstractPlatformTransactionManager 中,内层逻辑通过 REQUIRED 传播行为加入已存在的事务时,Spring 将其视为同一个物理事务的一个“逻辑参与者”(Participating Transaction)。

当内层逻辑抛出未捕获的运行时异常时,AOP 拦截器 TransactionAspectSupport.completeTransactionAfterThrowing() 会被触发。此时因为当前不是最外层事务(!status.isNewTransaction()),事务管理器无法执行直接回滚,而是调用 setRollbackOnly():

// AbstractPlatformTransactionManager.java 核心逻辑片段
if (status.isNewTransaction()) {
    doRollback(status);
} else if (status.hasTransaction()) {
    if (status.isLocalRollbackOnly() || isGlobalRollbackOnParticipationFailure()) {
        doSetRollbackOnly(status); // 标记全局连接 rollback-only
    }
}

2. 为什么不可盲目提交?

Spring 的这一设计是为了维护 ACID 中的原子性(Atomicity)。如果允许外层 catch 吞掉内层事务失败并照常提交,就会导致内层方法修改的数据或者执行失败的状态在数据层面出现不一致(或者部分生效部分丢失)。

3. 定位排查手段

排查此类问题的核心在于“抓出是哪一个内层方法首次标记了 rollback-only”以及“哪一个外层方法吞掉了异常”:

  1. 开启事务层日志级别: 在 application.properties 或 logback-spring.xml 中配置:
    logging.level.org.springframework.transaction=DEBUG
    logging.level.org.springframework.jdbc.datasource=DEBUG
  2. 检索异常调用链与捕获点: 在日志中逆向搜索 Participating transaction failed,定位引发回滚的具体内层业务类与方法名。 在源码中排查潜在吞掉异常的拦截点:
    # 检索 Service 层中包裹内层调用的 catch 块
    grep -rn -B1 "catch (Exception" src/main/java --include="*Service*"

The Fix(解决方案)

方案 A(推荐):消除吞异常逻辑,保持事务原子性

如果业务要求订单与积分必须强一致,最清晰的做法是移除外层的 try-catch 拦截,让异常自然向外层冒泡,由最外层事务统一触发完整回滚:

@Transactional
public void createOrder(Order order) {
    orderMapper.insert(order);
    // 移除 try-catch,保持整体事务原子性,失败则全量回滚
    pointService.gift(order.getUserId(), 100);
    orderMapper.updateOrderStatus(order.getId(), "CREATED");
}

方案 B:确需失败容错时,为内层配置独立事务(REQUIRES_NEW)

如果业务明确允许“赠送积分失败不影响主订单创建”,内层方法必须声明为 REQUIRES_NEW,挂起当前事务并启用独立的数据库物理连接进行提交/回滚:

@Service
public class PointService {

    @Autowired
    private PointMapper pointMapper;

    // 使用独立的物理事务,内层回滚不会污染外层的 Connection
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void gift(Long userId, int point) {
        pointMapper.insertPointRecord(userId, point);
        throw new RuntimeException("point service unavailable");
    }
}
@Service
public class OrderService {

    @Transactional
    public void createOrder(Order order) {
        orderMapper.insert(order);
        try {
            // 内层走独立事务,内层回滚后外层 catch 能够正常容错且不影响外层提交
            pointService.gift(order.getUserId(), 100);
        } catch (Exception e) {
            log.warn("赠送积分失败,降级忽略", e);
        }
        orderMapper.updateOrderStatus(order.getId(), "CREATED");
    }
}

注意:使用 REQUIRES_NEW 时外层和内层各持有一个数据库连接,高并发场景下需评估数据库连接池承载能力,防止连接池耗尽死锁。

方案 C:异步解耦(基于事务同步器或事件驱动)

如果赠送积分属于非核心旁路逻辑,更优解是解耦到事务提交后异步执行:

@Transactional
public void createOrder(Order order) {
    orderMapper.insert(order);
    orderMapper.updateOrderStatus(order.getId(), "CREATED");

    // 事务成功提交后再异步触发积分赠送,彻底规避事务耦合
    TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
        @Override
        public void afterCommit() {
            pointAsyncExecutor.giftAsync(order.getUserId(), 100);
        }
    });
}

避坑总结

  • UnexpectedRollbackException 是提交阶段的“受害者提示”,真正的“案发现场”在日志中首次出现 Participating transaction failed - marking existing transaction as rollback-only 的地方。
  • 在 REQUIRED 传播机制下,内外层共享同一个底层连接,try-catch 无法清除已经存在的 rollback-only 标记。
  • 需要部分容错时,必须使用 REQUIRES_NEW 独立事务或基于 afterCommit 事件进行解耦。

References(参考来源)

最后更新于 2026-09-14

这篇帮到你了吗?

刚解决了一个棘手的报错?花两分钟记录下来,帮助下一个遇到同样问题的开发者。

贡献一条解法