【关于Spring声明式事务】

"声明式事务"是 Spring 框架中最核心、也是日常开发中使用频率最高的特性之一。

简单来说,声明式事务就是"告诉框架我要做什么,而不是具体怎么做" 。它将事务管理的代码从业务逻辑中完全剥离出来,开发者只需要通过注解(如 @Transactional) 或 XML 配置声明一下,框架就会自动接管事务的开启、提交和回滚。

为了让你在面试或实际应用中彻底讲透这个概念,我将从对比、底层原理、失效场景(高频考点) 三个维度进行深度解析。


一、 为什么要用声明式事务?(对比编程式事务)

在 Spring 出现之前,或者在 Spring 中使用编程式事务时,代码是这样的:

java 复制代码
// 编程式事务:事务代码和业务代码严重耦合
public void createOrder() {
    TransactionStatus status = transactionManager.getTransaction(configuration);
    try {
        // 1. 扣减库存
        inventoryService.deduct();
        // 2. 创建订单
        orderMapper.insert();
        transactionManager.commit(status); // 手动提交
    } catch (Exception e) {
        transactionManager.rollback(status); // 手动回滚
        throw e;
    }
}

痛点 :满屏幕的 try-catch 和 commit/rollback,业务逻辑被事务管理代码淹没,代码侵入性极强,且极易写错(比如忘记 catch 某种异常导致没回滚)。

声明式事务的出现解决了这个问题:

java 复制代码
// 声明式事务:业务代码极其纯粹
@Transactional(rollbackFor = Exception.class)
public void createOrder() {
    inventoryService.deduct();
    orderMapper.insert();
    // 框架自动处理提交和回滚
}

核心思想 :基于 AOP(面向切面编程) 实现关注点分离。


二、 底层原理:Spring 是如何"自动"管理事务的?

这是面试中最喜欢深挖的点。声明式事务的本质是 AOP + 动态代理 + 事务拦截器。

当你在方法上加上 @Transactional 后,Spring 在启动时会做以下事情:

  1. 解析注解 :Spring 容器启动时,解析 Bean 上的 @Transactional 注解,提取事务属性(如隔离级别、传播行为、回滚规则)。
  2. 创建代理对象 :Spring 发现这个 Bean 需要事务管理,就会利用 AOP 为它生成一个代理对象 (如果目标类实现了接口,默认用 JDK 动态代理;否则用 CGLIB 代理)。你从容器里拿到的其实是代理对象,而不是原始对象。
  3. 拦截器接管(核心) :代理对象内部绑定了一个核心拦截器 ------ TransactionInterceptor。
  4. 执行流程 :
    • 当你调用代理对象的方法时,请求会先到达 TransactionInterceptor。
    • 拦截器获取 PlatformTransactionManager(事务管理器)。
    • 开启事务 :调用 getTransaction() 获取数据库连接并开启事务。
    • 执行业务 :调用目标对象的真实方法(proceed())。
    • 异常处理 :如果方法抛出异常,拦截器捕获异常,判断是否符合回滚规则。符合则调用 rollback(),否则调用 commit()。

一句话总结原理 :你调用的其实是代理对象的方法,代理对象通过事务拦截器,在执行业务代码前后,自动帮你调用了 Connection.commit() 或 Connection.rollback()。


三、 实战与面试高频考点:@Transactional 失效的 6 大场景

在实际开发和面试中,"声明式事务失效"是必考题。因为底层依赖 AOP 代理,所以任何绕过代理对象的行为,都会导致事务失效。

1. 同类内部方法调用(自调用问题)------ 最常见坑
java 复制代码
@Service
public class OrderService {
    
    public void methodA() {
        this.methodB(); // 直接调用同类方法,绕过了代理!
    }

    @Transactional
    public void methodB() {
        // 这里的 @Transactional 不会生效!
    }
}
  • 原因 :methodA 调用 methodB 时,使用的是 this(目标对象本身),而不是 Spring 注入的代理对象 。没有经过代理,TransactionInterceptor 就不会执行。
  • 解决办法 :
    1. 将 methodB 抽离到另一个 Service 中(推荐,符合单一职责)。
    2. 注入自己:@Lazy @Autowired private OrderService self; 然后 self.methodB()。
    3. 使用 AopContext.currentProxy() 获取当前代理对象。
2. 方法不是 public 的
  • 原因 :Spring AOP 默认只拦截 public 方法。如果加在 protected、private 或包可见的方法上,注解直接失效(Spring 4.0+ 会直接报错或忽略)。
  • 解决 :老老实实加上 public 修饰符。
3. 异常被 catch 吞掉了
java 复制代码
@Transactional
public void createOrder() {
    try {
        orderMapper.insert();
        int i = 1 / 0; // 抛出异常
    } catch (Exception e) {
        log.error("出错了", e); 
        // 异常被吃掉了,没有抛给事务拦截器!
    }
}
  • 原因 :事务拦截器没有感知到异常,认为方法正常执行完毕,于是执行了 commit()。
  • 解决 :在 catch 块中处理完日志后,必须 throw e 或 throw new RuntimeException(e) 将异常抛出去。
4. 抛出的异常类型不符合回滚规则
  • 原因 :Spring @Transactional 默认只对 RuntimeException 和 Error 进行回滚 。如果你的代码抛出了受检异常(如 IOException、SQLException),事务不会回滚,而是会提交!
  • 解决 :永远加上 rollbackFor 属性:@Transactional(rollbackFor = Exception.class)。这是企业级开发的铁律。
5. 数据库引擎本身不支持事务
  • 原因 :比如 MySQL 的表引擎是 MyISAM(不支持事务),你加再多 @Transactional 也没用。
  • 解决 :确保数据库表引擎为 InnoDB。
6. 传播行为(Propagation)配置错误
  • 原因 :如果配置了 @Transactional(propagation = Propagation.NOT_SUPPORTED),意思是"以非事务方式执行,如果当前存在事务,则挂起"。这也会导致事务不生效。

四、 进阶补充:事务的传播行为(Propagation)

当存在多个事务方法相互调用时,Spring 如何决定事务的边界?这就是传播行为。最常用的有三个:

  1. REQUIRED(默认):如果当前有事务,就加入;如果没有,就新建一个。(95% 的场景用这个)。
  2. REQUIRES_NEW :无论当前有没有事务,都挂起当前事务,新建一个独立的事务 。内层事务的回滚不会影响外层事务。(常用于:记录操作日志,即使主业务回滚了,日志也要保存下来)。
  3. NESTED :如果当前有事务,则在嵌套事务 内执行(底层依赖数据库的 Savepoint 保存点机制)。内层回滚,外层也会回滚;但外层回滚,内层不一定回滚(取决于具体实现)。

五、 总结

声明式事务是 Spring 提供的极其优雅的解决方案,它利用 AOP 动态代理 将事务管理从业务代码中解耦。

但在享受便利的同时,必须牢记它的边界和陷阱:

  1. 牢记 "代理对象" 的概念,避免自调用失效。
  2. 永远使用 @Transactional(rollbackFor = Exception.class)。
  3. 确保异常正确抛出 ,不要被 catch 吞掉。
  4. 在复杂的多数据源或嵌套调用场景下,合理配置传播行为。
相关推荐
子兮曰3 天前
jev-ultrafast 深度解析:7 秒订机票的浏览器 Agent 是如何炼成的
前端·后端·agent
小羊没烦恼!3 天前
微服务化的基石——持续集成
java·大数据·word·powerpoint·.net
子兮曰3 天前
Jev 爆发一周:7 秒 Agent 背后的 System One 生态与三场争议
前端·后端·ai编程
俊昭喜喜里3 天前
java中的继承和多态的区别
java
小羊没烦恼!3 天前
初探性能优化——2个月到4小时的性能提升
java·开发语言·windows·算法·c#
爱勇宝3 天前
ZCode 开源 24 小时:一份没有历史的账本,回答不了"有没有偷代码"
前端·后端·chatglm (智谱)
譕痕3 天前
JSONObject与JSONArray封装数据格式区别
java·json
胡写代码3 天前
别再前后端各写一套表单校验了
java·后端
小鱼能吃糖3 天前
缺陷修复总览 · mall电商项目:5类缺陷,1个病根,4个业务域
java·电商
此时不提桶,更待何时3 天前
01-06-A-JVM排查实战详解
java·jvm