StackTraces
Spring Boot已验证

Spring Boot REQUIRES_NEW 内层提交致外层回滚脏数据

@debug_master27 天前更新阅读 6 min00

REQUIRES_NEW 的语义是「挂起外层事务、开一个全新的事务,互不干扰」——独立是双向的:外层回滚救不了已提交的内层,内层也看不到外层未提交的数据。把主业务数据放进 REQUIRES_NEW,就是亲手制造“部分提交”的脏数据。

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

  • 技术栈:Spring Boot 2.x / 3.x、Spring Framework 声明式事务(@Transactional(propagation = Propagation.REQUIRES_NEW))
  • 业务场景:在主订单流程中,调用内部某服务记录操作日志、审计轨迹或发放附属资产。开发者将内层方法标记为 REQUIRES_NEW,期望子调用独立完成。
  • 故障特征:当下游业务发生未处理异常导致主事务发生回滚时,外层的主订单数据回滚消失,而内层通过 REQUIRES_NEW 插入的记录却已真实提交并永久留存,形成了业务上不可接受的孤儿脏数据。

StackTrace(堆栈日志)

该问题在业务逻辑上体现为数据不一致,代码可能抛出业务异常或正常报错。开启 Spring 事务 DEBUG 级别日志后可清晰观察到事务的生命周期分歧:

logging.level.org.springframework.jdbc.datasource.DataSourceTransactionManager=DEBUG

关键执行轨迹如下:

// 1. 外层开启主事务
2026-09-14 10:20:01 DEBUG o.s.j.d.DataSourceTransactionManager : Creating new transaction with name [OrderService.createOrder]

// 2. 遇到 REQUIRES_NEW:挂起外层事务,申请新连接创建全新独立事务
2026-09-14 10:20:01 DEBUG o.s.j.d.DataSourceTransactionManager : Suspending current transaction, creating new transaction with name [AuditService.record]
//                                                                ^^^^^^^^^^ 挂起当前事务,开辟新事务

// 3. 内层方法执行完毕:立即真实提交
2026-09-14 10:20:02 DEBUG o.s.j.d.DataSourceTransactionManager : Initiating transaction commit
2026-09-14 10:20:02 DEBUG o.s.j.d.DataSourceTransactionManager : Resuming suspended transaction after completion of inner transaction

// 4. 外层后续抛出异常:主事务触发回滚,但已无法追溯已提交的内层数据
2026-09-14 10:20:03 DEBUG o.s.j.d.DataSourceTransactionManager : Initiating transaction rollback
2026-09-14 10:20:03 DEBUG o.s.j.d.DataSourceTransactionManager : Rolling back JDBC transaction on Connection [...]

日志标志:Suspending current transaction, creating new transaction 是判定事务被切分为两段不同物理连接的铁证。

Reproduce(复现代码)

@Service
public class OrderService {

    @Autowired
    private OrderMapper orderMapper;

    @Autowired
    private AuditService auditService;

    @Autowired
    private InventoryService inventoryService;

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

        // 调用 REQUIRES_NEW 的独立事务
        auditService.record(order, "CREATE_ORDER");

        // 模拟后续业务抛出未捕获异常
        inventoryService.deduct(order.getSkuId(), order.getCount());
        throw new RuntimeException("库存扣减异常,主事务触发回滚");
    }
}

@Service
public class AuditService {

    @Autowired
    private AuditMapper auditMapper;

    // 错误地将与主业务强相关的操作配置为 REQUIRES_NEW
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void record(Order order, String action) {
        auditMapper.insert(new AuditLog(order.getId(), action));
    }
}

执行后数据库状态:

  • orders 表中无新记录(已回滚)。
  • audit_log 表中出现了一条指向不存在订单 ID 的孤立记录(已提前提交)。

Root Cause(根因分析)

Spring 事务传播行为中 REQUIRED 与 REQUIRES_NEW 的根本分水岭在于物理数据库连接(Connection)的生命周期与隔离:

REQUIRED(默认):
  当前存在事务 → 直接加入,全流程共享同一数据库物理连接与同一个 Commit/Rollback 终点。
  内层方法退出并不触发物理提交。

REQUIRES_NEW:
  当前存在事务 → 挂起当前事务上下文,从连接池借出全新的物理连接,开辟独立事务单元。
  内层方法退出时立即执行物理 commit。
  外层后续无论回滚与否,均无法撤销另一个已经提交的数据库连接所写入的数据。

REQUIRES_NEW 的独立性是双向的:

  1. 持久化不可逆:内层事务先行提交后,数据库底层已经完成写盘与锁释放,无法通过外层回滚进行数据撤销。
  2. 读未提交不可见:内层事务在新连接中执行,默认隔离级别(如 READ COMMITTED)下,内层根本查询不到外层刚插入但尚未提交的脏数据,容易引起二次主外键冲突或查询为空错误。

排查定位手段: 使用 grep 检索工程内所有的 REQUIRES_NEW 声明:

grep -rn "REQUIRES_NEW" src/main/java

针对每一处进行业务语义审视:该方法写入的数据在外层回滚后如果仍然存在,是否会导致系统脏数据。

The Fix(解决方案)

方案 A(推荐):业务数据强一致 → 改回默认 REQUIRED 传播级别

如果写入的数据必须同生共死(例如订单明细、附赠优惠券、业务流转记录),必须保持在同一个事务边界内:

@Service
public class AuditService {
    // 移除 REQUIRES_NEW,继承外层事务
    @Transactional(propagation = Propagation.REQUIRED)
    public void record(Order order, String action) {
        auditMapper.insert(new AuditLog(order.getId(), action));
    }
}

外层一旦异常回滚,内层写入的操作将一并被数据库回滚。

方案 B:确需独立持久化 → 严格隔离为纯旁路记录

如果业务确实属于“无论主业务成功与否,必须留痕(如安全审计、操作轨迹、接口防重日志)”的纯旁路需求,使用 REQUIRES_NEW 是合理的,但不能依赖主业务主键外键关联:

@Service
public class SecurityAuditService {
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void recordAccessLog(String traceId, String action, String payload) {
        // 只记录无外键强约束的无状态日志,明确接受即使主流程失败也会落库的语义
        auditMapper.insert(traceId, action, payload);
    }
}

总结与避坑要点

  1. REQUIRES_NEW 独立提交是双刃剑,它不是解决回滚问题的万能补丁,往往是引入数据不一致的源头。
  2. 外层回滚无法撤回已提交的内层事务,这是 JDBC 与数据库 ACID 的基本规律而非框架 Bug。
  3. Code Review 时,对所有标记为 REQUIRES_NEW 的方法必须追问:“外层回滚时,这里的持久化数据单独保留是否合法?”。

References(参考来源)

最后更新于 2026-09-14

这篇帮到你了吗?

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

贡献一条解法