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 的独立性是双向的:
- 持久化不可逆:内层事务先行提交后,数据库底层已经完成写盘与锁释放,无法通过外层回滚进行数据撤销。
- 读未提交不可见:内层事务在新连接中执行,默认隔离级别(如 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);
}
}
总结与避坑要点
REQUIRES_NEW独立提交是双刃剑,它不是解决回滚问题的万能补丁,往往是引入数据不一致的源头。- 外层回滚无法撤回已提交的内层事务,这是 JDBC 与数据库 ACID 的基本规律而非框架 Bug。
- Code Review 时,对所有标记为
REQUIRES_NEW的方法必须追问:“外层回滚时,这里的持久化数据单独保留是否合法?”。
