0%

spring事务失效场景

Spring 事务失效场景深度解析

Spring 事务基于 AOP 动态代理实现,事务失效的本质是:事务相关的 AOP 增强逻辑未被触发。本文从 “代理机制” 切入,逐一拆解 6 种常见失效场景的底层原因,并给出可落地的解决方案,尤其重点讲解 “this 调用失效” 这一难点。

事务失效的核心前提:理解 Spring 事务的 AOP 原理

在分析失效场景前,必须先明确 Spring 事务的执行流程(基于 CGLIB 代理,最常见场景):

  1. 代理类生成:Spring 为标注 @Transactional 的目标 Bean(如 UserService)生成 CGLIB 代理类(继承自目标 Bean);
  2. 外部调用触发增强:当外部代码调用代理类的方法时,代理类会先执行 事务增强逻辑(开启事务),再调用目标 Bean 的原始方法;
  3. 目标方法执行:目标 Bean 执行业务逻辑,若正常完成则代理类触发事务提交,若抛出异常则触发回滚;
  4. 内部调用绕过增强:若目标 Bean 内部通过 this 调用自身方法,this 指向目标 Bean 本身(非代理类),会直接执行原始方法,跳过代理类的增强逻辑 → 事务失效。

6 种事务失效场景与底层解析

1. 场景 1:事务方法非 public 修饰

底层原因

Spring AOP 动态代理(JDK/CGLIB)仅对 public 方法生效:

  • JDK 代理:基于接口实现,仅代理接口中的 public 方法;
  • CGLIB 代理:基于继承生成子类,非 public 方法(private/protected/default)无法被重写,代理类无法插入增强逻辑。

Spring 源码佐证(AbstractFallbackTransactionAttributeSource):

1
2
3
4
5
6
7
protected TransactionAttribute computeTransactionAttribute(Method method, @Nullable Class<?> targetClass) {
// 非 public 方法直接返回 null,无事务属性
if (Modifier.isPublic(method.getModifiers()) == false) {
return null;
}
// ... 后续逻辑
}

若方法非 public,Spring 会忽略其 @Transactional 注解,不生成事务增强。

示例(失效代码)
1
2
3
4
5
6
7
8
@Service
public class UserService {
// 非 public 方法:事务注解无效
@Transactional
void updateUser(Long id) { // default 修饰,非 public
jdbcTemplate.update("UPDATE user SET name = '张三' WHERE id = ?", id);
}
}
解决方案

将事务方法改为 public 修饰

1
2
3
4
5
6
7
8
@Service
public class UserService {
// 改为 public 方法,事务生效
@Transactional
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 注解仅为普通注解,无任何作用。

示例(失效代码)
1
2
3
4
5
6
7
8
9
10
// 未标注 @Service,未被 Spring 管理
public class UserService {
@Autowired
private JdbcTemplate jdbcTemplate;

@Transactional // 无效:类不是容器 Bean,无代理类生成
public void updateUser(Long id) {
jdbcTemplate.update("UPDATE user SET name = '张三' WHERE id = ?", id);
}
}
解决方案

给类添加 Spring 组件注解,确保被容器扫描管理:

1
2
3
4
5
6
7
8
9
10
@Service // 标注为容器 Bean
public class UserService {
@Autowired
private JdbcTemplate jdbcTemplate;

@Transactional // 生效:容器会生成代理类
public void updateUser(Long id) {
jdbcTemplate.update("UPDATE user SET name = '张三' WHERE id = ?", id);
}
}

3. 场景 3:同类内 this 调用事务方法(最易踩坑)

这是最常见且最难理解的失效场景,下面结合代理机制彻底讲透。

底层原因

this 指向 目标 Bean 本身,而非 Spring 生成的代理类,导致事务增强逻辑未被触发。具体流程如下:

  1. 外部调用代理类方法:若外部调用 proxy.updateUser()(代理类方法),会先执行事务增强(开启事务),再调用目标 Bean 的 userService.updateUser()
  2. 内部 this 调用:若目标 Bean 内部用 this.updateOrder() 调用自身方法,this 是目标 Bean 实例(如 userService),直接执行 updateOrder() 原始方法,跳过代理类的事务增强 → 事务失效。
源码分析佐证

DynamicAdvisedInterceptor(CGLIB 代理的核心拦截器)为例:

1
2
3
4
5
6
7
8
9
10
11
12
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 方法,无增强。
示例(失效代码)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Service
public class OrderService {
@Autowired
private JdbcTemplate jdbcTemplate;

// 外部调用此方法:事务生效(代理类触发增强)
@Transactional
public void createOrder(Long userId) {
// 内部 this 调用:this 指向 OrderService 目标 Bean,非代理类
this.updateUserOrderCount(userId); // 事务失效
}

// 事务注解无效:内部调用未走代理
@Transactional
public void updateUserOrderCount(Long userId) {
jdbcTemplate.update("UPDATE user SET order_count = order_count + 1 WHERE id = ?", userId);
}
}
解决方案:暴露代理类,内部调用代理对象

通过配置暴露代理类,让内部调用也能获取到代理对象,从而触发事务增强。步骤如下:

  1. 开启代理暴露(二选一):
    • XML 配置:<aop:config expose-proxy="true"/>
    • 注解配置(Spring Boot):@EnableAspectJAutoProxy(exposeProxy = true)
  2. 内部调用代理对象
    通过 AopContext.currentProxy() 获取代理类,再调用目标方法(而非 this)。
修复后代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Service
public class OrderService {
@Autowired
private JdbcTemplate jdbcTemplate;

@Transactional
public void createOrder(Long userId) {
// 1. 获取代理对象(OrderService 的代理类)
OrderService proxy = (OrderService) AopContext.currentProxy();
// 2. 调用代理类的方法:触发事务增强
proxy.updateUserOrderCount(userId); // 事务生效
}

@Transactional
public void updateUserOrderCount(Long userId) {
jdbcTemplate.update("UPDATE user SET order_count = order_count + 1 WHERE id = ?", userId);
}
}
关键原理

expose-proxy="true" 会将代理类存入 ThreadLocalAopContext),内部调用时通过 AopContext.currentProxy() 取出代理类,此时调用的是代理类的方法,会执行事务增强逻辑。

4. 场景 4:未捕获 RuntimeException 或自定义异常未指定回滚规则

底层原因

Spring 事务默认仅对 RuntimeException(运行时异常)和 Error 触发回滚,对 Checked Exception(编译时异常,如 IOException 不回滚。若业务抛出 Checked Exception,或捕获异常后未重新抛出,Spring 无法感知异常,会直接提交事务。

示例 1:抛出 Checked Exception(失效)
1
2
3
4
5
6
7
8
9
@Service
public class UserService {
@Transactional
public void updateUser(Long id) throws IOException {
jdbcTemplate.update("UPDATE user SET name = '张三' WHERE id = ?", id);
// 抛出 Checked Exception:默认不回滚,事务提交
throw new IOException("文件读取失败");
}
}
示例 2:捕获异常未抛出(失效)
1
2
3
4
5
6
7
8
9
10
11
12
13
@Service
public class UserService {
@Transactional
public void updateUser(Long id) {
try {
jdbcTemplate.update("UPDATE user SET name = '张三' WHERE id = ?", id);
throw new RuntimeException("业务异常");
} catch (RuntimeException e) {
// 捕获异常但未重新抛出:Spring 无法感知,事务提交
e.printStackTrace();
}
}
}
解决方案
  1. 自定义异常回滚:通过 rollbackFor 指定需要回滚的异常类型;
  2. 捕获异常需重新抛出:若必须捕获异常,需在 catch 中重新抛出(或抛出 RuntimeException)。
修复后代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Service
public class UserService {
// 方案 1:指定 Checked Exception 回滚
@Transactional(rollbackFor = IOException.class)
public void updateUser1(Long id) throws IOException {
jdbcTemplate.update("UPDATE user SET name = '张三' WHERE id = ?", id);
throw new IOException("文件读取失败"); // 事务回滚
}

// 方案 2:捕获异常后重新抛出
@Transactional
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:若当前无事务,则以非事务方式执行(仅依赖外部事务)。
示例(失效代码)
1
2
3
4
5
6
7
8
9
@Service
public class UserService {
// 传播行为为 NOT_SUPPORTED:不支持事务,事务注解无效
@Transactional(propagation = Propagation.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:嵌套事务,可独立回滚。
修复后代码
1
2
3
4
5
6
7
8
9
@Service
public class UserService {
// 正确传播行为:支持事务
@Transactional(propagation = Propagation.REQUIRED)
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 重写,代理类无法插入事务增强逻辑。
示例(失效代码)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// final 类:CGLIB 无法生成代理,事务失效
@Service
public final class UserService {
@Transactional
public void updateUser(Long id) {
jdbcTemplate.update("UPDATE user SET name = '张三' WHERE id = ?", id);
}
}

// 或 final 方法:无法重写,事务失效
@Service
public class UserService {
@Transactional
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. 排查思路(三步法)

  1. 检查代理是否生成:通过 context.getBean(UserService.class).getClass() 查看返回类型,若含 $$$EnhancerByCGLIB$$$ 则代理生成成功;
  2. 检查事务增强是否触发:在事务方法前后加日志,或 debug 查看是否进入 TransactionInterceptor(事务拦截器);
  3. 检查异常是否被捕获:确保异常未被 try-catch 消化,且异常类型符合 rollbackFor 配置。

关键注意点

  1. JDK 代理 vs CGLIB 代理:两种代理在 “this 调用”“final 方法” 等场景下失效规则一致,核心是 “增强逻辑是否被触发”;
  2. rollbackFor 建议显式配置:默认仅回滚 RuntimeException,实际开发中建议显式配置 rollbackFor = Exception.class,避免因异常类型遗漏导致失效;
  3. 避免过度依赖内部调用:尽量将事务方法拆分到不同类中,通过外部调用触发事务(减少 this 调用场景)

欢迎关注我的其它发布渠道

表情 | 预览
快来做第一个评论的人吧~
Powered By Valine
v1.3.10