spring事务失效场景
Spring 事务失效场景深度解析
Spring 事务基于 AOP 动态代理实现,事务失效的本质是:事务相关的 AOP 增强逻辑未被触发。本文从 “代理机制” 切入,逐一拆解 6 种常见失效场景的底层原因,并给出可落地的解决方案,尤其重点讲解 “this 调用失效” 这一难点。
事务失效的核心前提:理解 Spring 事务的 AOP 原理
在分析失效场景前,必须先明确 Spring 事务的执行流程(基于 CGLIB 代理,最常见场景):
- 代理类生成:Spring 为标注
@Transactional的目标 Bean(如UserService)生成 CGLIB 代理类(继承自目标 Bean); - 外部调用触发增强:当外部代码调用代理类的方法时,代理类会先执行 事务增强逻辑(开启事务),再调用目标 Bean 的原始方法;
- 目标方法执行:目标 Bean 执行业务逻辑,若正常完成则代理类触发事务提交,若抛出异常则触发回滚;
- 内部调用绕过增强:若目标 Bean 内部通过
this调用自身方法,this指向目标 Bean 本身(非代理类),会直接执行原始方法,跳过代理类的增强逻辑 → 事务失效。
6 种事务失效场景与底层解析
1. 场景 1:事务方法非 public 修饰
底层原因
Spring AOP 动态代理(JDK/CGLIB)仅对 public 方法生效:
- JDK 代理:基于接口实现,仅代理接口中的 public 方法;
- CGLIB 代理:基于继承生成子类,非 public 方法(private/protected/default)无法被重写,代理类无法插入增强逻辑。
Spring 源码佐证(AbstractFallbackTransactionAttributeSource):
protected TransactionAttribute computeTransactionAttribute(Method method, Class<?> targetClass) {
// 非 public 方法直接返回 null,无事务属性
if (Modifier.isPublic(method.getModifiers()) == false) {
return null;
}
// ... 后续逻辑
}
若方法非 public,Spring 会忽略其 @Transactional 注解,不生成事务增强。
示例(失效代码)
public class UserService {
// 非 public 方法:事务注解无效
void updateUser(Long id) { // default 修饰,非 public
jdbcTemplate.update("UPDATE user SET name = '张三' WHERE id = ?", id);
}
}
解决方案
将事务方法改为 public 修饰:
public class UserService {
// 改为 public 方法,事务生效
public void updateUser(Long id) {
jdbcTemplate.update("UPDATE user SET name = '张三' WHERE id = ?", id);
}
}
2. 场景 2:事务类未被 Spring 容器管理
底层原因
Spring 事务依赖 IOC 容器:只有被 Spring 管理的 Bean(标注 @Component/@Service/@Repository 等,或通过 @Bean 注册),才会被扫描并生成事务代理类。若类未被容器管理,@Transactional 注解仅为普通注解,无任何作用。
示例(失效代码)
// 未标注 @Service,未被 Spring 管理
public class UserService {
private JdbcTemplate jdbcTemplate;
// 无效:类不是容器 Bean,无代理类生成
public void updateUser(Long id) {
jdbcTemplate.update("UPDATE user SET name = '张三' WHERE id = ?", id);
}
}
解决方案
给类添加 Spring 组件注解,确保被容器扫描管理:
// 标注为容器 Bean
public class UserService {
private JdbcTemplate jdbcTemplate;
// 生效:容器会生成代理类
public void updateUser(Long id) {
jdbcTemplate.update("UPDATE user SET name = '张三' WHERE id = ?", id);
}
}
3. 场景 3:同类内 this 调用事务方法(最易踩坑)
这是最常见且最难理解的失效场景,下面结合代理机制彻底讲透。
底层原因
this 指向 目标 Bean 本身,而非 Spring 生成的代理类,导致事务增强逻辑未被触发。具体流程如下:
- 外部调用代理类方法:若外部调用
proxy.updateUser()(代理类方法),会先执行事务增强(开启事务),再调用目标 Bean 的userService.updateUser(); - 内部 this 调用:若目标 Bean 内部用
this.updateOrder()调用自身方法,this是目标 Bean 实例(如userService),直接执行updateOrder()原始方法,跳过代理类的事务增强 → 事务失效。
源码分析佐证
以 DynamicAdvisedInterceptor(CGLIB 代理的核心拦截器)为例:
public Object intercept(Object proxy, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
Object target = getTarget(); // 目标 Bean(如 UserService 实例)
try {
if (chain.isEmpty()) {
// 无增强链:直接调用目标 Bean 的方法(非代理)
retVal = methodProxy.invoke(target, args);
} else {
// 有增强链:执行事务增强后,再调用目标 Bean 方法
retVal = new CglibMethodInvocation(proxy, target, method, args, targetClass, chain, methodProxy).proceed();
}
}
}
- 外部调用时:触发
chain(事务增强链),执行proceed()走增强逻辑; - 内部
this调用时:直接执行methodProxy.invoke(target, args),调用目标 Bean 方法,无增强。
示例(失效代码)
public class OrderService {
private JdbcTemplate jdbcTemplate;
// 外部调用此方法:事务生效(代理类触发增强)
public void createOrder(Long userId) {
// 内部 this 调用:this 指向 OrderService 目标 Bean,非代理类
this.updateUserOrderCount(userId); // 事务失效
}
// 事务注解无效:内部调用未走代理
public void updateUserOrderCount(Long userId) {
jdbcTemplate.update("UPDATE user SET order_count = order_count + 1 WHERE id = ?", userId);
}
}
解决方案:暴露代理类,内部调用代理对象
通过配置暴露代理类,让内部调用也能获取到代理对象,从而触发事务增强。步骤如下:
- 开启代理暴露(二选一):
- XML 配置:
<aop:config expose-proxy="true"/> - 注解配置(Spring Boot):
@EnableAspectJAutoProxy(exposeProxy = true)
- XML 配置:
- 内部调用代理对象:
通过AopContext.currentProxy()获取代理类,再调用目标方法(而非this)。
修复后代码
public class OrderService {
private JdbcTemplate jdbcTemplate;
public void createOrder(Long userId) {
// 1. 获取代理对象(OrderService 的代理类)
OrderService proxy = (OrderService) AopContext.currentProxy();
// 2. 调用代理类的方法:触发事务增强
proxy.updateUserOrderCount(userId); // 事务生效
}
public void updateUserOrderCount(Long userId) {
jdbcTemplate.update("UPDATE user SET order_count = order_count + 1 WHERE id = ?", userId);
}
}
关键原理
expose-proxy="true" 会将代理类存入 ThreadLocal(AopContext),内部调用时通过 AopContext.currentProxy() 取出代理类,此时调用的是代理类的方法,会执行事务增强逻辑。
4. 场景 4:未捕获 RuntimeException 或自定义异常未指定回滚规则
底层原因
Spring 事务默认仅对 RuntimeException(运行时异常)和 Error 触发回滚,对 Checked Exception(编译时异常,如 IOException) 不回滚。若业务抛出 Checked Exception,或捕获异常后未重新抛出,Spring 无法感知异常,会直接提交事务。
示例 1:抛出 Checked Exception(失效)
public class UserService {
public void updateUser(Long id) throws IOException {
jdbcTemplate.update("UPDATE user SET name = '张三' WHERE id = ?", id);
// 抛出 Checked Exception:默认不回滚,事务提交
throw new IOException("文件读取失败");
}
}
示例 2:捕获异常未抛出(失效)
public class UserService {
public void updateUser(Long id) {
try {
jdbcTemplate.update("UPDATE user SET name = '张三' WHERE id = ?", id);
throw new RuntimeException("业务异常");
} catch (RuntimeException e) {
// 捕获异常但未重新抛出:Spring 无法感知,事务提交
e.printStackTrace();
}
}
}
解决方案
- 自定义异常回滚:通过
rollbackFor指定需要回滚的异常类型; - 捕获异常需重新抛出:若必须捕获异常,需在
catch中重新抛出(或抛出RuntimeException)。
修复后代码
public class UserService {
// 方案 1:指定 Checked Exception 回滚
public void updateUser1(Long id) throws IOException {
jdbcTemplate.update("UPDATE user SET name = '张三' WHERE id = ?", id);
throw new IOException("文件读取失败"); // 事务回滚
}
// 方案 2:捕获异常后重新抛出
public void updateUser2(Long id) {
try {
jdbcTemplate.update("UPDATE user SET name = '张三' WHERE id = ?", id);
throw new RuntimeException("业务异常");
} catch (RuntimeException e) {
e.printStackTrace();
throw e; // 重新抛出,触发回滚
}
}
}
5. 场景 5:事务传播行为配置错误
底层原因
选择了不支持事务的传播行为,导致事务无法生效或被挂起。常见错误传播行为:
PROPAGATION_NOT_SUPPORTED:以非事务方式执行,若当前有事务则挂起;PROPAGATION_NEVER:以非事务方式执行,若当前有事务则抛出异常;PROPAGATION_SUPPORTS:若当前无事务,则以非事务方式执行(仅依赖外部事务)。
示例(失效代码)
public class UserService {
// 传播行为为 NOT_SUPPORTED:不支持事务,事务注解无效
public void updateUser(Long id) {
jdbcTemplate.update("UPDATE user SET name = '张三' WHERE id = ?", id);
throw new RuntimeException("业务异常"); // 无回滚,数据已修改
}
}
解决方案
选择支持事务的传播行为(根据业务场景):
- 默认值
PROPAGATION_REQUIRED:最常用,支持当前事务,无则新建; PROPAGATION_REQUIRES_NEW:新建独立事务,与外部事务隔离;PROPAGATION_NESTED:嵌套事务,可独立回滚。
修复后代码
public class UserService {
// 正确传播行为:支持事务
public void updateUser(Long id) {
jdbcTemplate.update("UPDATE user SET name = '张三' WHERE id = ?", id);
throw new RuntimeException("业务异常"); // 事务回滚,数据未修改
}
}
6. 场景 6:事务方法所在类是 final 或方法是 final
底层原因
Spring 事务依赖 CGLIB 代理(继承目标类),若目标类或方法是 final:
- final 类:CGLIB 无法生成子类(继承被禁止),无法创建代理类;
- final 方法:无法被 CGLIB 重写,代理类无法插入事务增强逻辑。
示例(失效代码)
// final 类:CGLIB 无法生成代理,事务失效
public final class UserService {
public void updateUser(Long id) {
jdbcTemplate.update("UPDATE user SET name = '张三' WHERE id = ?", id);
}
}
// 或 final 方法:无法重写,事务失效
public class UserService {
public final void updateUser(Long id) { // final 方法
jdbcTemplate.update("UPDATE user SET name = '张三' WHERE id = ?", id);
}
}
解决方案
- 移除类或方法的
final修饰符; - 若必须用
final,改用 JDK 代理(目标类实现接口),但 JDK 代理仍不支持final方法(接口方法无法是final),因此根本解决方案是取消 final 修饰。
事务失效场景总结与排查思路
1. 失效场景汇总表
| 失效场景 | 底层原因 | 解决方案 |
|---|---|---|
| 非 public 方法 | 代理仅对 public 方法生效 | 改为 public 修饰 |
| 类未被 Spring 管理 | 无代理类生成 | 加 @Component/@Service 等注解 |
| 同类内 this 调用 | this 指向目标 Bean,跳过代理增强 | 开启 expose-proxy,用 AopContext 取代理 |
| 异常未抛出或类型不匹配 | Spring 无法感知异常,不触发回滚 | 抛出异常,配置 rollbackFor |
| 传播行为错误 | 选择了不支持事务的传播行为 | 改用 REQUIRED/REQUIRES_NEW 等 |
| 类 / 方法是 final | CGLIB 无法生成代理或重写方法 | 移除 final 修饰符 |
2. 排查思路(三步法)
- 检查代理是否生成:通过
context.getBean(UserService.class).getClass()查看返回类型,若含 $$$EnhancerByCGLIB$$$ 则代理生成成功; - 检查事务增强是否触发:在事务方法前后加日志,或 debug 查看是否进入
TransactionInterceptor(事务拦截器); - 检查异常是否被捕获:确保异常未被
try-catch消化,且异常类型符合rollbackFor配置。
关键注意点
- JDK 代理 vs CGLIB 代理:两种代理在 “this 调用”“final 方法” 等场景下失效规则一致,核心是 “增强逻辑是否被触发”;
rollbackFor建议显式配置:默认仅回滚 RuntimeException,实际开发中建议显式配置rollbackFor = Exception.class,避免因异常类型遗漏导致失效;- 避免过度依赖内部调用:尽量将事务方法拆分到不同类中,通过外部调用触发事务(减少
this调用场景)