问题引入

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() 中的 thisOrderService原始对象,不是 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,原因:

  1. 避免代理类型转换问题:JDK 代理返回的是接口类型,如果代码中注入的是实现类类型,会报 ClassCastException
  2. 一致性:无论有没有接口,都用同一种代理方式,行为更一致
  3. 性能:CGLIB 的 FastClass 机制在频繁调用时性能优于 JDK 反射

副作用:CGLIB 不能代理 finalprivate 方法,如果业务类有 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 只回滚 RuntimeExceptionError,checked 异常(如 IOExceptionSQLException)不会触发回滚。

最稳妥的配置:

java 复制代码
@Transactional(rollbackFor = Exception.class)  // 所有异常都回滚
public void createOrder(Order order) {
    // ...
}

如果某些异常明确不需要回滚,用 noRollbackFor

java 复制代码
@Transactional(
    rollbackFor = Exception.class,
    noRollbackFor = BusinessException.class  // 业务异常不触发回滚
)

Q4:长事务有什么危害?怎么监控?

:长事务是生产环境的隐形杀手:

  1. 连接池耗尽:事务持有数据库连接不释放,其他请求无法获取连接
  2. 锁竞争:长事务持有的行锁/间隙锁长时间不释放,阻塞其他事务
  3. 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 和事务不是魔法,而是基于代理模式的精巧设计:

  1. AOP 代理在 BeanPostProcessor 后置处理阶段创建:原始 Bean 完成初始化后,如果匹配了 Pointcut,Spring 用 JDK 或 CGLIB 生成代理对象替代原始 Bean
  2. JDK 代理基于接口,CGLIB 基于继承:JDK 代理要求目标类实现接口,代理类也实现相同接口;CGLIB 生成目标类的子类,重写非 final 方法。SpringBoot 2.x 默认 CGLIB
  3. 声明式事务的底层是 AOP + ThreadLocal:@Transactional 的方法被调用时,TransactionInterceptor 拦截调用,通过 TransactionSynchronizationManager 将数据库连接绑定到线程,保证同一个事务内的操作共用同一个 Connection
  4. @Transactional 失效的根本原因是"绕过了代理":同类内部调用走 this(原始对象),异常被 try-catch 吞掉,非 public 方法无法被代理——这些都是让事务切面无法介入的场景
  5. REQUIRES_NEW 和 NESTED 有本质区别:前者创建完全独立的新事务(新连接),后者在当前事务中创建 savepoint(同一连接)。REQUIRES_NEW 滥用会导致连接池耗尽,NESTED 依赖 JDBC savepoint 支持

理解 AOP 和事务后,下一个追问自然指向 SpringBoot——因为 SpringBoot 的自动配置大量使用了条件注解和 AOP 代理,内嵌 Tomcat 的启动也依赖于 ApplicationContext 的刷新流程。