【关于Spring声明式事务】

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

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

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


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

在 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-catchcommit/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 方法。如果加在 protectedprivate 或包可见的方法上,注解直接失效(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 ethrow new RuntimeException(e) 将异常抛出去。
4. 抛出的异常类型不符合回滚规则
  • 原因 :Spring @Transactional 默认只对 RuntimeExceptionError 进行回滚 。如果你的代码抛出了受检异常(如 IOExceptionSQLException),事务不会回滚,而是会提交!
  • 解决 :永远加上 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. 在复杂的多数据源或嵌套调用场景下,合理配置传播行为
相关推荐
倒流时光三十年1 小时前
Java 内存模型(JMM)通俗解释
java·开发语言
码兄科技2 小时前
Java AI智能体开发实战:从零构建企业级智能应用指南
java·开发语言·人工智能
2401_859506242 小时前
AIGC赋能大漆摆件设计:从痛点分析到技术架构与实战验证
java·大数据·人工智能
剑挑星河月2 小时前
54.螺旋矩阵
java·算法·leetcode·矩阵
Lhappy嘻嘻3 小时前
Java 并发编程(六)|并发进阶高频:CAS、锁升级
java·开发语言
techdashen3 小时前
Arborium:把 tree-sitter 语法高亮打包成 Rust 文档生态的基础设施
开发语言·后端·rust
要开心吖ZSH3 小时前
MVCC 进阶:快照读 vs 当前读、幻读与 Next-Key Lock
java·数据库·sql·mysql·mvcc
Profile排查笔记3 小时前
指纹浏览器环境异常排查:Fingerprint、Profile、Proxy、Session 和 Task Log 怎么看
前端·人工智能·后端·自动化
京韵养生记3 小时前
【无标题】
java·服务器·前端