分类:后端开发 · 作者:管理员 · 发布于 2026-09-18 · 阅读 0 · 标签:Spring Boot / 事务管理 / @Transactional / 多数据源 / AOP

一、场景引入:一个回滚失败的线上事故

某天凌晨促销活动结束后,运营反馈「订单状态更新了但库存没扣减」。排查日志发现:方法 B 抛了异常,但外层方法 A 用 try-catch 吞掉了异常,Spring 检测到 rollback-only 标记与 commit 意图冲突,抛出 UnexpectedRollbackException,最终事务既没提交也没回滚干净——数据出现了部分写入。

这类事务边界问题在 Spring Boot 应用中极为常见,本文梳理 6 种 @Transactional 失效场景,并给出多数据源事务的实战方案。

二、6 种 @Transactional 失效场景

场景 1:同一类中方法内部调用(自调用)

现象:

@Service
public class OrderService {
    public void createOrder(OrderDTO dto) {
        saveOrder(dto);              // 无事务
        deductStock(dto.getSkuId()); // 有 @Transactional,但无效
    }

    @Transactional
    public void deductStock(Long skuId) {
        // 扣减库存 ...
    }
}

原因:Spring 声明式事务基于 AOP 代理。内部方法通过 this 调用,不走代理对象,事务拦截器不生效。

解决:

  • 将事务方法提取到单独的 StockService,注入后调用
  • 或注入自身代理:@Autowired private OrderService self;(配合 @EnableAspectJAutoProxy(exposeProxy = true) + AopContext.currentProxy(),但不太推荐)

场景 2:rollbackFor 未指定,非 RuntimeException 不回滚

现象:

@Transactional
public void payOrder(String orderNo) throws Exception {
    // 更新订单状态...
    if (someCondition) {
        throw new Exception("业务校验失败"); // 不会回滚!
    }
}

原因:Spring 默认只对 RuntimeException 和 Error 回滚,受检异常(Exception)不回滚。

解决:

@Transactional(rollbackFor = Exception.class)
public void payOrder(String orderNo) throws Exception {
    // ...
}

场景 3:异常被 try-catch 吞掉

现象:

@Transactional
public void processOrder(OrderDTO dto) {
    try {
        orderDao.update(dto);
        stockService.deduct(dto.getSkuId()); // 内部抛异常标记了 rollback-only
    } catch (Exception e) {
        log.error("忽略异常", e); // 吞掉,直接导致 UnexpectedRollbackException
    }
}

后果:子事务标记了 rollback-only,外层却尝试 commit,Spring 抛出 UnexpectedRollbackException: Transaction rolled back because it has been marked as rollback-only。

解决:

  • 不要在事务方法内 try-catch 后不抛出;要么重新抛出,要么手动调用 TransactionAspectSupport.currentTransactionStatus().setRollbackOnly()
  • 若需异常处理且不回滚,改用 Propagation.REQUIRES_NEW 隔离子事务

场景 4:事务传播级别设置错误

现象:

@Transactional(propagation = Propagation.NEVER) // 如果当前存在事务则抛异常
public void queryPrice(Long skuId) {
    // ...
}

原因:SUPPORTS、NOT_SUPPORTED、NEVER 三种传播特性不会在当前线程中创建事务,若期望事务而未检查传播类型则失效。

常用传播对照表:

  • REQUIRED(默认):有事务则加入,无则新建
  • REQUIRES_NEW:挂起当前事务,新建独立事务
  • NESTED:嵌套事务(JDBC savepoint),外部回滚会影响内部
  • MANDATORY:必须有事务,否则抛异常
  • SUPPORTS / NOT_SUPPORTED / NEVER:不保证事务存在

场景 5:@Transactional 加在 private 或非 public 方法上

原因:Spring AOP 默认使用 JDK 动态代理(或 CGLIB),代理对象只拦截 public 方法。private 方法上的 @Transactional 完全被忽略。

解决:确保事务方法为 public。

场景 6:多线程调用

现象:

@Transactional
public void batchCreate(List<OrderDTO> dtos) {
    dtos.parallelStream().forEach(dto -> {
        orderDao.insert(dto); // 新线程内无事务
    });
}

原因:Spring 事务通过 ThreadLocal 将数据库连接绑定到当前线程。新线程拿不到已有事务的连接,每个线程自己也没有开启事务。

解决:

  • 每个线程内独立开启事务(编程式事务)
  • 或使用声明式事务 + @Async + 单独 TransactionTemplate
  • 大数据量下建议分批同步处理,避免线程安全问题

三、多数据源事务实战

电商中台常见场景:订单库(MySQL)+ 日志库(PostgreSQL),跨库操作必须保证要么全成功、要么全回滚。

方案一:ChainedTransactionManager(已废弃,不推荐)

Spring Data 曾提供 ChainedTransactionManager,按顺序提交各数据源事务。问题在于:若第二个数据源提交失败,第一个已成功提交的数据源无法回滚,造成数据不一致。

方案二:Atomikos(JTA 分布式事务)

引入依赖:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-jta-atomikos</artifactId>
</dependency>

配置两个数据源,并将事务管理器改为 JtaTransactionManager:

@Primary
@Bean(name = "orderTransactionManager")
public PlatformTransactionManager orderTransactionManager() {
    return new JtaTransactionManager();
}

此时 @Transactional 即可跨两个数据源实现两阶段提交(2PC)。代价是性能损耗和 XA 协议的超时风险。

方案三:TCC / Saga + 最终一致性(推荐)

对于高并发场景,2PC 的「准备-提交」阻塞模型难以承受。更务实的做法是放弃强一致性,改为最终一致性:

// 步骤1:本地事务写入本地表 + 消息表
@Transactional(rollbackFor = Exception.class)
public void createOrder(OrderDTO dto) {
    orderDao.insert(dto);
    eventDao.insert(new EventMessage("ORDER_CREATED", dto.getOrderNo()));
}

// 步骤2:轮询消息表,发送到 MQ
// 步骤3:下游消费后更新日志库

若必须在一个 @Transactional 内跨库,且无高性能要求,Atomikos 可用;否则请拥抱最终一致性——本地消息表或 MQ 事务消息。

四、调试与排查工具

  • 开启 SQL 日志:logging.level.org.springframework.jdbc.datasource.DataSourceTransactionManager=DEBUG,观察事务 begin / commit / rollback 日志
  • 观察事务同步钩子:TransactionSynchronizationManager.isActualTransactionActive() 可在代码中打点验证
  • 检查代理状态:AopUtils.isAopProxy(service) 确认代理是否生效

五、最佳实践总结

  1. 事务方法务必 public,且不要同类自调用
  2. 必须指定 rollbackFor = Exception.class,除非你清楚默认行为
  3. 不要在事务内 try-catch 后不重新抛出异常
  4. 多线程不在事务管理范围内,改用编程式事务或异步消息
  5. 跨数据源事务优先考虑 TCC + 最终一致性,而非 2PC
  6. 开启事务日志,上线前用单元测试验证回滚行为