问题引入
2024 年初,某电商平台的订单系统出现了一个诡异 bug:用户下单后支付失败,订单状态却变成了"已支付"。技术团队排查日志发现——支付异常被 try-catch 捕获后打印了错误日志,但订单服务的方法上明明有 @Transactional 注解,异常却没有触发回滚。
java
@Transactional
public void createOrder(OrderDTO dto) {
orderMapper.insert(order);
try {
paymentService.charge(order.getAmount()); // 抛出 PaymentException
} catch (PaymentException e) {
log.error("支付失败", e); // 异常被吞掉,事务不回滚
}
orderMapper.updateStatus(order.getId(), "PAID");
}
这不是个例。Spring AOP 和事务是面试的绝对核心,但追问下去:
- JDK 动态代理和 CGLIB 有什么区别?SpringBoot 2.x 默认用哪个?
- 事务传播机制有哪些?REQUIRES_NEW 和 NESTED 到底有什么区别?
- @Transactional 什么情况下会失效?同类内部调用为什么事务不生效?
- 声明式事务的底层是怎么实现的?TransactionInterceptor 在哪里介入?
本文从 AOP 代理原理出发,穿透事务传播的每一个细节,揭示 @Transactional 失效的根本原因。
核心概念
1. AOP 核心概念
| 术语 | 英文 | 含义 |
|---|---|---|
| 切面 | Aspect | 横切关注点的模块化,包含通知和切入点 |
| 连接点 | JoinPoint | 程序执行过程中可以插入切面的点(方法调用、异常抛出等) |
| 切入点 | Pointcut | 匹配连接点的表达式,决定"在哪里切" |
| 通知 | Advice | 切面在特定连接点上执行的动作,决定"做什么" |
| 目标对象 | Target | 被代理的原始对象 |
| 代理 | Proxy | AOP 框架创建的对象,包含目标对象和切面逻辑 |
2. 代理方式:JDK 动态代理 vs CGLIB
| 维度 | JDK 动态代理 | CGLIB |
|---|---|---|
| 原理 | 基于接口,生成实现相同接口的代理类 | 基于继承,生成目标类的子类 |
| 依赖 | 目标类必须实现接口 | 目标类不能是 final |
| 方法限制 | 只能代理接口方法 | 不能代理 final/private 方法 |
| 性能 | 反射调用,略慢 | 字节码生成,略快(但启动慢) |
| 生成方式 | Proxy.newProxyInstance() |
Enhancer.create()(ASM 字节码) |
JDK 动态代理 CGLIB 代理
┌─────────────────┐ ┌─────────────────┐
│ OrderService │ │ OrderService │
│ (接口) │ │ (原始类) │
└────────┬────────┘ └────────┬────────┘
│ │
│ 实现 │ 继承
▼ ▼
┌─────────────────┐ ┌─────────────────┐
│ $Proxy0 │ │ OrderService$$ │
│ implements │ │ EnhancerByCGLIB │
│ OrderService │ │ extends │
│ │ │ OrderService │
│ InvocationHandler│ │ │
│ invoke() │ │ MethodInterceptor│
│ ↓ │ │ intercept() │
│ 目标方法 │ │ ↓ │
└─────────────────┘ │ 目标方法(super)│
└─────────────────┘
读图导引:左图是 JDK 代理——代理类实现和目标类相同的接口,所有方法调用都路由到 InvocationHandler。右图是 CGLIB——代理类继承目标类,重写非 final 方法,方法调用路由到 MethodInterceptor。
Spring 的代理选择策略:
- 目标类实现了接口:默认使用 JDK 代理
- 目标类没有接口:使用 CGLIB
- SpringBoot 2.x:默认全部使用 CGLIB(
spring.aop.proxy-target-class=true) - 强制 JDK 代理:
@EnableAspectJAutoProxy(proxyTargetClass = false)
3. 通知类型
| 通知 | 注解 | 执行时机 |
|---|---|---|
| 前置通知 | @Before | 目标方法执行前 |
| 后置通知 | @After | 目标方法执行后(无论是否异常) |
| 返回通知 | @AfterReturning | 目标方法正常返回后 |
| 异常通知 | @AfterThrowing | 目标方法抛出异常后 |
| 环绕通知 | @Around | 包裹目标方法,可控制是否执行 |
目标方法正常执行 目标方法抛出异常
│ │
▼ ▼
┌──────────┐ ┌──────────┐
│ @Before │ │ @Before │
└────┬─────┘ └────┬─────┘
│ │
┌────▼─────┐ ┌────▼─────┐
│ @Around │ │ @Around │
│ 前半部分 │ │ 前半部分 │
│ ↓ │ │ ↓ │
│ 目标方法 │ │ 目标方法 │
│ ↓ │ │ ↓ │
│ 正常返回 │ │ 抛出异常 │
│ ↓ │ │ ↓ │
│ 后半部分 │ │ 后半部分 │
└────┬─────┘ └────┬─────┘
│ │
┌────▼─────┐ ┌────▼─────┐
│@After │ │@After │
└────┬─────┘ └────┬─────┘
│ │
┌────▼─────┐ ┌────▼─────┐
│@AfterReturning│ │@AfterThrowing│
└──────────┘ └──────────┘
读图导引:正常执行时,通知按 Before → Around(前) → 目标方法 → Around(后) → After → AfterReturning 的顺序执行。抛出异常时,AfterReturning 被跳过,执行 AfterThrowing。
4. 事务传播机制
事务传播(Propagation)定义了当前事务方法被调用时,如何与已有事务交互。
| 传播行为 | 含义 | 典型场景 |
|---|---|---|
| REQUIRED(默认) | 当前有事务就加入,没有就新建 | 绝大多数业务方法 |
| SUPPORTS | 当前有事务就加入,没有就以非事务执行 | 查询方法 |
| MANDATORY | 当前必须有事务,否则抛异常 | 强依赖上层事务的子方法 |
| REQUIRES_NEW | 挂起当前事务,新建独立事务 | 日志记录(不受主事务回滚影响) |
| NOT_SUPPORTED | 挂起当前事务,以非事务执行 | 不需要事务的辅助操作 |
| NEVER | 当前必须没有事务,否则抛异常 | 纯查询,禁止事务上下文 |
| NESTED | 在当前事务中创建 savepoint 嵌套事务 | 部分回滚(如订单主表成功、明细失败) |
传播行为决策树
当前是否存在事务?
├── 是
│ ├── REQUIRED → 加入当前事务
│ ├── SUPPORTS → 加入当前事务
│ ├── MANDATORY → 加入当前事务
│ ├── REQUIRES_NEW → 挂起当前事务,创建新事务
│ ├── NOT_SUPPORTED → 挂起当前事务,非事务执行
│ ├── NEVER → 抛出异常
│ └── NESTED → 在当前事务中创建 savepoint
└── 否
├── REQUIRED → 创建新事务
├── SUPPORTS → 非事务执行
├── MANDATORY → 抛出异常
├── REQUIRES_NEW → 创建新事务
├── NOT_SUPPORTED → 非事务执行
├── NEVER → 非事务执行
└── NESTED → 创建新事务(等价于 REQUIRED)
读图导引:REQUIRED 是默认且最常用的传播行为。REQUIRES_NEW 和 NESTED 是最容易被混淆的两个——前者创建完全独立的新事务,后者在当前事务中创建 savepoint。
5. 事务隔离级别
Spring 的事务隔离级别直接映射数据库隔离级别:
| 隔离级别 | 脏读 | 不可重复读 | 幻读 |
|---|---|---|---|
| READ_UNCOMMITTED | 允许 | 允许 | 允许 |
| READ_COMMITTED | 禁止 | 允许 | 允许 |
| REPEATABLE_READ | 禁止 | 禁止 | 允许 |
| SERIALIZABLE | 禁止 | 禁止 | 禁止 |
| DEFAULT | 使用数据库默认(MySQL RR,Oracle RC) |
原理分析
1. AOP 代理的创建时机
AOP 代理不是在 Bean 实例化时创建的,而是在初始化后的 BeanPostProcessor 后置处理阶段。
Bean 生命周期中的 AOP 介入点
① 实例化
↓
② 属性填充
↓
③ 初始化
├── Aware 回调
├── BeanPostProcessor.before()
├── @PostConstruct / afterPropertiesSet()
├── BeanPostProcessor.after() ← AOP 代理在此创建!
│ AbstractAutoProxyCreator
│ postProcessAfterInitialization()
│ ↓
│ 检查是否需要代理
│ (匹配 Pointcut)
│ ↓
│ 创建代理对象
│ (JDK 或 CGLIB)
│ ↓
│ 用代理对象替代原始 Bean
│
④ 使用
↓
⑤ 销毁
读图导引:AOP 代理在 BeanPostProcessor 的后置处理阶段创建。这意味着原始 Bean 已经完成了属性填充和初始化,代理对象包装的是"已经准备好的 Bean"。
AbstractAutoProxyCreator 的核心逻辑:
java
public Object postProcessAfterInitialization(@Nullable Object bean, String beanName) {
if (bean != null) {
// 获取缓存 key
Object cacheKey = getCacheKey(bean.getClass(), beanName);
// 检查是否已经代理过
if (this.earlyProxyReferences.remove(cacheKey) != bean) {
// 判断是否需要代理(匹配 Advisor)
return wrapIfNecessary(bean, beanName, cacheKey);
}
}
return bean;
}
protected Object wrapIfNecessary(Object bean, String beanName, Object cacheKey) {
// 获取适用于当前 Bean 的 Advisor 列表
Object[] specificInterceptors = getAdvicesAndAdvisorsForBean(
bean.getClass(), beanName, null);
if (specificInterceptors != DO_NOT_PROXY) {
this.advisedBeans.put(cacheKey, Boolean.TRUE);
// 创建代理对象
Object proxy = createProxy(
bean.getClass(), beanName, specificInterceptors,
new SingletonTargetSource(bean));
this.proxyTypes.put(cacheKey, proxy.getClass());
return proxy; // 用代理对象替代原始 Bean
}
this.advisedBeans.put(cacheKey, Boolean.FALSE);
return bean; // 不需要代理,返回原始 Bean
}
2. JDK 动态代理与 CGLIB 的本质区别
JDK 动态代理
java
// InvocationHandler 实现
public class JdkProxy implements InvocationHandler {
private final Object target;
public JdkProxy(Object target) {
this.target = target;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("前置通知");
Object result = method.invoke(target, args); // 调用目标方法
System.out.println("后置通知");
return result;
}
}
// 创建代理
OrderService target = new OrderServiceImpl();
OrderService proxy = (OrderService) Proxy.newProxyInstance(
target.getClass().getClassLoader(),
target.getClass().getInterfaces(), // 必须实现接口
new JdkProxy(target)
);
proxy.createOrder(); // 走代理
特点:
- 代理类实现了和目标类相同的接口
- 只能代理接口中定义的方法
- 通过反射调用目标方法
CGLIB 代理
java
// MethodInterceptor 实现
public class CglibProxy implements MethodInterceptor {
@Override
public Object intercept(Object obj, Method method, Object[] args,
MethodProxy proxy) throws Throwable {
System.out.println("前置通知");
Object result = proxy.invokeSuper(obj, args); // 调用父类(目标类)方法
System.out.println("后置通知");
return result;
}
}
// 创建代理
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(OrderService.class); // 继承目标类
enhancer.setCallback(new CglibProxy());
OrderService proxy = (OrderService) enhancer.create();
proxy.createOrder(); // 走代理
特点:
- 代理类继承目标类(
extends OrderService) - 可以代理类中的所有非 final、非 private 方法
- 使用
MethodProxy.invokeSuper()调用父类方法,比反射更快(通过 FastClass 机制)
FastClass 机制:
CGLIB 为代理类和目标类各生成一个 FastClass,为每个方法分配一个索引号,调用时直接通过索引定位方法,避免了反射的开销:
java
// CGLIB 生成的 FastClass 伪代码
public int getIndex(String methodName, Class[] params) {
// 为每个方法返回唯一的索引
if (methodName.equals("createOrder") && params.length == 0) return 12;
// ...
}
public Object invoke(int index, Object target, Object[] args) {
// 通过索引直接调用,无需反射
switch (index) {
case 12: return ((OrderService)target).createOrder();
// ...
}
}
3. 声明式事务的底层实现
@EnableTransactionManagement 做了什么
java
@Import(TransactionManagementConfigurationSelector.class)
public @interface EnableTransactionManagement {
// proxyTargetClass: 是否强制使用 CGLIB
// mode: PROXY(默认)或 ASPECTJ
// order: 事务切面的优先级
}
TransactionManagementConfigurationSelector 会导入两个关键配置类:
AutoProxyRegistrar:注册InfrastructureAdvisorAutoProxyCreator(用于创建事务代理的 BeanPostProcessor)ProxyTransactionManagementConfiguration:定义事务相关的 Advisor 和 Interceptor
事务代理的执行流程
调用代理方法
│
▼
┌─────────────────────────┐
│ TransactionInterceptor │
│ invoke() │
└────┬────────────────────┘
│
▼
┌─────────────────────────┐
│ TransactionAspectSupport│
│ invokeWithinTransaction│
└────┬────────────────────┘
│
├── 1. 获取事务属性(@Transactional 注解信息)
│
├── 2. 获取事务管理器(PlatformTransactionManager)
│
├── 3. 判断当前是否存在事务
│ └── 根据传播行为决定:加入、新建、挂起、异常...
│
├── 4. 开启事务(获取 Connection,设置 autoCommit=false)
│
├── 5. 执行目标方法(invocation.proceed())
│ └── 目标方法执行
│
├── 6. 判断是否回滚
│ └── 发生异常?异常类型匹配 rollbackFor?
│ ├── 是 → 回滚事务(Connection.rollback())
│ └── 否 → 提交事务(Connection.commit())
│
└── 7. 清理事务资源(释放 Connection)
读图导引:事务代理的核心是 TransactionInterceptor,它在方法调用前后包裹事务逻辑。关键步骤是"判断传播行为 → 开启事务 → 执行业务 → 根据异常决定是否回滚"。
事务同步管理器:
java
// TransactionSynchronizationManager 维护线程绑定的事务资源
private static final ThreadLocal<Map<Object, Object>> resources =
new NamedThreadLocal<>("Transactional resources");
// 当前线程绑定的数据库连接
ConnectionHolder holder = (ConnectionHolder) TransactionSynchronizationManager
.getResource(dataSource);
TransactionSynchronizationManager 用 ThreadLocal 将数据库连接绑定到线程,保证同一个事务内的多个 DAO 操作使用同一个 Connection。
4. @Transactional 失效的 8 种场景
@Transactional 失效场景分析
┌─────────────────────────────────────────────────────────────┐
│ 场景1:非 public 方法 │
│ 原因:@Transactional 只能作用于 public 方法(JDK 代理限制) │
│ 解决:改为 public │
├─────────────────────────────────────────────────────────────┤
│ 场景2:同类内部调用(this 调用) │
│ 原因:this 指向原始对象,绕过代理层 │
│ 解决:注入自身代理 / 拆分到另一个类 │
├─────────────────────────────────────────────────────────────┤
│ 场景3:异常被吞掉(try-catch 未抛出) │
│ 原因:事务切面只能拦截抛出的异常 │
│ 解决:catch 后重新抛出 / 手动设置回滚 │
├─────────────────────────────────────────────────────────────┤
│ 场景4:rollbackFor 配置错误 │
│ 原因:默认只回滚 RuntimeException, checked 异常不回滚 │
│ 解决:@Transactional(rollbackFor = Exception.class) │
├─────────────────────────────────────────────────────────────┤
│ 场景5:数据库引擎不支持事务 │
│ 原因:MyISAM 不支持事务,必须用 InnoDB │
│ 解决:改用 InnoDB │
├─────────────────────────────────────────────────────────────┤
│ 场景6:异步方法(@Async) │
│ 原因:异步在新线程执行,ThreadLocal 的事务上下文丢失 │
│ 解决:异步方法单独加事务 / 主方法事务覆盖异步逻辑 │
├─────────────────────────────────────────────────────────────┤
│ 场景7:private/final 方法(CGLIB 限制) │
│ 原因:CGLIB 无法代理 private/final 方法 │
│ 解决:改为 public 非 final │
├─────────────────────────────────────────────────────────────┤
│ 场景8:Spring 容器外调用 │
│ 原因:直接 new 的对象没有代理 │
│ 解决:从容器中获取 Bean │
└─────────────────────────────────────────────────────────────┘
读图导引:8 种失效场景可分为三类——代理限制(场景1、7)、调用方式问题(场景2、8)、异常处理问题(场景3、4)、基础设施问题(场景5、6)。最常见的是场景2(同类内部调用)和场景3(异常被吞)。
场景2详解:同类内部调用为什么失效
java
@Service
public class OrderService {
@Transactional
public void createOrder(Order order) {
orderMapper.insert(order);
// 同类内部调用:this 指向原始对象,不是代理对象
this.updateInventory(order); // 事务注解不生效!
}
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void updateInventory(Order order) {
inventoryService.deduct(order.getItems());
}
}
this.updateInventory() 中的 this 是 OrderService 的原始对象,不是 Spring 创建的代理对象。调用 this.updateInventory() 直接走了原始对象的方法,完全绕过了 TransactionInterceptor,所以 @Transactional 注解被忽略。
解决方案:
java
@Service
public class OrderService {
@Autowired
private OrderService self; // 注入自己的代理对象
@Transactional
public void createOrder(Order order) {
orderMapper.insert(order);
// 通过代理对象调用,事务生效
self.updateInventory(order);
}
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void updateInventory(Order order) {
inventoryService.deduct(order.getItems());
}
}
更优雅的方案是将 updateInventory 拆分到另一个 Service 类中,通过依赖注入调用。
5. REQUIRES_NEW vs NESTED 的深度对比
这是事务传播中最容易被混淆的一对。
REQUIRES_NEW NESTED
┌─────────────────┐ ┌─────────────────┐
│ 外层事务 T1 │ │ 外层事务 T1 │
│ begin │ │ begin │
│ ↓ │ │ ↓ │
│ 操作 A │ │ 操作 A │
│ ↓ │ │ ↓ │
│ ┌───────────┐ │ │ savepoint S1 │ ← 创建 savepoint
│ │ 挂起 T1 │ │ │ ↓ │
│ │ 新建 T2 │ │ │ 操作 B │
│ │ begin │ │ │ ↓ │
│ │ 操作 B │ │ │ 内层异常? │
│ │ commit │ │ │ ├── 是 → rollback to S1│
│ │ 恢复 T1 │ │ │ └── 否 → 继续 │
│ └───────────┘ │ │ ↓ │
│ ↓ │ │ 操作 C │
│ 操作 C │ │ ↓ │
│ commit │ │ commit │
└─────────────────┘ └─────────────────┘
读图导引:REQUIRES_NEW 会挂起外层事务、创建完全独立的新事务,新事务的提交和回滚不影响外层。NESTED 不创建新事务,而是在当前事务中设置 savepoint,内层回滚只回滚到 savepoint,不影响外层已完成的操作。
关键区别:
| 维度 | REQUIRES_NEW | NESTED |
|---|---|---|
| 事务独立性 | 完全独立的新事务 | 仍是同一个事务,只是有 savepoint |
| 数据库连接 | 获取新连接 | 使用同一个连接 |
| 外层回滚影响 | 不影响内层(内层已独立提交) | 外层回滚会连带内层 |
| 内层回滚影响 | 不影响外层 | 只回滚到 savepoint,不影响外层 |
| 实现依赖 | 标准事务 | JDBC savepoint |
| 使用场景 | 日志记录(必须保存) | 部分回滚(订单主表保留,明细回滚) |
REQUIRES_NEW 的陷阱:连接池耗尽
java
@Service
public class OrderService {
@Transactional
public void batchCreate(List<Order> orders) {
for (Order order : orders) {
// 每次循环都挂起外层事务、创建新事务
// 如果连接池大小为 20,循环 21 次就会耗尽
logService.saveLog(order); // REQUIRES_NEW
}
}
}
REQUIRES_NEW 每次都会从连接池获取新连接,如果外层事务持有连接不放,同时内层又不断申请新连接,可能导致连接池耗尽。
实战/源码
1. 自定义 AOP 日志切面
java
@Aspect
@Component
public class LogAspect {
@Pointcut("@annotation(com.example.annotation.LogOperation)")
public void logPointcut() {}
@Around("logPointcut()")
public Object around(ProceedingJoinPoint point) throws Throwable {
long start = System.currentTimeMillis();
String methodName = point.getSignature().getName();
String className = point.getTarget().getClass().getSimpleName();
try {
Object result = point.proceed();
long cost = System.currentTimeMillis() - start;
System.out.printf("[SUCCESS] %s.%s cost=%dms%n", className, methodName, cost);
return result;
} catch (Throwable e) {
long cost = System.currentTimeMillis() - start;
System.out.printf("[FAILED] %s.%s cost=%dms, error=%s%n",
className, methodName, cost, e.getMessage());
throw e;
}
}
}
// 使用
@LogOperation
public void createOrder(Order order) {
// ...
}
2. 事务传播行为验证
java
@Service
public class OuterService {
@Autowired
private InnerService innerService;
@Autowired
private JdbcTemplate jdbcTemplate;
// 测试 REQUIRED(默认)
@Transactional
public void testRequired() {
jdbcTemplate.update("INSERT INTO test VALUES (1)");
innerService.required(); // 加入当前事务
throw new RuntimeException("外层回滚,内层也回滚");
}
// 测试 REQUIRES_NEW
@Transactional
public void testRequiresNew() {
jdbcTemplate.update("INSERT INTO test VALUES (1)");
try {
innerService.requiresNew(); // 独立事务,会提交
} catch (Exception e) {
// 内层异常不影响外层
}
throw new RuntimeException("外层回滚,但内层已独立提交");
}
// 测试 NESTED
@Transactional
public void testNested() {
jdbcTemplate.update("INSERT INTO test VALUES (1)");
try {
innerService.nested(); // 创建 savepoint
} catch (Exception e) {
// 内层回滚到 savepoint,外层继续
}
jdbcTemplate.update("INSERT INTO test VALUES (3)"); // 这条会保留
}
}
@Service
public class InnerService {
@Autowired
private JdbcTemplate jdbcTemplate;
@Transactional(propagation = Propagation.REQUIRED)
public void required() {
jdbcTemplate.update("INSERT INTO test VALUES (2)");
}
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void requiresNew() {
jdbcTemplate.update("INSERT INTO test VALUES (2)");
}
@Transactional(propagation = Propagation.NESTED)
public void nested() {
jdbcTemplate.update("INSERT INTO test VALUES (2)");
throw new RuntimeException("内层异常,回滚到 savepoint");
}
}
验证结果:
| 测试 | test 表中的数据 | 说明 |
|---|---|---|
| testRequired | 无 | 内外层同一个事务,一起回滚 |
| testRequiresNew | 只有 2 | 内层独立事务提交,外层回滚 |
| testNested | 1 和 3 | 内层回滚到 savepoint(2 消失),外层继续 |
3. @Transactional 失效场景复现与修复
java
@Service
public class TransactionDemoService {
// ========== 场景2:同类内部调用 ==========
@Transactional
public void outerMethod() {
jdbcTemplate.update("INSERT INTO test VALUES (1)");
// 问题:this 指向原始对象,updateStatus 的事务注解不生效
this.updateStatus();
}
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void updateStatus() {
jdbcTemplate.update("INSERT INTO test VALUES (2)");
throw new RuntimeException("内层异常");
}
// ========== 修复方案1:注入自身代理 ==========
@Autowired
private TransactionDemoService self;
@Transactional
public void outerMethodFixed() {
jdbcTemplate.update("INSERT INTO test VALUES (1)");
// 通过代理对象调用,事务生效
self.updateStatus();
}
// ========== 修复方案2:拆分到另一个类 ==========
@Autowired
private StatusService statusService;
@Transactional
public void outerMethodBestPractice() {
jdbcTemplate.update("INSERT INTO test VALUES (1)");
statusService.updateStatus(); // 通过依赖注入调用,走代理
}
// ========== 场景3:异常被吞掉 ==========
@Transactional
public void swallowException() {
jdbcTemplate.update("INSERT INTO test VALUES (1)");
try {
riskyOperation();
} catch (Exception e) {
log.error("出错", e); // 异常被吞,事务不回滚!
}
}
// 修复:重新抛出 或 手动回滚
@Transactional
public void fixSwallowedException() {
jdbcTemplate.update("INSERT INTO test VALUES (1)");
try {
riskyOperation();
} catch (Exception e) {
log.error("出错", e);
throw new RuntimeException(e); // 重新抛出
// 或者:TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
}
}
}
4. TransactionInterceptor 源码精简版
java
public class TransactionInterceptor extends TransactionAspectSupport implements MethodInterceptor {
@Override
public Object invoke(MethodInvocation invocation) throws Throwable {
Class<?> targetClass = (invocation.getThis() != null ?
AopUtils.getTargetClass(invocation.getThis()) : null);
// 委托给父类的核心方法
return invokeWithinTransaction(invocation.getMethod(), targetClass,
invocation::proceed);
}
}
// TransactionAspectSupport 的核心逻辑
protected Object invokeWithinTransaction(Method method, Class<?> targetClass,
final InvocationCallback invocation) {
// 1. 获取事务属性
TransactionAttribute txAttr = getTransactionAttributeSource()
.getTransactionAttribute(method, targetClass);
// 2. 获取事务管理器
PlatformTransactionManager tm = determineTransactionManager(txAttr);
// 3. 创建事务信息(包含传播行为判断)
TransactionInfo txInfo = createTransactionIfNecessary(tm, txAttr, methodName);
Object retVal;
try {
// 4. 执行目标方法
retVal = invocation.proceedWithInvocation();
} catch (Throwable ex) {
// 5. 异常时回滚
completeTransactionAfterThrowing(txInfo, ex);
throw ex;
} finally {
cleanupTransactionInfo(txInfo);
}
// 6. 正常时提交
commitTransactionAfterReturning(txInfo);
return retVal;
}
常见问题
Q1:SpringBoot 2.x 为什么默认使用 CGLIB?
答:SpringBoot 2.x 将 spring.aop.proxy-target-class 默认为 true,原因:
- 避免代理类型转换问题:JDK 代理返回的是接口类型,如果代码中注入的是实现类类型,会报
ClassCastException - 一致性:无论有没有接口,都用同一种代理方式,行为更一致
- 性能:CGLIB 的 FastClass 机制在频繁调用时性能优于 JDK 反射
副作用:CGLIB 不能代理 final 和 private 方法,如果业务类有 final 方法需要被 AOP 拦截,会失效。
Q2:事务方法里调用 RPC 或发 MQ,RPC 失败会回滚吗?
答:不会。Spring 的声明式事务只管理数据库连接(通过 ThreadLocal 绑定的 Connection),RPC 调用、MQ 发送、Redis 操作都不在事务范围内。
java
@Transactional
public void createOrder(Order order) {
orderMapper.insert(order); // 在事务中
rpcService.notifyWarehouse(order); // 不在事务中!
mqService.sendOrderMessage(order); // 不在事务中!
redisTemplate.opsForValue().set(...);// 不在事务中!
}
如果 RPC 失败后数据库回滚了,但 RPC 请求可能已经到达对端并执行。这就是分布式事务问题,需要用 TCC、本地消息表、Seata 等方案解决。
Q3:@Transactional 的 rollbackFor 怎么配置最稳妥?
答:默认配置 @Transactional 只回滚 RuntimeException 和 Error,checked 异常(如 IOException、SQLException)不会触发回滚。
最稳妥的配置:
java
@Transactional(rollbackFor = Exception.class) // 所有异常都回滚
public void createOrder(Order order) {
// ...
}
如果某些异常明确不需要回滚,用 noRollbackFor:
java
@Transactional(
rollbackFor = Exception.class,
noRollbackFor = BusinessException.class // 业务异常不触发回滚
)
Q4:长事务有什么危害?怎么监控?
答:长事务是生产环境的隐形杀手:
- 连接池耗尽:事务持有数据库连接不释放,其他请求无法获取连接
- 锁竞争:长事务持有的行锁/间隙锁长时间不释放,阻塞其他事务
- undo log 堆积:InnoDB 需要维护长事务开始时的数据版本,purge 线程无法清理
监控方法:
sql
-- MySQL:查看运行中的事务
SELECT * FROM information_schema.innodb_trx
WHERE TIME_TO_SEC(timediff(now(), trx_started)) > 10; -- 超过 10 秒
-- 应用层:设置事务超时
@Transactional(timeout = 5) // 5 秒超时,自动回滚
最佳实践:
- 事务方法只做数据库操作,RPC/MQ 在事务外执行
- 大查询不要放在事务里
- 设置合理的超时时间(
@Transactional(timeout = 10)) - 监控
innodb_trx表,发现长事务及时告警
Q5:没有接口的类怎么强制使用 JDK 代理?
答:JDK 代理必须基于接口,没有接口的类无法用 JDK 代理。如果业务需要接口契约(如需要精确控制哪些方法暴露),应该先定义接口:
java
public interface OrderService {
void createOrder(Order order);
}
@Service
public class OrderServiceImpl implements OrderService {
@Override
@Transactional
public void createOrder(Order order) {
// ...
}
}
然后强制 JDK 代理:
java
@SpringBootApplication
@EnableAspectJAutoProxy(proxyTargetClass = false) // 强制 JDK 代理
public class Application {
// ...
}
总结
Spring AOP 和事务不是魔法,而是基于代理模式的精巧设计:
- AOP 代理在 BeanPostProcessor 后置处理阶段创建:原始 Bean 完成初始化后,如果匹配了 Pointcut,Spring 用 JDK 或 CGLIB 生成代理对象替代原始 Bean
- JDK 代理基于接口,CGLIB 基于继承:JDK 代理要求目标类实现接口,代理类也实现相同接口;CGLIB 生成目标类的子类,重写非 final 方法。SpringBoot 2.x 默认 CGLIB
- 声明式事务的底层是 AOP + ThreadLocal:@Transactional 的方法被调用时,TransactionInterceptor 拦截调用,通过 TransactionSynchronizationManager 将数据库连接绑定到线程,保证同一个事务内的操作共用同一个 Connection
- @Transactional 失效的根本原因是"绕过了代理":同类内部调用走 this(原始对象),异常被 try-catch 吞掉,非 public 方法无法被代理——这些都是让事务切面无法介入的场景
- REQUIRES_NEW 和 NESTED 有本质区别:前者创建完全独立的新事务(新连接),后者在当前事务中创建 savepoint(同一连接)。REQUIRES_NEW 滥用会导致连接池耗尽,NESTED 依赖 JDBC savepoint 支持
理解 AOP 和事务后,下一个追问自然指向 SpringBoot——因为 SpringBoot 的自动配置大量使用了条件注解和 AOP 代理,内嵌 Tomcat 的启动也依赖于 ApplicationContext 的刷新流程。