Spring Boot 的事务注解 @Transactional 失效的几种情况

开发中我们经常会用到 Spring Boot 的事务注解,为含有多种操作的方法添加事务,做到如果某一个环节出错,全部回滚的效果。但是在开发中可能会因为不了解事务机制,而导致我们的方法使用了 @Transactional 注解但是没有生效的情况,下面就把这几种不能生效的情况整理一下。

文章目录

  • 一、非public方法(动态代理限制)
  • 二、自调用问题(类内部方法调用,不走代理)
  • 三、异常类型不匹配(默认只回滚RuntimeException)
  • 四、多线程切换(事务连接绑定ThreadLocal)
  • 五、错误传播行为(如:PROPAGATION_NOT_SUPPORTED挂起事务)
  • 六、总结

一、非public方法(动态代理限制)

Spring 的事务管理本质上是通过 AOP 动态代理 实现的(JDK 动态代理或 CGLIB 代理)。

代理对象在调用目标方法时,会添加事务管理的逻辑(开启事务、提交/回滚事务)。

然而,动态代理只能代理 public 方法。

如果你将 @Transactional 注解放在 protectedprivate 或默认(包级私有)方法上,Spring 在创建代理时无法为这些方法添加事务增强逻辑。

当你通过代理对象调用这些非 public 方法时,事务相关的代码(如 beginTransaction(), commit(), rollback())不会被织入,因此事务管理完全失效。

所以,要确保所有需要事务管理的方法都是 public 的。这是 Spring AOP 代理机制的一个硬性限制。

二、自调用问题(类内部方法调用,不走代理)

这是 AOP 代理机制带来的另一个典型问题。假设一个 Service 类中有两个方法:

  • methodA():没有 @Transactional 注解。
  • methodB():有 @Transactional 注解。

如果你在 methodA() 内部直接调用 this.methodB(),那么你调用的是 Service 类本身的 methodA()this 指向目标对象本身)。methodA() 内部调用 this.methodB(),是目标对象内部的方法调用
这个调用完全不经过为该 Service 类生成的代理对象。

因为调用 methodB() 没有经过代理对象,所以代理对象上附加的事务拦截逻辑根本不会被执行。methodB() 虽然标注了 @Transactional,但在此次调用中完全失效。


解决方案有以下几种:推荐重构代码。

方案一:注入自身代理对象

开启 exposeProxy:在配置类(如 @SpringBootApplication 主类)上添加 @EnableAspectJAutoProxy(exposeProxy = true)

在需要自调用事务方法的地方获取代理对象:

((YourServiceClass) AopContext.currentProxy()).methodB();

AopContext.currentProxy() 获取到当前方法执行上下文中的代理对象(即被 Spring AOP 增强过的对象),通过这个代理对象调用 methodB(),就会走代理逻辑,事务拦截器生效。

这种方式不常用,会有缺点,引入了 Spring AOP 特定 API (AopContext),增加了代码耦合度。

方案二:重构代码(推荐)

将需要事务管理的业务逻辑 methodB() 抽取到另一个独立的 Bean(如另一个 Service)中。然后在原来的 methodA() 中注入并使用这个新的 Bean 来调用 methodB()。这样调用自然通过代理对象进行。

这是更符合设计原则(单一职责、依赖注入)的做法,避免了自调用问题,也降低了耦合。

方案三:使用 ApplicationContext 获取 Bean

在类中注入 ApplicationContext,然后通过 ctx.getBean(YourServiceClass.class).methodB() 来调用。这样获取到的是代理 Bean,调用会走代理。

代码略显繁琐,并且也需要依赖 Spring 容器。

三、异常类型不匹配(默认只回滚RuntimeException)

@Transactional 注解的 rollbackFor 属性默认值是 RuntimeExceptionError

  • 当方法抛出 RuntimeException 或其子类(如 NullPointerException, IllegalArgumentException)时,Spring 会回滚事务。
  • 当方法抛出检查型异常(如 IOException, SQLException)时,Spring 默认会提交事务!

如果你在一个事务方法中抛出了自定义的业务异常(继承自 Exception 而非 RuntimeException),或者抛出了其他检查型异常,并且没有显式配置 rollbackFor,那么即使业务逻辑出错抛出了异常,Spring 也会正常提交事务,导致数据不一致。

这时,我们要显式指定 rollbackFor:在 @Transactional 注解中明确声明哪些异常需要触发回滚。

// 回滚所有 Exception 和自定义异常
@Transactional(rollbackFor = {Exception.class, YourCustomBusinessException.class}) 
public void transactionalMethod() throws Exception { ... }

或者修改默认行为(谨慎):虽然不推荐,但可以通过修改 Spring 的全局事务管理器配置来改变默认的回滚异常类型(例如改为回滚所有 Throwable)。

但这样做风险较大,可能回滚不应该回滚的异常(如 OutOfMemoryError)。

最佳实践还是根据具体业务在注解上显式配置 rollbackFornoRollbackFor

四、多线程切换(事务连接绑定ThreadLocal)

Spring 的事务管理核心是将数据库连接(Connection)绑定到当前执行线程(Thread)的 ThreadLocal 变量上。

一个事务从开始(beginTransaction)到提交/回滚(commit/rollback)期间,所有数据库操作都使用这个绑定在当前线程 ThreadLocal 上的同一个 Connection,以此保证 ACID 特性。

如果你在一个事务方法内部启动了一个新线程(new Thread() 或者使用线程池(如 @Async)执行数据库操作,会出现以下情况:

  • 新线程拥有自己独立的 ThreadLocal 存储。
  • 新线程无法访问到原始事务线程绑定的 Connection 对象。
  • 新线程中的数据库操作会从连接池获取一个新的、独立的 Connection
  • 这个新 Connection 不参与原始事务,其操作会在自身 autoCommit 模式下立即执行(通常是自动提交),与原始事务完全隔离。

新线程中的数据库操作成功与否不影响原始事务的提交或回滚,反之亦然。破坏了事务的原子性(Atomicity)。原始事务回滚不会回滚新线程中的操作;新线程操作失败也不会导致原始事务回滚。

解决方案:处理多线程下的数据一致性非常复杂,没有银弹:

  • **避免在事务方法内开启异步线程执行 DB 操作:**这是最根本的预防措施。将需要在同一事务中完成的操作放在同一个线程内执行。
  • 编程式事务管理: 在新线程内部,使用 TransactionTemplate 手动管理事务边界。但这只是让新线程内部操作具有事务性,无法与原始线程的事务合并成一个原子事务。
  • **分布式事务:**如果业务强要求跨线程的 ACID,可能需要引入分布式事务管理器(如 Seata, Atomikos)来处理这种跨 资源(不同线程可视为不同资源管理者)的场景,但代价高昂且复杂。
  • 设计补偿机制: 在业务层设计最终一致性方案(如 Saga 模式),通过记录操作日志、发送消息、定时任务补偿等方式,在异步操作失败后尝试回滚或修正原始事务已提交的操作。这是更常见的处理异步事务一致性的实践。

五、错误传播行为(如:PROPAGATION_NOT_SUPPORTED挂起事务)

@Transactionalpropagation 属性定义了当前方法的事务如何与已存在的事务进行交互。使用不当会导致事务行为不符合预期。

PROPAGATION_NOT_SUPPORTED 不支持事务。如果当前存在事务,则挂起(Suspend) 这个事务;然后以非事务方式执行当前方法。方法执行完毕后,之前挂起的事务恢复(Resume)。

假设方法 outer() 开启了一个事务(Propagation.REQUIRED),在其内部调用 inner() 方法,而 inner() 被标注为 @Transactional(propagation = Propagation.NOT_SUPPORTED),当执行到 inner() 时:

  1. 系统检测到当前存在 outer() 开启的事务。
  2. 根据 NOT_SUPPORTED 语义,挂起 outer() 的事务
  3. inner() 方法在无事务状态下执行(相当于 autoCommit=true)。
  4. inner() 方法执行完毕(无论成功失败,其操作已立即提交)。
  5. 恢复 outer() 的事务,继续执行 outer() 剩余代码。

结果是 inner() 方法中的数据库操作不受 outer() 事务控制。即使 outer() 最终因异常回滚,inner() 中已提交的操作不会被回滚!这通常不是开发者想要的效果,极易造成数据不一致。

其他易错传播行为:

  • PROPAGATION_NEVER 要求不能存在事务。如果调用者在一个事务中调用了标记为 NEVER 的方法,会直接抛出 IllegalTransactionStateException 异常。
  • PROPAGATION_SUPPORTS 如果当前存在事务,就加入该事务;如果没有,就以非事务方式执行。关键点在于非事务方式。如果方法中有多个操作且需要原子性,而外部又恰好没有事务,这些操作就会各自独立提交。
  • PROPAGATION_REQUIRES_NEW 总是开启一个全新的、独立的事务。会挂起外部事务(如果存在)。新事务的提交/回滚与外部事务互不影响。注意: 这虽然创建了新事务,但不同于自调用失效,它是有效的(通过代理调用)。它的陷阱在于开发者可能误以为新事务是外部事务的一部分,其实它们是独立的。

解决方案:

  • 深入理解传播行为: 务必清楚每种传播行为(REQUIRED, REQUIRES_NEW, SUPPORTS, MANDATORY, NOT_SUPPORTED, NEVER, NESTED)的精确语义。
  • 谨慎选择传播行为: 默认使用 Propagation.REQUIRED 通常能满足大多数场景(加入现有事务,没有则新建)。只有在有明确且充分理由时才使用其他传播行为。
  • 代码审查与测试: 对使用了非默认传播行为的代码进行重点审查,并通过单元测试、集成测试模拟各种调用链路,验证事务边界和回滚行为是否符合预期。特别注意跨方法、跨服务调用时的事务传播。

六、总结

Spring Boot 事务失效的核心原因通常围绕:

  • AOP 代理机制的限制(非 public、自调用)
  • 异常处理机制(默认回滚异常类型)
  • 资源绑定机制(ThreadLocal 导致多线程失效)
  • 配置错误(传播行为误用)

解决这些问题需要深入理解 Spring 事务管理的底层原理(代理、ThreadLocal、异常回滚规则、传播语义),并在编码和配置时保持谨慎,遵循最佳实践(如方法 public、避免自调用、显式指定 rollbackFor、理解传播行为、避免事务内跨线程操作 DB)。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mfbz.cn/a/2708.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

RabbitMQ面试精讲 Day 8:死信队列与延迟队列实现

【RabbitMQ面试精讲 Day 8】死信队列与延迟队列实现 文章标签 RabbitMQ,消息队列,死信队列,延迟队列,面试技巧,分布式系统 文章简述 本文是"RabbitMQ面试精讲"系列第8天,深入讲解死信队列与延迟队列的实现原理与实战应用。文章详细解析死信队列的触发…

快速掌握Python编程基础

干货分享,感谢您的阅读!备注:本博客将自己初步学习Python的总结进行分享,希望大家通过本博客可以在短时间内快速掌握Python的基本程序编码能力,如有错误请留言指正,谢谢!(持续更新&a…

Redis数据库存储键值对的底层原理

前言Redis可以简单理解为是一个存储键值对的内存结构下面我们来看一下Redis使用什么数据结构来存储键值对的叭Redis键值对的存储原理Redis存储键值对的数据结构是哈希表存储键值对的运行机制因为Redis的数据存储类型是多种多样的,所以管理键值对的哈希表只是存储这个数据的地址…

全球化 2.0 | 中国香港教育机构通过云轴科技ZStack实现VMware替代

中国香港教育机构是非营利性组织。随着智慧教育升级与业务量激增,客户面临VMware持续的授权和维护成本带来总体拥有成本压力;部分业务仍运行在性能与扩展性不足的老旧物理服务器和 VMware 架构上,存在单点故障风险;跨校区物理机与…

C#中对于List的多种排序方式

在 C# 中给 List<AI> 排序&#xff0c;只要 明确排序规则&#xff08;比如按某个字段、某几个字段、或外部规则&#xff09;&#xff0c;就能用下面几种常见写法。下面全部基于这个示例类&#xff1a;public class AI {public int country; // 国家编号public int pr…

Redis 核心概念、命令详解与应用实践:从基础到分布式集成

目录 1. 认识 Redis 2. Redis 特性 2.1 操作内存 2.2 速度快 2.3 丰富的功能 2.4 简单稳定 2.5 客户端语言多 2.6 持久化 2.7 主从复制 2.8 高可用 和 分布式 2.9 单线程架构 2.9.1 引出单线程模型 2.9.2 单线程快的原因 2.10 Redis 和 MySQL 的特性对比 2.11 R…

react 和 react native 的开发过程区别

React 和 React Native 虽然都使用 React 思想和语法&#xff08;函数组件、Hooks、JSX 等&#xff09;&#xff0c;但在 开发流程、渲染机制、UI 组件、样式处理、运行平台 等方面有明显差异。以下是对比总结&#xff1a;✅ 一、开发目的和平台不同对比项ReactReact Native应用…

Pycaita二次开发基础代码解析:几何体重命名与参数提取技术

一、几何体智能重命名技术1.1 功能需求与应用场景classmethod def rename_bodies(cls):"""重命名零部件中的所有几何体"""# 主几何体名称标准化opart.main_body.name "零件几何体"i 1 # 计数器初始化for body in opart.bodies:if b…

《React Router深解:复杂路由场景下的性能优化与导航流畅性构建》

路由系统是连接用户操作与应用功能的中枢神经,而React Router作为React生态中处理路由逻辑的核心工具,其在复杂应用中的表现直接决定着用户体验的优劣。当应用规模扩张至数十甚至上百个路由,嵌套层级跨越多层,导航控制中的性能问题便会逐渐凸显——从首屏加载的延迟到路由切…

Linux进程程序替换

单进程版程序替换——最简单的程序替换程序替换&#xff08;Process Replacement&#xff09;是Linux/Unix系统中一个重要的概念&#xff0c;指的是一个正在运行的进程完全被另一个程序替换的过程。这是通过exec系列函数实现的。特点&#xff1a;1.进程不变性。替换前后进程的P…

【数据结构与算法】21.合并两个有序链表(LeetCode)

文章目录合并两个有序链表&#xff1a;高效算法解析与实现问题描述核心思路&#xff1a;双指针尾插法完整代码实现关键点解析1. 边界条件处理2. 头节点初始化3. 节点比较与插入4. 剩余节点处理常见错误与修正优化方案&#xff1a;哨兵节点算法应用场景总结总结合并两个有序链表…

gd32modbus从机移植

文章目录1. 背景2. 改写方式2.1 cursor2.2 使用方式3. 移植过程修改概述修改的文件和内容1. PRO2/Core/Inc/usart.h2. PRO2/Core/Src/usart.c3. PRO2/Drivers/BSP/STM32MB/port/portserial.c4. PRO2/Core/Src/stm32f1xx_it.c5. PRO2/Core/Src/main.c6. PRO2/Core/Src/gpio.c引脚…