Spring 事务传播机制深度解析:7 种行为实战

Spring 事务传播机制是日常开发里最容易”以为会用、实际用错”的特性:明明加了 @Transactional,异常却没回滚;两个方法互相调用,事务却莫名合并成一个;想让子方法独立提交,结果父方法一抛异常全没了。这些诡异现象几乎都出在传播行为(Propagation)理解不清上。本文把 7 种传播行为按适用场景讲透,并给出一份踩过坑的实战清单,配合我们之前写过的 Spring Boot 3 升级踩坑实录 一起看,能少走很多弯路。

一、传播行为到底在回答什么问题

当方法 A 已经在一个事务里,调用方法 B 时,B 要不要新开事务?要不要挂起 A 的事务?要不要干脆报错?传播行为定义的正是”被调用方法如何加入或创建事务“。它不是数据库概念,而是 Spring 在 JDBC 事务之上做的代理层规则。理解这一点很关键:传播行为由 Spring 的代理在调用前决定,绕过代理的直接调用(如 this.methodB())不会生效

一个典型翻车现场是:开发者在同一个 Service 里写了 createOrder()deduct(),两个方法都标了 @Transactional,却以为它们是”两个事务”。实际上因为同对象内部调用不走代理,deduct() 上的传播注解被完全忽略,二者天然处于同一事务——想做”扣库存独立提交”从一开始就不可能。这也是为什么本文后续反复强调”调用路径”与”代理生效条件”。

二、7 种传播行为一张表讲清

传播行为已有事务时无事务时典型场景
REQUIRED(默认)加入当前事务新建事务绝大多数业务写操作
REQUIRES_NEW挂起当前,新建独立事务新建事务审计日志、独立计费等必须落库
NESTED在当前事务内建保存点新建事务子步骤可部分回滚
SUPPORTS加入当前事务以非事务方式运行查询方法,可有可无
NOT_SUPPORTED挂起当前事务以非事务方式运行长耗时、不希望持锁的操作
MANDATORY加入当前事务抛 IllegalTransactionStateException必须在事务中被调用的方法
NEVER抛异常以非事务方式运行严禁在事务内调用的只读工具

三、REQUIRED:默认行为为什么最常出问题

REQUIRED 是默认值,最容易被误用。它的语义是”有就用、没有就建”,因此多个 REQUIRED 方法调用会合并到同一个物理事务。一旦其中一个抛异常,整个事务全部回滚:

@Service
public class OrderService {

    @Autowired
    private InventoryService inventoryService;

    @Transactional(propagation = Propagation.REQUIRED)
    public void createOrder(Order order) {
        orderMapper.insert(order);            // 步骤1:落订单
        inventoryService.deduct(order.getSku(), order.getQty()); // 步骤2:扣库存
        // 若 deduct 抛异常,orderMapper.insert 也会一起回滚
    }
}

@Service
public class InventoryService {
    @Transactional(propagation = Propagation.REQUIRED)
    public void deduct(String sku, int qty) {
        // 与 createOrder 在同一事务,异常将整体回滚
        inventoryMapper.decrease(sku, qty);
    }
}

这种”同生共死”通常是期望行为,但如果你希望扣库存失败不影响订单已创建,就不该用 REQUIRED,而要改用 REQUIRES_NEW 让扣库存独立成事。

四、REQUIRES_NEW:什么时候需要独立事务

REQUIRES_NEW 会先把外层事务挂起(suspend),再开一个全新事务,内层提交或回滚都与外层无关。典型用途是写审计日志、操作流水——无论主业务成功与否都必须留下记录:

@Service
public class AuditService {
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void log(String action, String operator) {
        // 独立事务:外层回滚也不影响这条审计记录
        auditMapper.insert(new Audit(action, operator));
    }
}

// 调用方
@Transactional
public void pay(Long userId, BigDecimal amount) {
    try {
        doPay(userId, amount);
    } finally {
        auditService.log("PAY", "system"); // 即便 doPay 失败,日志照常落库
    }
}

注意:挂起/恢复需要事务管理器支持(DataSourceTransactionManager 支持),且 REQUIRES_NEW 因为要新建数据库连接,并发高时会增加连接池压力,不要滥用。

五、NESTED 与 REQUIRES_NEW 的本质区别

两者都涉及”子事务”,但语义完全不同:REQUIRES_NEW 是两个完全独立的事务(各自提交/回滚、各自连接);NESTED 是同一事务内的保存点,子操作回滚只回滚到保存点,外层仍可继续。NESTED 依赖 JDBC 保存点,需要数据库与驱动支持。

对比点REQUIRES_NEWNESTED
事务个数2 个独立事务1 个事务 + 1 个保存点
外层回滚影响不影响内层已提交部分外层回滚,内层也随之回滚
内层回滚影响不影响外层只回滚到保存点,外层可继续
连接占用2 个连接1 个连接
适用诉求必须独立落库(日志/计费)子步骤可单独失败、主流程继续

六、其余四种:何时该用

@Transactional(propagation = Propagation.SUPPORTS)     // 有事务就加入,没有就普通查询
public List<Order> queryOrders(Long userId) { ... }

@Transactional(propagation = Propagation.NOT_SUPPORTED) // 挂起事务,纯计算不持锁
public Report buildReport() { ... }

@Transactional(propagation = Propagation.MANDATORY)  // 必须被事务调用,否则直接报错
public void assertInTx() { ... }

@Transactional(propagation = Propagation.NEVER)      // 严禁在事务内调用
public void exportCsv() { ... }

SUPPORTS/NOT_SUPPORTED 常用于读方法,避免无谓的事务开销;MANDATORY 适合”必须在事务上下文中执行”的基础组件,防止被误用在非事务路径;NEVER 则用于明确不能持锁的导出类操作。

七、高频踩坑清单

  • 自调用导致注解失效:同类内部 this.methodB() 不经过代理,传播行为不生效。解决:注入自己(@Lazy 防循环)或拆到另一个 Bean,也可通过 AopContext.currentProxy() 获取代理调用。
  • 异常被吞掉不回滚:Spring 默认只对 RuntimeException 和 Error 回滚,受检异常不回滚。需要回滚受检异常时显式声明 @Transactional(rollbackFor = Exception.class)
  • catch 后未抛出:方法内 try-catch 吞掉异常且没有手动 setRollbackOnly,事务会正常提交,造成”以为回滚实际没回滚”。
  • 方法被标记 final / private:CGLIB 无法代理 final 方法,事务直接失效且不报错,排查极难。
  • 超时与长事务:事务内调用远程 HTTP、大批量循环更新,会长时间占用数据库连接并持有锁,引发雪崩。把这类操作移到事务外,或改用 NOT_SUPPORTED。
  • 多数据源未指定事务管理器:多库场景必须 @Transactional("txManagerB") 指定,否则走默认管理器导致错库事务。

这些坑的本质都回到一个判断——”这一行变更,到底属于哪个事务边界“。想清楚边界,传播行为就只是工具。事务与并发其实是连动的,Java 并发实战:线程池调优与异步编排 里提到的异步线程不会自动继承事务上下文,异步方法里的数据库操作要单独声明事务。高并发下事务越短越好,必要时可以结合 Java 21 虚拟线程 把非数据库阻塞移到虚拟线程,缩短持锁时间。

八、总结

Spring 事务传播行为的选择逻辑可以收敛为一句话:默认 REQUIRED 覆盖九成场景;需要”必须独立落库”用 REQUIRES_NEW;需要”子步骤可单独失败、主流程继续”用 NESTED;只读或禁事务场景用 SUPPORTS / NOT_SUPPORTED / NEVER / MANDATORY 明确边界。真正让人翻车的从来不是记不住 7 个枚举,而是忽略了”代理生效条件、回滚范围、事务边界”这三件事。配合合理的连接池与短事务纪律,事务就能从隐患变成可靠的基础能力。

上一篇 MongoDB 复制集与分片集群高可用实战
下一篇 vLLM 部署实战:高吞吐 LLM 推理服务调优