Lesson 55 · Spring 生态核心原理

Spring 事务传播机制与 @Transactional 失效的 8 种场景

高级·⭐ 必问·#Spring·#事务·#面试高频

第 1 站

面试官:@Transactional 在什么情况下会失效?

"你项目里用了 @Transactional 吧?那你说说,它有哪些失效的场景?" —— 这道题几乎出现在所有 Spring 面试中,不是因为面试官刁钻,而是因为踩坑的人太多了。

Spring 的事务管理是日常开发中最常用的基础设施之一。只需在方法上加一个 @Transactional,Spring 就帮我们搞定事务的开启、提交和回滚。但"简单"背后藏着大量陷阱:

  • 自调用失效:同类中 A 方法调用 B 方法,B 上的 @Transactional 不生效
  • 异常类型不匹配:默认只回滚 RuntimeException,Checked Exception 不回滚
  • private 方法:Spring AOP 基于代理,无法拦截 private 方法
  • 异常被吞掉:方法内部 catch 了异常没抛出,Spring 感知不到

更让人困惑的是事务传播行为——REQUIRED、REQUIRES_NEW、NESTED 到底有什么区别?什么时候该用哪种?理解这些需要从事务的底层机制出发,而非死记硬背。

Spring 事务核心公式
@EnableTransactionManagement → TransactionInterceptor(AOP 代理)→ PlatformTransactionManager → JDBC Connection 管理(setAutoCommit / commit / rollback)

本文以面试高频问题为主线,系统拆解 7 种传播行为、8 种失效场景、事务实现原理,让你面对事务问题时有底层支撑。

第 2 站

7 种事务传播行为:一图看懂

事务传播行为(Propagation)定义了当一个事务方法被另一个事务方法调用时,事务应该如何传播。Spring 定义了 7 种,全部在 Propagation 枚举中:

事务传播行为决策流程 调用 @Transactional 方法 传播行为 = ? 当前已有事务? 不管有没有事务 当前必须无事务 REQUIRED 加入已有事务 无则新建 默认值 MANDATORY 必须加入已有 无则抛异常 REQUIRES_NEW 总是新建事务 挂起已有事务 NEVER 必须无事务 有则抛异常 SUPPORTS: 有则加入 无则非事务执行 NOT_SUPPORTED: 挂起事务,非事务执行 NESTED: 在已有事务中 创建 Savepoint 面试重点:REQUIRED vs REQUIRES_NEW vs NESTED 共享事务 vs 独立事务 vs 保存点事务 —— 回滚边界完全不同
图 1事务传播行为决策图:根据"当前是否存在事务"选择不同传播策略
传播行为有事务时无事务时使用频率
REQUIRED(默认)加入当前事务创建新事务90% 场景
SUPPORTS加入当前事务非事务执行读操作优化
MANDATORY加入当前事务抛 IllegalTransactionStateException强制调用方开事务
REQUIRES_NEW挂起当前事务,创建新事务创建新事务日志、审计
NOT_SUPPORTED挂起当前事务,非事务执行非事务执行极少使用
NEVER抛 IllegalTransactionStateException非事务执行极少使用
NESTED在当前事务中创建 Savepoint等同于 REQUIRED部分回滚
"挂起事务"是怎么实现的?

REQUIRES_NEW 和 NOT_SUPPORTED 需要"挂起"当前事务。Spring 通过 TransactionSynchronizationManager 保存当前线程绑定的 ConnectionHolder,然后将其从 ThreadLocal 中移除,待内层事务结束后再恢复。挂起本质是解绑当前线程与数据库连接的关联

第 3 站

REQUIRED vs REQUIRES_NEW vs NESTED:回滚边界

面试中最高频的追问就是这三者的区别。核心差异在于回滚边界

OrderService.java · REQUIRED — 共享事务
@Service
public class OrderService {
    @Autowired private InventoryService inventoryService;

    @Transactional  // 默认 REQUIRED
    public void createOrder(Order order) {
        orderDao.insert(order);             // ① 插入订单
        inventoryService.deduct(order);     // ② 扣减库存
    }
}

@Service
public class InventoryService {
    @Transactional  // REQUIRED:加入 createOrder 的事务
    public void deduct(Order order) {
        stockDao.deduct(order.getSkuId());
        if (stock < 0) throw new RuntimeException("库存不足");
        // ★ 抛异常后,createOrder 的 ① 也会回滚!
        // 因为它们在同一个事务中
    }
}

REQUIRED 的关键:内层方法加入外层事务,任何一层抛异常,整个事务一起回滚。适用于大多数业务场景——订单和库存要么都成功,要么都失败。

AuditService.java · REQUIRES_NEW — 独立事务
@Service
public class OrderService {
    @Autowired private AuditService auditService;

    @Transactional
    public void createOrder(Order order) {
        orderDao.insert(order);
        auditService.log("创建订单: " + order.getId());  // 记录审计日志
        throw new RuntimeException("业务失败");
        // ★ 外层回滚不影响 auditService.log()
        // REQUIRES_NEW 是独立事务,已提交就提交了
    }
}

@Service
public class AuditService {
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void log(String message) {
        auditDao.insert(new AuditLog(message));
        // ★ 独立事务,外层回滚不影响这里
    }
}

REQUIRES_NEW 的关键:挂起外层事务,开启全新事务。内层提交后即使外层回滚,内层数据依然持久化。典型场景:审计日志、操作记录——业务失败也要留痕。

BatchService.java · NESTED — 保存点事务
@Service
public class BatchService {
    @Autowired private ItemService itemService;

    @Transactional
    public void batchProcess(List<Item> items) {
        for (Item item : items) {
            try {
                itemService.processItem(item);  // NESTED
            } catch (Exception e) {
                // ★ 只回滚当前 item 的处理
                // 其他已成功的 item 不受影响
                log.warn("item " + item.getId() + " 处理失败,跳过");
            }
        }
    }
}

@Service
public class ItemService {
    @Transactional(propagation = Propagation.NESTED)
    public void processItem(Item item) {
        itemDao.update(item);
        // ★ 在 Savepoint 上执行,失败只回滚到 Savepoint
        // 不回滚外层事务中已成功的操作
    }
}

NESTED 的关键:在同一个物理事务中通过 JDBC Savepoint 实现逻辑上的子事务。内层失败可以只回滚到 Savepoint,外层 catch 后继续。但外层整体回滚时,内层也会一起回滚(因为是同一个 Connection)。

维度REQUIREDREQUIRES_NEWNESTED
物理事务同一个两个独立事务同一个
Connection共享各用各的共享
内层失败整个事务回滚只回滚内层回滚到 Savepoint
外层失败全部回滚内层不受影响全部回滚
典型场景订单+库存审计日志批量处理(部分失败不阻断)
JDBC 依赖无特殊要求需要多连接池需要驱动支持 Savepoint
面试回答要点

"REQUIRED 共享同一个事务,一起提交一起回滚。REQUIRES_NEW 是两个独立事务,内层提交后不受外层影响。NESTED 在同一个事务中通过 Savepoint 实现部分回滚,外层失败时内层也会回滚。选哪种取决于业务对回滚边界的要求。"

第 4 站

@Transactional 失效的 8 种场景

理解失效场景的前提是理解 @Transactional 的生效条件:方法必须通过 Spring 代理对象调用,代理拦截方法后由 TransactionInterceptor 管理事务。一旦绕过代理或代理无法控制事务边界,就会失效。

场景 1:自调用(Self-invocation)—— 最高频踩坑

OrderService.java · 自调用失效
@Service
public class OrderService {
    public void methodA() {
        // ★ 这里调用 methodB() 等于 this.methodB()
        // 不经过代理,@Transactional 不生效!
        methodB();
    }

    @Transactional
    public void methodB() {
        orderDao.insert(new Order());
        throw new RuntimeException();
        // 不会回滚!因为没有事务
    }
}

原因:Spring 默认用 JDK 动态代理或 CGLIB 代理。外部调用 orderService.methodB() 时走的是代理对象,但 methodA() 内部 this.methodB() 走的是原始对象,绕过了代理。

解决方案

解决方案对比
// 方案 1:注入自身(推荐)
@Service
public class OrderService {
    @Autowired private OrderService self;  // 注入代理对象
    public void methodA() { self.methodB(); }
}
// 方案 2:从 ApplicationContext 获取代理
OrderService proxy = context.getBean(OrderService.class);
proxy.methodB();
// 方案 3:启用 AspectJ 织入(非代理模式)
@EnableTransactionManagement(mode = AdviceMode.ASPECTJ)

场景 2:private 方法

失效示例
@Transactional
private void insertOrder(Order order) {  // ★ private:事务不生效
    orderDao.insert(order);
}

CGLIB 代理通过生成子类重写方法来拦截。private 方法无法被重写,代理拦截不到。Spring 在创建代理时会检测并打印 WARN 日志。

场景 3:非 public 方法(Spring Boot 3.x 之前)

Spring Framework 5.x 及以前,@Transactional 只能加在 public 方法上。protected / package-private 方法上的注解会被静默忽略。Spring Boot 3.x(Spring 6)放宽了限制,但仍推荐统一使用 public。

场景 4:异常类型不匹配

默认回滚规则
@Transactional  // 默认 rollbackFor = { RuntimeException, Error }
public void process() throws IOException {
    fileDao.insert(record);
    throw new IOException("文件读写失败");
    // ★ 不回滚!IOException 是 Checked Exception
}

// 正确写法:显式指定 rollbackFor
@Transactional(rollbackFor = Exception.class)
public void process() throws IOException { ... }

Spring 默认只对 RuntimeExceptionError 回滚。Checked Exception(如 IOException、SQLException)需要显式声明 rollbackFor

场景 5:异常被方法内部 catch

异常被吞
@Transactional
public void transfer() {
    try {
        accountDao.deduct(from, amount);
        accountDao.add(to, amount);
        if (balance < 0) throw new RuntimeException("余额不足");
    } catch (Exception e) {
        log.error("转账失败", e);
        // ★ 异常没抛出去!Spring 感知不到,不会回滚
    }
}
// 正确:catch 后重新抛出
catch (Exception e) { log.error(...); throw e; }

场景 6:数据库引擎不支持事务

MySQL 的 MyISAM 引擎不支持事务。即使 Spring 正确配置了事务管理器,底层 JDBC 的 connection.commit()connection.rollback() 也不起作用。InnoDB 才支持事务。这是基础设施层面的问题,Spring 无法修复。

场景 7:Bean 未被 Spring 管理

未托管的 Bean
// ★ 没有 @Service / @Component,Spring 不管理这个 Bean
public class OrderService {
    @Transactional
    public void createOrder() { ... }
}
// 手动 new 出来的对象也不行
OrderService svc = new OrderService();  // 无代理,事务无效
svc.createOrder();

场景 8:传播行为设置错误

传播行为误用
@Transactional(propagation = Propagation.NOT_SUPPORTED)
public void createOrder() {
    // ★ NOT_SUPPORTED:以非事务方式执行
    // 即使方法里有数据库操作,也不在事务中
    orderDao.insert(order);
    throw new RuntimeException();  // 不回滚
}
8 种失效场景速查

1. 自调用(this 绕过代理) 2. private 方法(无法被代理) 3. 非 public 方法(旧版本限制) 4. 异常类型不匹配(默认只回滚 RuntimeException) 5. 异常被 catch 吞掉 6. 数据库引擎不支持(MyISAM) 7. Bean 未被 Spring 管理 8. 传播行为配置错误(如 NOT_SUPPORTED)

第 5 站

事务实现原理:从注解到 Connection

@Transactional 的生效链路可以拆解为四个层次:

① 启用事务管理 · @EnableTransactionManagement
@Configuration
@EnableTransactionManagement  // 导入 TransactionManagementConfigurationSelector
public class AppConfig {
    @Bean
    public PlatformTransactionManager transactionManager(DataSource ds) {
        return new DataSourceTransactionManager(ds);
    }
}

@EnableTransactionManagement 通过 @Import 注册 AutoProxyRegistrarProxyTransactionManagementConfiguration。前者负责注册 InfrastructureAdvisorAutoProxyCreator(AOP 自动代理创建器),后者注册 BeanFactoryTransactionAttributeSourceAdvisor(事务切面)。

② AOP 拦截 · TransactionInterceptor
public class TransactionInterceptor extends TransactionAspectSupport
        implements MethodInterceptor {
    public Object invoke(MethodInvocation invocation) {
        // 获取目标类和方法
        Class<?> targetClass = getTargetClass(invocation.getThis());
        // 解析 @Transactional 属性(传播行为、隔离级别、超时、rollbackFor...)
        TransactionAttribute txAttr = getTransactionAttributeSource()
                .getTransactionAttribute(method, targetClass);
        // 获取事务管理器
        PlatformTransactionManager tm = determineTransactionManager(txAttr);
        // 创建事务(核心!)
        return invokeWithinTransaction(method, targetClass, invocation::proceed);
    }
}
③ 事务管理核心 · TransactionAspectSupport
protected Object invokeWithinTransaction(Method method, Class<?> targetClass,
        InvocationCallback invocation) {
    // 1. 获取事务定义
    TransactionAttribute txAttr = getTransactionAttribute(method, targetClass);
    PlatformTransactionManager tm = determineTransactionManager(txAttr);

    // 2. 创建事务(根据传播行为决定是新建还是加入)
    TransactionInfo txInfo = createTransactionIfNecessary(tm, txAttr, joinpointId);

    Object retVal;
    try {
        // 3. 执行目标方法
        retVal = invocation.proceedWithInvocation();
    } catch (Throwable ex) {
        // 4a. 异常 → 判断是否回滚
        completeTransactionAfterThrowing(txInfo, ex);
        throw ex;
    }
    // 4b. 正常 → 提交事务
    commitTransactionAfterReturning(txInfo);
    return retVal;
}
④ 底层 Connection 管理 · DataSourceTransactionManager
protected void doBegin(Object transaction, TransactionDefinition def) {
    DataSourceTransactionObject txObject = (DataSourceTransactionObject) transaction;
    // 从连接池获取新 Connection
    Connection con = obtainDataSource().getConnection();
    // ★ 关闭自动提交 —— 事务的起点
    con.setAutoCommit(false);
    // 绑定到 ThreadLocal
    TransactionSynchronizationManager.bindResource(dataSource,
            new ConnectionHolder(con));
}
protected void doCommit(DefaultTransactionStatus status) {
    Connection con = txObject.getConnectionHolder().getConnection();
    con.commit();  // 提交
}
protected void doRollback(DefaultTransactionStatus status) {
    Connection con = txObject.getConnectionHolder().getConnection();
    con.rollback();  // 回滚
}
@Transactional 完整生效链路 @EnableTransaction Management 注册 Advisor CGLIB Proxy 生成代理子类 拦截方法调用 Transaction Interceptor 解析注解属性 PlatformTransactionManager doBegin / doCommit / doRollback Connection 管理 ThreadLocal(TransactionSynchronizationManager)保存 ConnectionHolder → 同一线程复用同一 Connection 保证事务内所有 DAO 操作使用同一个数据库连接 JDBC Connection setAutoCommit(false) → SQL → commit() / rollback()
图 2@Transactional 完整生效链路:注解 → AOP 代理 → 拦截器 → 事务管理器 → JDBC Connection
为什么事务内所有 DAO 必须用同一个 Connection?

事务的 ACID 特性依赖于同一个数据库会话(Session)。如果 DAO-A 用 Connection-1、DAO-B 用 Connection-2,它们是两个独立事务,无法做到原子提交。Spring 通过 TransactionSynchronizationManager(基于 ThreadLocal)将 Connection 绑定到当前线程,确保同一事务内所有 JdbcTemplate / MyBatis 操作拿到同一个连接。

第 6 站

编程式事务 vs 声明式事务

Spring 提供两种事务管理方式:

维度声明式(@Transactional)编程式(TransactionTemplate)
实现方式注解 + AOP 代理代码显式调用
侵入性零侵入(注解即可)业务代码中嵌入事务逻辑
粒度方法级别代码块级别(任意范围)
适用场景绝大多数业务方法需要精确控制事务边界
可读性高(意图清晰)较低(模板代码多)

编程式事务的两种用法

TransactionTemplate · 推荐用法
@Service
public class PaymentService {
    @Autowired private TransactionTemplate txTemplate;

    public void pay(Order order) {
        // 外层:非事务逻辑
        validateOrder(order);

        // 内层:精确控制事务范围
        String result = txTemplate.execute(status -> {
            try {
                accountDao.deduct(order.getUserId(), order.getAmount());
                paymentDao.insert(new Payment(order));
                return "SUCCESS";
            } catch (Exception e) {
                status.setRollbackOnly();  // 手动标记回滚
                return "FAILED: " + e.getMessage();
            }
        });
        // 事务已结束,继续非事务逻辑
        notifyUser(order, result);
    }
}
PlatformTransactionManager · 底层用法
@Autowired private PlatformTransactionManager txManager;

public void manualTx() {
    DefaultTransactionDefinition def = new DefaultTransactionDefinition();
    def.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW);
    def.setIsolationLevel(TransactionDefinition.ISOLATION_READ_COMMITTED);

    TransactionStatus status = txManager.getTransaction(def);
    try {
        // 业务 SQL
        orderDao.insert(order);
        txManager.commit(status);
    } catch (Exception e) {
        txManager.rollback(status);
        throw e;
    }
}

什么时候用编程式事务?

  • 事务边界不在方法级别——比如循环中每条记录独立事务
  • 需要根据运行时条件动态决定事务策略
  • 只需要对方法中的一小段代码加事务,避免整个方法都在事务中(长事务问题)
  • 避免自调用失效——编程式事务不依赖代理
选型建议

"日常开发 95% 用 @Transactional 声明式即可。需要精确控制事务边界、避免长事务、或在循环中管理事务时,用 TransactionTemplate。底层 PlatformTransactionManager 直接操作适合框架开发,业务代码中很少使用。"

第 7 站

总结:事务面试全景图

事务是 Spring 面试的"必争之地",因为它是最容易踩坑的基础设施。掌握以下内容足以应对绝大多数面试追问:

事务有效性 = 代理生效 + 异常传播 + 引擎支持
三者缺一,@Transactional 就是摆设
失效场景根因解决方案
自调用this 绕过代理注入自身 / AopContext.currentProxy()
private 方法CGLIB 无法重写改为 public
非 public(旧版本)Spring 5.x 仅支持 public升级到 Spring 6 / 改为 public
异常类型不匹配默认只回滚 RuntimeExceptionrollbackFor = Exception.class
异常被 catch 吞掉Spring 感知不到异常catch 后重新 throw
数据库引擎不支持MyISAM 无事务改用 InnoDB
Bean 未被 Spring 管理无代理对象加 @Service / @Component
传播行为设置错误NOT_SUPPORTED / NEVER检查 propagation 配置

最佳实践清单

  • 统一使用 rollbackFor = Exception.class,避免 Checked Exception 不回滚
  • 事务方法只做数据库操作,避免 RPC、文件 IO 等耗时操作(防止长事务)
  • REQUIRES_NEW 用于审计日志等"必须持久化"的场景,不要滥用
  • NESTED 依赖 JDBC Savepoint,确认数据库驱动支持后再用
  • 同类方法调用必须通过代理对象,不要 this.xxx()
  • 事务粒度尽量小——方法越短,锁持有时间越短,并发性能越好

面试回答模板

Q:@Transactional 在哪些情况下会失效?

"@Transactional 基于 AOP 代理实现,失效场景主要有八类:一、自调用——this.method() 绕过代理对象;二、private 方法——CGLIB 无法拦截;三、非 public 方法(Spring 5.x 限制);四、异常类型不匹配,默认只回滚 RuntimeException;五、异常被 catch 吞掉;六、数据库引擎不支持事务如 MyISAM;七、Bean 未被 Spring 容器管理;八、传播行为配置错误如 NOT_SUPPORTED。根因可以归结为:代理未生效、异常未传播、引擎不支持。"

Q:REQUIRED、REQUIRES_NEW、NESTED 有什么区别?

"REQUIRED 是默认行为,有事务就加入,没有就新建,内外层共享同一个物理事务,一起回滚。REQUIRES_NEW 总是创建独立的新事务,挂起外层事务,内层提交后不受外层回滚影响,适合审计日志。NESTED 在同一个物理事务中通过 Savepoint 实现逻辑子事务,内层失败只回滚到 Savepoint,但外层失败时内层也会一起回滚。三者核心区别在于回滚边界不同。"

Q:编程式事务和声明式事务怎么选?

"声明式事务 @Transactional 适合绝大多数场景,零侵入、意图清晰。编程式事务用 TransactionTemplate,适合需要精确控制事务边界的情况,比如循环中每条记录独立事务、方法中只有一小段需要事务、或者避免自调用失效。底层 PlatformTransactionManager 直接操作一般只在框架开发中使用。"