Spring 事务传播行为与失效场景排查

举报
yd_287908631 发表于 2026/11/01 00:43:37 2026/11/01
【摘要】 用可复现的错误代码讲透 7 种传播行为与 8 类事务失效。

@Transactional 是 Spring 里最容易"看起来生效、实际没生效"的注解:加上了注解、方法也抛了异常,数据却写进了库。这类问题的根因几乎都落在传播行为和代理机制上。本文用可复现的错误代码,逐个拆解事务失效的场景并给出正确写法。适合负责订单、支付、库存等强一致业务的后端开发阅读。

一、事务的底层抽象

Spring 事务不自己实现事务,而是对 JDBC、JTA、JPA 做统一封装。核心接口有三个:

  • PlatformTransactionManager:事务管理器,负责 getTransaction、commit、rollback。
  • TransactionDefinition:事务定义,包含传播行为、隔离级别、超时、只读标记。
  • TransactionStatus:事务运行状态,记录是否新事务、是否已完成、是否标记回滚。

以 DataSourceTransactionManager 为例,它通过 TransactionSynchronizationManager 把 Connection 绑定到当前线程的 ThreadLocal,从而让同一线程内的多个 DAO 复用同一个连接。事务边界靠 setAutoCommit(false) + commit() / rollback() 实现,ThreadLocal 在 cleanupAfterCompletion 中清理。

这解释了事务的第一个隐含前提:同一个线程。跨线程、跨 CompletableFuture 的操作不在同一个事务里。

二、七种传播行为

| 传播行为 | 当前有事务 | 当前无事务 | 典型用途 |

| --- | --- | --- | --- |

| REQUIRED(默认) | 加入 | 新建 | 绝大多数业务 |

| REQUIRES_NEW | 挂起,新建 | 新建 | 日志、审计,不能受主事务影响 |

| SUPPORTS | 加入 | 非事务执行 | 查询方法 |

| NOT_SUPPORTED | 挂起,非事务执行 | 非事务执行 | 耗时操作,避免长事务 |

| MANDATORY | 加入 | 抛异常 | 强制要求上游开启事务 |

| NEVER | 抛异常 | 非事务执行 | 明确禁止事务 |

| NESTED | 建保存点 | 新建 | 子操作失败不影响主流程 |

REQUIRES_NEW 与 NESTED 最容易混淆:

  • REQUIRES_NEW 会挂起外层事务、使用新的数据库连接,内外完全独立。外层回滚不影响内层已提交的数据,内层回滚也不影响外层。
  • NESTED 在同一个连接上设置保存点(Savepoint),内层回滚只回滚到保存点,外层可以继续。它要求底层驱动支持保存点,JTA 事务不支持。

NESTED 的一个典型陷阱:内层抛异常被外层捕获后,DataSourceTransactionManager 会把外层事务标记为 rollback-only,最终外层提交时抛 UnexpectedRollbackException。这一点在下面第五节展开。

三、失效场景一:自调用

@Service
public class OrderService {

    public void createOrder(Order order) {
        insertOrder(order);
        insertDetail(order);   // 内部调用,不经过代理
    }

    @Transactional
    public void insertDetail(Order order) {
        detailMapper.insert(order);
        if (order.getAmount() == null) {
            throw new IllegalArgumentException("amount is null");
        }
    }
}

createOrder 没有事务注解,从 Controller 直接调用它时,insertDetail 的 @Transactional 完全不生效。异常抛出后 insertOrder 已写入的数据不会回滚。

原因在代理:外部调用 createOrder 进入代理,代理把调用转给目标对象;目标对象内部 this.insertDetail() 是普通方法调用,不经过代理,事务拦截器无从介入。

正确写法:

@Service
public class OrderService {
    private final OrderDetailService detailService;

    public OrderService(OrderDetailService detailService) {
        this.detailService = detailService;
    }

    @Transactional(rollbackFor = Exception.class)
    public void createOrder(Order order) {
        orderMapper.insert(order);
        detailService.insertDetail(order);   // 跨 Bean 调用,经过代理
    }
}

四、失效场景二:异常类型不对

@Transactional
public void transfer(Long from, Long to, BigDecimal amount) throws Exception {
    accountMapper.debit(from, amount);
    accountMapper.credit(to, amount);
    throw new Exception("业务校验失败");   // 受检异常,默认不回滚
}

@Transactional 默认只对 RuntimeException 和 Error 回滚,受检异常不会触发回滚。上面这段代码会把钱转出去却不回滚。

正确写法是显式声明:

@Transactional(rollbackFor = Exception.class)

同理,如果异常被 catch 后没有重新抛出,事务拦截器根本感知不到异常,也不会回滚:

@Transactional
public void save(Order order) {
    try {
        orderMapper.insert(order);
        throw new IllegalStateException("boom");
    } catch (Exception e) {
        log.error("insert failed", e);   // 吞掉异常,事务照常提交
    }
}

需要局部容错时,把可能失败的操作拆到独立 Bean 并标注 REQUIRES_NEW,或手动调用 TransactionAspectSupport.currentTransactionStatus().setRollbackOnly()。

五、失效场景三:rollback-only 导致提交失败

@Service
public class OuterService {
    private final InnerService innerService;

    @Transactional
    public void outer() {
        try {
            innerService.inner();   // inner 标注 @Transactional
        } catch (Exception e) {
            log.warn("inner failed, continue", e);
        }
        // 继续写主表
    }
}

inner() 抛出运行时异常时,事务拦截器调用 completeTransactionAfterThrowing,发现当前事务是参与已有事务(REQUIRED),于是执行 txInfo.transactionStatus.setRollbackOnly(),把整个事务标记为只能回滚。外层 catch 之后继续执行,提交时抛:

org.springframework.transaction.UnexpectedRollbackException:
Transaction silently rolled back because it has been marked as rollback-only

这是分布式系统里极常见的线上问题:日志显示"已捕获异常",接口却返回 500。

正确做法取决于业务语义:

  • 内层失败必须影响外层:不要 catch,让异常向上传播。
  • 内层失败不应影响外层:内层改用 @Transactional(propagation = Propagation.REQUIRES_NEW),使用独立事务。

六、失效场景四:非 public 方法与 final 类

@Service
public class ReportService {

    @Transactional
    protected void generate() {   // protected,CGLIB 跨包无法覆写
        reportMapper.insert();
    }
}

CGLIB 通过子类覆写实现拦截,private 方法无法覆写,protected 和包级方法在跨包代理时同样不可见。Spring 6 的 AnnotationTransactionAttributeSource 对非 public 方法直接跳过,不抛异常。

final 类、final 方法同样无法被 CGLIB 代理。检查 Lombok 的 @Value、Kotlin 默认 final 类,这些都会让事务静默失效。

七、失效场景五:多线程与异步

@Transactional
public void batchImport(List<Order> orders) {
    orders.parallelStream().forEach(orderMapper::insert);   // 子线程无事务
}

事务上下文绑定在 ThreadLocal,parallelStream 的工作线程拿不到当前事务的连接,每条 insert 各自自动提交。任何一条失败都不会回滚已插入的数据。

正确做法是在单线程内完成批量写入,使用 MyBatis 的 foreach 批量 SQL 或 JDBC addBatch:

@Transactional(rollbackFor = Exception.class)
public void batchImport(List<Order> orders) {
    orderMapper.insertBatch(orders);
}

如果确实需要并发,必须自己控制每个子任务的事务边界,并接受"部分成功"的语义。

八、失效场景六:数据源与事务管理器不匹配

多数据源场景下,@Transactional 默认使用标注了 @Primary 的 TransactionManager。操作从库时如果注解没有指定 transactionManager = "slaveTransactionManager",事务会开在主库连接上,从库的写入根本不在事务内。

@Transactional(transactionManager = "slaveTransactionManager", rollbackFor = Exception.class)
public void writeToSlave(Record record) {
    slaveMapper.insert(record);
}

另一个隐蔽情况是:@Transactional 标注在类上,但方法内部调用了 JdbcTemplate 的另一个 DataSource,两者连接不同,事务不覆盖。

九、失效场景七:只读事务里做写操作

@Transactional(readOnly = true)
public void refreshCache() {
    cacheMapper.deleteAll();   // 只读事务下部分驱动会拒绝写操作
    cacheMapper.insertBatch(data);
}

readOnly = true 会把连接设为只读,MySQL 驱动在 SET SESSION TRANSACTION READ ONLY 后拒绝写操作,抛 Connection is read-only。同时它也是给数据库优化器的提示,避免不必要的锁。查询方法加 readOnly = true 是好事,但不要在其中写数据。

十、常见坑 / 最佳实践

  1. @Transactional 只加在 Service 的 public 方法上。Controller 不加,DAO 不加,避免事务边界模糊。
  2. 统一使用 rollbackFor = Exception.class。默认只回滚运行时异常,受检异常漏掉是高频事故。
  3. 不要在事务方法里做远程调用。HTTP、MQ 发送会把事务拉长到秒级,连接池很快耗尽。先提交事务,再用 TransactionSynchronization#afterCommit 发消息。
  4. REQUIRES_NEW 要评估连接数。它需要额外一条连接,嵌套多层时容易打满连接池,且内外事务可能互相等待锁。
  5. 自调用一律抽类。AopContext.currentProxy() 和注入自身代理都能用,但可读性和可测试性都差,抽类是最稳的解法。
  6. 事务方法内不要吞异常。要么抛出,要么显式 setRollbackOnly(),不要既 catch 又假装没事。

十一、小结

  • 传播行为决定"加入、新建、挂起、拒绝"四种关系,REQUIRED 是默认值。
  • 自调用绕过代理,是事务失效的第一大原因,跨 Bean 调用才能生效。
  • 默认只回滚 RuntimeException 和 Error,受检异常必须显式 rollbackFor。
  • 内层参与事务失败会污染外层,导致 UnexpectedRollbackException,需要 REQUIRES_NEW 隔离。
  • 非 public、final、多线程、多数据源都会让事务静默失效,排查时按这五项逐一确认。
【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。