人人都会AI编程

6.5 事务失效的底层原因总览

更新时间:2026-07-11

在实际开发中,明明在方法上标注了 @Transactional,事务却没有生效——不回滚、不提交、连接提早释放,甚至根本没开启事务。这类问题的根因通常并不复杂,只是藏在 Spring 的事务抽象和代理机制背后。本节一次性梳理最常见的事务失效场景及其底层原因,为日常排查提供一份参考清单。

6.5.1 Spring 事务管理的本质依赖:AOP 代理

核心前提:Spring 的声明式事务是通过 AOP 动态代理实现的。当容器发现一个 Bean 的方法上有 @Transactional 时,并不会直接调用原始对象,而是为其生成一个代理对象。在代理中,方法调用被“拦截”:代理先通过事务管理器开启事务,再通过反射调用目标方法,最后根据方法执行结果提交或回滚。

因此,凡是让 AOP 代理失效的场景,事务必然失效。这是理解大部分失效问题的出发点。

6.5.2 八大经典失效场景解析

1. 同类内部调用:直接调用本类方法绕过代理

@Service
public class OrderService {
    @Transactional
    public void methodA() {
        // 事务开启
        this.methodB();   // 直接调用,不走代理
    }

    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void methodB() {
        // 这里的事务不会单独开启!
    }
}

原因this.methodB() 是目标对象自己的方法调用,完全绕过了 AOP 代理。methodB 上的事务注解根本不会被代理感知。
解决:将 methodB 抽到另一个 Bean 中注入调用,或通过 AopContext.currentProxy() 获取当前代理对象来调用。

2. 方法非 public:CGLIB 无法代理私有/受保护方法

@Transactional
void updateOrder(Order order) {  // 包级私有方法
    // ...
}

原因:Spring 默认使用 CGLIB 生成子类代理,而 CGLIB 只能重写父类的 public 或 protected 方法(JDK 动态代理则要求基于接口)。Spring 的事务代理无法拦截包级私有或 private 方法,事务自然不生效。
解决:将目标方法改为 public

3. 异常被“吞掉”:捕获异常却未正确传播

@Transactional
public void placeOrder(Order order) {
    try {
        inventoryService.reduceStock(order);
        paymentService.charge(order);
    } catch (Exception e) {
        log.error("下单失败", e);
        // 没有抛出异常,事务会正常提交!
    }
}

原因:Spring 的事务回滚规则是“遇到未捕获的 RuntimeException 和 Error 时自动回滚”。一旦异常在方法内部被 catch 并消化掉,代理层根本感知不到异常,自然会认为方法正常结束,执行事务提交。
解决:在 catch 块中重新抛出异常,或显式调用 TransactionAspectSupport.currentTransactionStatus().setRollbackOnly() 标记当前事务为仅回滚。

4. 异常类型不匹配:受检异常默认不回滚

@Transactional
public void exportReport() throws IOException {
    // 抛出一个受检异常
    throw new IOException("文件写入失败");
}

原因:默认回滚策略只针对 unchecked 异常(RuntimeException 及其子类)和 Error。对于受检异常(Exception 中除 RuntimeException 外的部分),Spring 选择不回滚,因为这类异常通常被认为可以恢复。
解决:在 @Transactional(rollbackFor = Exception.class) 中明确声明需要回滚的异常类型。

5. 数据库引擎不支持事务:MyISAM 是典型陷阱

现象:代码完全正确,但数据操作无法回滚,甚至连事务开启都没有报错。

原因:Spring 并不知道底层表的存储引擎是什么。如果 MySQL 中使用的表是 MyISAM(不支持事务),Spring 依然会“成功开启事务”,但任何操作都会立即提交,回滚毫无效果。
解决:确保核心业务表使用 InnoDB 或其他支持事务的引擎。

6. 多线程环境:事务绑定在线程上

@Transactional
public void processAsync() {
    // 当前线程持有事务
    new Thread(() -> {
        orderDao.updateStatus(1001, "PAID");  
        // 这里的事务是新线程独立开启的,与外部无关!
    }).start();
}

原因:Spring 的事务通过 ThreadLocal 绑定到当前线程。新线程中获取不到原有事务上下文,因而会以全新的事务执行。更危险的是,如果外部事务需要回滚,新线程的操作已经独立提交,导致数据不一致。
解决:避免在事务方法内直接启动新线程执行数据操作,或通过事务管理器显式传播事务逻辑。

7. 传播行为配置错误:NOT_SUPPORTED 等导致挂起事务

@Transactional(propagation = Propagation.NOT_SUPPORTED)
public void queryData() {
    // 当前事务被挂起,方法在无事务状态下执行
}

原因:某些传播行为(如 NOT_SUPPORTEDNEVER)会强制以非事务方式运行。如果误用了这些属性,即便外层有事务,方法内部也会在没有事务保护的情况下执行。
解决:检查传播行为设置,确保使用 REQUIRED(默认)或符合预期的配置。

8. 自调用和事务传播的组合陷阱

即使通过注入解决了自调用问题,如果内外传播行为配置不当,仍可能失效。例如外层 REQUIRED,内层 REQUIRES_NEW,期望内层能独立提交,但若内层也是通过 this 调用,同样无效。

6.5.3 快速排查清单

当发现事务未按预期工作时,按以下顺序排查:

  1. 确认代理存在:检查目标对象是否由 Spring 管理(通过 @Service@Component 等注册为 Bean),并确认调用链是否经过代理。可在调试中打印对象类名,看是否为 CGLIB 代理(类名含 $$EnhancerBySpringCGLIB)。
  2. 确认方法可见性:目标方法是否 public
  3. 确认异常传播:异常是否被 try-catch 吃掉?日志中是否有异常栈?
  4. 确认回滚规则:抛出的异常类型是否符合回滚条件?必要时扩展 rollbackFor
  5. 确认数据库支持:查询表的存储引擎(SHOW TABLE STATUS FROM db_name LIKE 'table_name')。
  6. 确认线程上下文:事务操作是否发生在同一个线程内?是否使用了 @Async 或手动 new Thread
  7. 确认事务管理器:是否配置了正确的 PlatformTransactionManager,尤其是多数据源情境下。
  8. 确认配置开关:是否在主配置类上使用了 @EnableTransactionManagement(Spring Boot 已默认开启,但纯 Spring 项目需要)。

事务失效的根源几乎都集中在这八个场景中。理解代理机制和线程亲和性后,大多数问题都能在几分钟内定位。这也是为什么深入掌握 AOP 和事务传播原理,对于日常开发而言并非“面试知识点”,而是实实在在的排错能力。