Spring声明式事务失效场景
Spring声明式事务失效场景
- 背景
- 搭建测试环境
- 测试事务失效场景
- @Transactional 注解标注在 private 方法上
- 异常被 catch 了,事务失效
- 方法抛出的是受检异常,事务也会失效
- 事务传播行为配置不合理导致事务失效
背景
Spring 针对 Java Transaction API (JTA)、JDBC、Hibernate 和 Java Persistence API (JPA) 等事务 API,实现了一致的编程模型,而 Spring 的声明式事务功能更是提供了极其方便的事务配置方式,配合 Spring Boot 的自动配置,大多数 Spring Boot 项目只需要在方法上标记 @Transactional 注解,即可一键开启方法的事务性配置。
但是很多童鞋在使用上大多仅限于为方法标记 @Transactional,不会去关注事务是否有效、出错后事务是否正确回滚,也不会考虑复杂的业务代码中涉及多个子业务逻辑时,怎么正确处理事务。
本文既是记录这些坑
搭建测试环境
为了简单,这里使用 SpringBoot 整合 Mp快速搭建一下环境
spring.datasource.url=jdbc:mysql://192.168.133.128:3306/wxpay?useUnicode=true&characterEncoding=utf-8&rewriteBatchedStatements=true&useSSL=false&serverTimezone=Asia/Shanghai
spring.datasource.username=root
spring.datasource.password=root
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver#mybatis-plus.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl
mybatis-plus.mapper-locations=classpath*:mapper/*.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"><modelVersion>4.0.0</modelVersion><parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>3.3.0</version><relativePath/> <!-- lookup parent from repository --></parent><groupId>com.example.springbootV3</groupId><artifactId>springbootV3</artifactId><version>0.0.1-SNAPSHOT</version><name>springbootV3</name><properties><java.version>17</java.version></properties><dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId><version>3.1.3</version></dependency><dependency><groupId>com.mysql</groupId><artifactId>mysql-connector-j</artifactId></dependency><!-- springboot3版本整合mp--><dependency><groupId>com.baomidou</groupId><artifactId>mybatis-plus-spring-boot3-starter</artifactId><version>3.5.5</version></dependency><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId></dependency></dependencies><build><plugins><plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId></plugin></plugins></build>
</project>
@SpringBootApplication
@MapperScan(basePackages = "com.example.demo.mapper")
public class DemoApplication {public static void main(String[] args) {var run = SpringApplication.run(DemoApplication.class, args);}
}
@RestController
@RequestMapping("/tx")
public class TxController {@Resourceprivate UserService userService;@GetMapping("/createUser")public int createUser(@RequestParam("name") String name) {return userService.createUser(name);}
}
测试事务失效场景
@Transactional 注解标注在 private 方法上
代码如下,在 controller 层调用 userService 的 createUser 方法,但是 createUser 方法并没有标注 Transactional 注解,这样搞事务是不会生效的,虽然抛了异常,数据还是入库了
那你可能会想到,把 insertUser 方法变成 public 不就行了,然后重新测试,发现依然不行哈哈,因为Spring 通过 AOP 技术对方法进行增强,要调用增强过的方法必然是调用代理后的对象,而 this 指针代表对象自己,Spring 不可能注入 this,所以通过 this 访问方法必然不是代理。
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {@Overridepublic int createUser(String name) {User user = new User();user.setAge(18);user.setId(1);user.setName(name);user.setCreateTime(new Date());try {this.insertUser(user);} catch (Exception ex) {log.error("create user failed because {}", ex);}return 1;}@Transactionalprivate void insertUser(User user) {this.save(user);throw new RuntimeException("invalid username!");}
}
像上面这种可以简化如下,也被称为Spring AOP 自调用问题,当一个方法被标记了@Transactional 注解的时候,Spring 事务管理器只会在被其他类方法调用的时候生效,而不会在一个类中方法调用生效。这是因为 Spring AOP 工作原理决定的。因为 Spring AOP 使用动态代理来实现事务的管理,它会在运行的时候为带有 @Transactional 注解的方法生成代理对象,并在方法调用的前后应用事物逻辑。如果该方法被其他类调用我们的代理对象就会拦截方法调用并处理事务。但是在一个类中的其他方法内部调用的时候,我们代理对象就无法拦截到这个内部调用,因此事务也就失效了。
@Service
public class MyService {
private void method1() {method2();//......
}
@Transactionalpublic void method2() {//......}
}
异常被 catch 了,事务失效
看到上面的例子,你可能马上想出整改方向,直接 controller 调用 service 层带有 Transactional 注解的 public 方法就好了,于是你立马写出下面一版代码
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {@Override@Transactionalpublic int createUser(String name) {User user = new User();user.setAge(18);user.setName(name);user.setCreateTime(new Date());try {this.save(user);throw new RuntimeException("invalid username!");} catch (Exception ex) {log.error("create user failed because {}", ex);}return 1;}
启动重新运行,发现事务还是失效了…
通过 AOP 实现事务处理可以理解为,使用 try…catch…来包裹标记了 @Transactional 注解的方法,当方法出现了异常并且满足一定条件的时候,在 catch 里面我们可以设置事务回滚,没有异常则直接提交事务。
这里的“一定条件”,主要包括两点。
第一,只有异常传播出了标记了 @Transactional 注解的方法,事务才能回滚。
第二,默认情况下,出现 RuntimeException(非受检异常)或 Error 的时候,Spring 才会回滚事务。
那怎么整改呢?很简单,在 catch 代码块加上手动回滚代码
@Override
@Transactional
public int createUser(String name) {User user = new User();...try {this.save(user);throw new RuntimeException("invalid username!");} catch (Exception ex) {log.error("create user failed because {}", ex);TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();//手动回滚}return 1;
}
方法抛出的是受检异常,事务也会失效
上面也说了,默认情况下,出现 RuntimeException(非受检异常)或 Error 的时候,Spring 才会回滚事务,假如你写出下面的代码
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {@Override@Transactionalpublic int createUser(String name) throws IOException {User user = new User();user.setAge(18);user.setId(1);user.setName(name);user.setCreateTime(new Date());this.save(user);otherTask(); // 抛出了IOException ,这是个受检异常return 1;}private void otherTask() throws IOException {Files.readAllLines(Paths.get("file-that-not-exist"));}
}
启动重新运行,发现事务失效了,那怎么改呢,很简单,·在注解中声明,期望遇到所有的 Exception 都回滚事务(来突破默认不回滚受检异常的限制):@Transactional(rollbackFor = Exception.class)
事务传播行为配置不合理导致事务失效
有这么一个场景:一个用户注册的操作,会插入一个主用户到用户表,还会注册一个关联的子用户。我们希望将子用户注册的数据库操作作为一个独立事务来处理,即使失败也不会影响主流程,即不影响主用户的注册。
@Service
@Slf4j
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {@Resourceprivate SubUserService subUserService;@Override@Transactionalpublic int createUser(String name) {createMainUser(name+"主"); // 注册主账号subUserService.createSubUserWithExceptionWrong(name); // 注册子账号return 1;}private void createMainUser(String name) {User user = new User();user.setAge(18);user.setId(1);user.setName(name);user.setCreateTime(new Date());this.save(user);log.info("注册主账号..");}
}
@Service
@Slf4j
public class SubUserService {@Autowiredprivate UserMapper userMapper;@Transactionalpublic void createSubUserWithExceptionWrong(String name) {User user = new User();user.setAge(18);user.setId(22);user.setName(name+"子");user.setCreateTime(new Date());userMapper.insert(user); // 这里不要使用 userService的方法,不然启动报循环引用错误》throw new RuntimeException("注册子账号失败了...");}
}
@GetMapping("/createUser")
public int createUser(@RequestParam("name") String name) {try {return userService.createUser(name);} catch (IOException e) {log.error("createUserWrong failed, reason:{}", e.getMessage());}return 222;
}
启动运行会发现,事务回滚了,子账号和主账号都没有插入到数据库。
你马上就会意识到,不对呀,因为运行时异常逃出了 @Transactional 注解标记的 createUser 方法,Spring 当然会回滚事务了。如果我们希望主方法不回滚,应该把子方法抛出的异常捕获了。也就是这么改,把 subUserService.createSubUserWithExceptionWrong 包裹上 catch,这样外层主方法就不会出现异常了:
@Override
@Transactional
public int createUser(String name) {createMainUser(name+"主"); // 注册主账号try {subUserService.createSubUserWithExceptionWrong(name); // 注册子账号} catch (Exception exception) {// 虽然捕获了异常,但是因为没有开启新事务,而当前事务因为异常已经被标记为 rollback了,所以最终还是会回滚。log.error("create sub user error:{}", exception.getMessage());}return 1;
}
你按照上面改了之后发现还是不行,还是回滚了,这是因为,主方法注册主用户的逻辑和子方法注册子用户的逻辑是同一个事务,子逻辑标记了事务需要回滚,主逻辑自然也不能提交了。
看到这里,修复方式就很明确了,想办法让子逻辑在独立事务中运行,也就是改一下 SubUserService 注册子用户的方法,为注解加上 propagation = Propagation.REQUIRES_NEW 来设置 REQUIRES_NEW 方式的事务传播策略,也就是执行到这个方法时需要开启新的事务,并挂起当前事务:
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void createSubUserWithExceptionWrong(String name) {User user = new User();user.setAge(18);user.setId(22);user.setName(name+"子");user.setCreateTime(new Date());userMapper.insert(user); // 这里不要使用 userService的方法,不然启动报循环引用错误》throw new RuntimeException("注册子账号失败了...");
}
主方法没什么变化,同样需要捕获异常,防止异常漏出去导致主事务回滚
@Override
@Transactional
public int createUser(String name) {createMainUser(name+"主"); // 注册主账号try {subUserService.createSubUserWithExceptionWrong(name); // 注册子账号} catch (Exception exception) {// 捕获异常,防止主方法回滚log.error("create sub user error:{}", exception.getMessage());}return 1;
}
数据库可以看到,主账号的注册不受影响
相关文章:

Spring声明式事务失效场景
Spring声明式事务失效场景 背景搭建测试环境测试事务失效场景Transactional 注解标注在 private 方法上异常被 catch 了,事务失效方法抛出的是受检异常,事务也会失效事务传播行为配置不合理导致事务失效 背景 Spring 针对 Java Transaction API (JTA)、…...

基于SpringBoot+UniAPP宠物食品外卖点单小程序的设计与实现》
✅博主简介:Java 全栈开发工程师,抖音优质技术创作者,日常分享实用的前端、后端、运维开发技术。 ✅技术栈:Java、SpringBoot、Vue、React、Node.js、Nest.js、Nuxt.js、uni-app ✅技术擅长:计算机毕设选题、开题报告、…...
ssrf 内网访问 伪协议 读取文件 端口扫描
SSRF(Server-Side Request Forgery,服务器侧请求伪造)是一种利用服务器发起网络请求的能力来攻击内网资源或执行其他恶意活动的技术。SSRF可以用于访问通常不可由外部直接访问的内网资源,读取文件,甚至进行端口扫描。以…...

发布包到npm
目录 注册npm账号 创建包 登录npm 上架包 更新包 删除包 注册npm账号 首先注册npm账号:npm | Sign Up (npmjs.com) 创建包 可以在桌面上新建一个文件夹:文件夹名随便起,但是别跟npm已经上架的包名重复了 可以通过下面的指令查看&…...

Python | Leetcode Python题解之第324题摆动排序II
题目: 题解: def quickSelect(a: List[int], k: int) -> int:seed(datetime.datetime.now())shuffle(a)l, r 0, len(a) - 1while l < r:pivot a[l]i, j l, r 1while True:i 1while i < r and a[i] < pivot:i 1j - 1while j > l an…...

IGModel——提高基于 GNN与Attention 机制的方法在药物发现中的实用性
导言 深度学习在药物发现(发现治疗药物)领域的应用以及传统方法面临的挑战。 药物(尤其是我们将在本文中讨论的被称为抑制剂的药物)通过与在人体中发挥不良功能的蛋白质结合并改变这些蛋白质的功能来发挥治疗效果。因此…...

AArch64中的寄存器
目录 通用寄存器 其他寄存器 系统寄存器 通用寄存器 大多数A64指令在寄存器上操作。该架构提供了31个通用寄存器。 每个寄存器可以作为64位的X寄存器(X0..X30)使用,或者作为32位的W寄存器(W0..W30)使用。这两种是查…...

树莓派Pico 2来了
这两天开源圈的大事之一,就是树莓派基金会发布了树莓派Pico 2。 帖子原文:Raspberry Pi Pico 2, our new $5 microcontroller board, on sale now 总结一些关键信息: 产品发布:Raspberry Pi Pico 2 是 Raspberry Pi 基金会推出的…...
LeetCode面试题Day7|LeetCode135 分发糖果、LeetCode42 接雨水
题目1: 指路: . - 力扣(LeetCode)135 分发糖果 思路与分析: 给n个孩子按照评分给糖果,要求有二,其一为每个孩子最少有一颗糖果;其二为相邻孩子评分更高的糖果越多。那么在这里第…...

[免费]适用于 Windows 10 的十大数据恢复软件
Windows 10 是 Microsoft 开发的跨平台和设备应用程序操作系统。它启动速度更快,具有熟悉且扩展的“开始”菜单,甚至可以在多台设备上以新的方式工作。因此,Windows 10 非常受欢迎,我们用它来保存照片、音乐、文档和更多文件。但有…...

Win11+docker+vscode配置anomalib并训练自己的数据(3)
在前两篇博文中,我使用Win11+docker配置了anomalib,并成功的调用了GPU运行了示例程序。这次我准备使用anomalib训练我自己的数据集。 数据集是我在工作中收集到的火腿肠缺陷数据,与MVTec等数据不同,我的火腿肠数据来源于多台设备和多个品种,因此,它们表面的纹理与颜色差异…...

Java | Leetcode Java题解之第332题重新安排行程
题目: 题解: class Solution {Map<String, PriorityQueue<String>> map new HashMap<String, PriorityQueue<String>>();List<String> itinerary new LinkedList<String>();public List<String> findItine…...
招聘公告|健安环保科技(广东)有限公司
招聘岗位:销售经理 岗位职责: 对PCB线路板和电镀行业的客户,推广针对镀锡漂洗水的低浓度锡回收技术(投资运营或设备销售),并销售无耗材材的电镀智能过滤设备,达成销售目标; 任职要求: 1、大专以上学历&…...
小程序的安全设计
小程序的安全设计 安全指引 | 微信开放文档 (qq.com) 开发原则与注意事项 本文档整理了部分小程序开发中常见的安全风险和漏洞,用于帮助开发者在开发环节中发现和修复相关漏洞,避免在上线后对业务和数据造成损失。 开发者在开发环节中必须基于以下原则: 互不信任原则,不要…...

【Android】网络技术知识总结之WebView,HttpURLConnection,OKHttp,XML的pull解析方式
文章目录 webView使用步骤示例 HttpURLConnection使用步骤示例GET请求POST请求 okHttp使用步骤1. 添加依赖2. 创建OkHttpClient实例3. 创建Request对象构建请求4. 发送请求5. 获取响应 Pull解析方式1. 准备XML数据2. 创建数据类3. 使用Pull解析器解析XML webView WebView 是 An…...

Kubernetes—k8s集群存储卷(pvc存储卷)
目录 一、PVC 和 PV 1.PV 2.PVC 3.StorageClass 4.PV和PVC的生命周期 二、实操 1.创建静态pv 1.配置nfs 2.创建pv 3.创建pvc 4.结合pod,将pv、pvc一起运行 2.创建动态pv 1.上传 2.创建 Service Account,用来管理 NFS Provisioner 在 k8s …...
用网格大师转换的3D Tiles数据,在进行了顶点重建后,尝试加载到Cesium中却无法显示内容。应该如何解决这一问题?
答: 建议首先尝试使用DasViewer来打开并检查这个3D Tiles的json文件。DasViewer能够迅速加载并显示3D Tiles数据,可以帮助快速验证数据是否完整且格式正确。 网格大师是一款能够解决实景三维模型空间参考、原点、瓦块大小不统一,重叠区域处理…...

display:flex布局,最简单的案例
1. 左右贴边 <!DOCTYPE html> <html lang"en"> <head><meta charset"UTF-8"><title>Title</title><style>#parent{width: 800px;background: red;height: 200px;display: flex;justify-content: space-between…...

SQL注入实例(sqli-labs/less-17)
0、初始网页 1、确定闭合字符 注入点在于password框,闭合字符为单引号 2、爆库名 1 and updatexml(1,concat(0x7e,database(),0x7e),1)# 1 and (select 1 from (select count(*),concat((select database()),floor(rand()*2))x from information_schema.tables gr…...

HTML+CSS+JS计算器
效果图 计算器功能详解 本计算器实现了多种功能,以下是所有功能的详细说明: 清空显示框 © 功能: 清除显示框中的所有内容。解释: 该功能用于重置计算器状态,清空当前输入的内容,使用户可以重新开始输入。 输入数字 (0-9) 功…...

shell脚本--常见案例
1、自动备份文件或目录 2、批量重命名文件 3、查找并删除指定名称的文件: 4、批量删除文件 5、查找并替换文件内容 6、批量创建文件 7、创建文件夹并移动文件 8、在文件夹中查找文件...
vue3 字体颜色设置的多种方式
在Vue 3中设置字体颜色可以通过多种方式实现,这取决于你是想在组件内部直接设置,还是在CSS/SCSS/LESS等样式文件中定义。以下是几种常见的方法: 1. 内联样式 你可以直接在模板中使用style绑定来设置字体颜色。 <template><div :s…...
Linux云原生安全:零信任架构与机密计算
Linux云原生安全:零信任架构与机密计算 构建坚不可摧的云原生防御体系 引言:云原生安全的范式革命 随着云原生技术的普及,安全边界正在从传统的网络边界向工作负载内部转移。Gartner预测,到2025年,零信任架构将成为超…...
【git】把本地更改提交远程新分支feature_g
创建并切换新分支 git checkout -b feature_g 添加并提交更改 git add . git commit -m “实现图片上传功能” 推送到远程 git push -u origin feature_g...

面向无人机海岸带生态系统监测的语义分割基准数据集
描述:海岸带生态系统的监测是维护生态平衡和可持续发展的重要任务。语义分割技术在遥感影像中的应用为海岸带生态系统的精准监测提供了有效手段。然而,目前该领域仍面临一个挑战,即缺乏公开的专门面向海岸带生态系统的语义分割基准数据集。受…...
API网关Kong的鉴权与限流:高并发场景下的核心实践
🔥「炎码工坊」技术弹药已装填! 点击关注 → 解锁工业级干货【工具实测|项目避坑|源码燃烧指南】 引言 在微服务架构中,API网关承担着流量调度、安全防护和协议转换的核心职责。作为云原生时代的代表性网关,Kong凭借其插件化架构…...

Linux 下 DMA 内存映射浅析
序 系统 I/O 设备驱动程序通常调用其特定子系统的接口为 DMA 分配内存,但最终会调到 DMA 子系统的dma_alloc_coherent()/dma_alloc_attrs() 等接口。 关于 dma_alloc_coherent 接口详细的代码讲解、调用流程,可以参考这篇文章,我觉得写的非常…...

Python环境安装与虚拟环境配置详解
本文档旨在为Python开发者提供一站式的环境安装与虚拟环境配置指南,适用于Windows、macOS和Linux系统。无论你是初学者还是有经验的开发者,都能在此找到适合自己的环境搭建方法和常见问题的解决方案。 快速开始 一分钟快速安装与虚拟环境配置 # macOS/…...

项目进度管理软件是什么?项目进度管理软件有哪些核心功能?
无论是建筑施工、软件开发,还是市场营销活动,项目往往涉及多个团队、大量资源和严格的时间表。如果没有一个系统化的工具来跟踪和管理这些元素,项目很容易陷入混乱,导致进度延误、成本超支,甚至失败。 项目进度管理软…...

华为云Flexus+DeepSeek征文 | 基于Dify构建具备联网搜索能力的知识库问答助手
华为云FlexusDeepSeek征文 | 基于Dify构建具备联网搜索能力的知识库问答助手 一、构建知识库问答助手引言二、构建知识库问答助手环境2.1 基于FlexusX实例的Dify平台2.2 基于MaaS的模型API商用服务 三、构建知识库问答助手实战3.1 配置Dify环境3.2 创建知识库问答助手3.3 使用知…...