"声明式事务"是 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 在启动时会做以下事情:
- 解析注解 :Spring 容器启动时,解析 Bean 上的
@Transactional注解,提取事务属性(如隔离级别、传播行为、回滚规则)。 - 创建代理对象 :Spring 发现这个 Bean 需要事务管理,就会利用 AOP 为它生成一个代理对象 (如果目标类实现了接口,默认用 JDK 动态代理;否则用 CGLIB 代理)。你从容器里拿到的其实是代理对象,而不是原始对象。
- 拦截器接管(核心) :代理对象内部绑定了一个核心拦截器 ------
TransactionInterceptor。 - 执行流程 :
- 当你调用代理对象的方法时,请求会先到达
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就不会执行。 - 解决办法 :
- 将
methodB抽离到另一个 Service 中(推荐,符合单一职责)。 - 注入自己:
@Lazy @Autowired private OrderService self;然后self.methodB()。 - 使用
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 如何决定事务的边界?这就是传播行为。最常用的有三个:
REQUIRED(默认):如果当前有事务,就加入;如果没有,就新建一个。(95% 的场景用这个)。REQUIRES_NEW:无论当前有没有事务,都挂起当前事务,新建一个独立的事务 。内层事务的回滚不会影响外层事务。(常用于:记录操作日志,即使主业务回滚了,日志也要保存下来)。NESTED:如果当前有事务,则在嵌套事务 内执行(底层依赖数据库的Savepoint保存点机制)。内层回滚,外层也会回滚;但外层回滚,内层不一定回滚(取决于具体实现)。
五、 总结
声明式事务是 Spring 提供的极其优雅的解决方案,它利用 AOP 动态代理 将事务管理从业务代码中解耦。
但在享受便利的同时,必须牢记它的边界和陷阱:
- 牢记 "代理对象" 的概念,避免自调用失效。
- 永远使用
@Transactional(rollbackFor = Exception.class)。 - 确保异常正确抛出 ,不要被
catch吞掉。 - 在复杂的多数据源或嵌套调用场景下,合理配置传播行为。