Lesson 55 · Spring 生态核心原理
Spring 事务传播机制与 @Transactional 失效的 8 种场景
面试官:@Transactional 在什么情况下会失效?
Spring 的事务管理是日常开发中最常用的基础设施之一。只需在方法上加一个 @Transactional,Spring 就帮我们搞定事务的开启、提交和回滚。但"简单"背后藏着大量陷阱:
- 自调用失效:同类中 A 方法调用 B 方法,B 上的 @Transactional 不生效
- 异常类型不匹配:默认只回滚 RuntimeException,Checked Exception 不回滚
- private 方法:Spring AOP 基于代理,无法拦截 private 方法
- 异常被吞掉:方法内部 catch 了异常没抛出,Spring 感知不到
更让人困惑的是事务传播行为——REQUIRED、REQUIRES_NEW、NESTED 到底有什么区别?什么时候该用哪种?理解这些需要从事务的底层机制出发,而非死记硬背。
@EnableTransactionManagement → TransactionInterceptor(AOP 代理)→ PlatformTransactionManager → JDBC Connection 管理(setAutoCommit / commit / rollback)
本文以面试高频问题为主线,系统拆解 7 种传播行为、8 种失效场景、事务实现原理,让你面对事务问题时有底层支撑。
7 种事务传播行为:一图看懂
事务传播行为(Propagation)定义了当一个事务方法被另一个事务方法调用时,事务应该如何传播。Spring 定义了 7 种,全部在 Propagation 枚举中:
| 传播行为 | 有事务时 | 无事务时 | 使用频率 |
|---|---|---|---|
| REQUIRED(默认) | 加入当前事务 | 创建新事务 | 90% 场景 |
| SUPPORTS | 加入当前事务 | 非事务执行 | 读操作优化 |
| MANDATORY | 加入当前事务 | 抛 IllegalTransactionStateException | 强制调用方开事务 |
| REQUIRES_NEW | 挂起当前事务,创建新事务 | 创建新事务 | 日志、审计 |
| NOT_SUPPORTED | 挂起当前事务,非事务执行 | 非事务执行 | 极少使用 |
| NEVER | 抛 IllegalTransactionStateException | 非事务执行 | 极少使用 |
| NESTED | 在当前事务中创建 Savepoint | 等同于 REQUIRED | 部分回滚 |
REQUIRES_NEW 和 NOT_SUPPORTED 需要"挂起"当前事务。Spring 通过 TransactionSynchronizationManager 保存当前线程绑定的 ConnectionHolder,然后将其从 ThreadLocal 中移除,待内层事务结束后再恢复。挂起本质是解绑当前线程与数据库连接的关联。
REQUIRED vs REQUIRES_NEW vs NESTED:回滚边界
面试中最高频的追问就是这三者的区别。核心差异在于回滚边界:
@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 的关键:内层方法加入外层事务,任何一层抛异常,整个事务一起回滚。适用于大多数业务场景——订单和库存要么都成功,要么都失败。
@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 的关键:挂起外层事务,开启全新事务。内层提交后即使外层回滚,内层数据依然持久化。典型场景:审计日志、操作记录——业务失败也要留痕。
@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)。
| 维度 | REQUIRED | REQUIRES_NEW | NESTED |
|---|---|---|---|
| 物理事务 | 同一个 | 两个独立事务 | 同一个 |
| Connection | 共享 | 各用各的 | 共享 |
| 内层失败 | 整个事务回滚 | 只回滚内层 | 回滚到 Savepoint |
| 外层失败 | 全部回滚 | 内层不受影响 | 全部回滚 |
| 典型场景 | 订单+库存 | 审计日志 | 批量处理(部分失败不阻断) |
| JDBC 依赖 | 无特殊要求 | 需要多连接池 | 需要驱动支持 Savepoint |
"REQUIRED 共享同一个事务,一起提交一起回滚。REQUIRES_NEW 是两个独立事务,内层提交后不受外层影响。NESTED 在同一个事务中通过 Savepoint 实现部分回滚,外层失败时内层也会回滚。选哪种取决于业务对回滚边界的要求。"
@Transactional 失效的 8 种场景
理解失效场景的前提是理解 @Transactional 的生效条件:方法必须通过 Spring 代理对象调用,代理拦截方法后由 TransactionInterceptor 管理事务。一旦绕过代理或代理无法控制事务边界,就会失效。
场景 1:自调用(Self-invocation)—— 最高频踩坑
@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 默认只对 RuntimeException 和 Error 回滚。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 管理
// ★ 没有 @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(); // 不回滚
}
1. 自调用(this 绕过代理) 2. private 方法(无法被代理) 3. 非 public 方法(旧版本限制) 4. 异常类型不匹配(默认只回滚 RuntimeException) 5. 异常被 catch 吞掉 6. 数据库引擎不支持(MyISAM) 7. Bean 未被 Spring 管理 8. 传播行为配置错误(如 NOT_SUPPORTED)
事务实现原理:从注解到 Connection
@Transactional 的生效链路可以拆解为四个层次:
@Configuration
@EnableTransactionManagement // 导入 TransactionManagementConfigurationSelector
public class AppConfig {
@Bean
public PlatformTransactionManager transactionManager(DataSource ds) {
return new DataSourceTransactionManager(ds);
}
}
@EnableTransactionManagement 通过 @Import 注册 AutoProxyRegistrar 和 ProxyTransactionManagementConfiguration。前者负责注册 InfrastructureAdvisorAutoProxyCreator(AOP 自动代理创建器),后者注册 BeanFactoryTransactionAttributeSourceAdvisor(事务切面)。
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);
}
}
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;
}
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(); // 回滚
}
事务的 ACID 特性依赖于同一个数据库会话(Session)。如果 DAO-A 用 Connection-1、DAO-B 用 Connection-2,它们是两个独立事务,无法做到原子提交。Spring 通过 TransactionSynchronizationManager(基于 ThreadLocal)将 Connection 绑定到当前线程,确保同一事务内所有 JdbcTemplate / MyBatis 操作拿到同一个连接。
编程式事务 vs 声明式事务
Spring 提供两种事务管理方式:
| 维度 | 声明式(@Transactional) | 编程式(TransactionTemplate) |
|---|---|---|
| 实现方式 | 注解 + AOP 代理 | 代码显式调用 |
| 侵入性 | 零侵入(注解即可) | 业务代码中嵌入事务逻辑 |
| 粒度 | 方法级别 | 代码块级别(任意范围) |
| 适用场景 | 绝大多数业务方法 | 需要精确控制事务边界 |
| 可读性 | 高(意图清晰) | 较低(模板代码多) |
编程式事务的两种用法:
@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);
}
}
@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 直接操作适合框架开发,业务代码中很少使用。"
总结:事务面试全景图
事务是 Spring 面试的"必争之地",因为它是最容易踩坑的基础设施。掌握以下内容足以应对绝大多数面试追问:
三者缺一,@Transactional 就是摆设
| 失效场景 | 根因 | 解决方案 |
|---|---|---|
| 自调用 | this 绕过代理 | 注入自身 / AopContext.currentProxy() |
| private 方法 | CGLIB 无法重写 | 改为 public |
| 非 public(旧版本) | Spring 5.x 仅支持 public | 升级到 Spring 6 / 改为 public |
| 异常类型不匹配 | 默认只回滚 RuntimeException | rollbackFor = 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 直接操作一般只在框架开发中使用。"