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) 功…...

EasyCVR视频汇聚平台云计算技术核心优势:高效、灵活与可扩展性深度解读
随着科技的飞速发展和社会的不断进步,视频监控已经成为现代社会治安防控、企业管理等场景安全管理中不可或缺的一部分。在这一背景下,EasyCVR视频汇聚平台凭借其强大的云计算技术,展现出了卓越的性能和广泛的应用前景。本文将深入解析EasyCVR…...

JavaScript高阶笔记总结(Xmind格式):第一天
Xmind鸟瞰图: 简单文字总结: js高阶知识总结: 理解Object: 1.返回一个由一个给定对象的自身可枚举属性组成的数组:Object.keys(对象名) 2.in 判断属性是否存在:"属性名" in 对象名 …...

十三、代理模式
文章目录 1 基本介绍2 案例2.1 Sortable 接口2.2 BubbleSort 类2.3 SortTimer 类2.4 Client 类2.5 Client 类的运行结果2.6 总结 3 各角色之间的关系3.1 角色3.1.1 Subject ( 主体 )3.1.2 RealObject ( 目标对象 )3.1.3 Proxy ( 代理 )3.1.4 Client ( 客户端 ) 3.2 类图 4 动态…...

Unity物理模块 之 2D效应器
本文仅作笔记学习和分享,不用做任何商业用途 本文包括但不限于unity官方手册,unity唐老狮等教程知识,如有不足还请斧正 1.什么是效应器 2D 效应器 - Unity 手册 2D 效应器是与 2D 碰撞器一起使用的组件,相当于预先编写好的插…...

一款手机壳凭什么卖800元?Casetify品牌策略全解析 | 品牌出海
Casetify官网 巴黎奥运会,张怡宁的手机壳火了。 张怡宁在现场观战并使用手机的照片在网上流传,不是因为这位奥运前冠军,而是她的手机壳。这款满是「花花绿绿」图案的手机壳,迅速被网友发掘出是Casetify品牌的名为「炫彩花卉」的…...

【Rust光年纪】并发编程利器:探索 Rust 异步库与并行处理工具
构建高效异步应用:Rust 异步库详细解读 前言 在当今软件开发领域,Rust语言作为一种快速、安全和并发性能出色的编程语言,备受开发者青睐。随着Rust生态系统的不断扩大,越来越多的异步库和并行处理工具被引入到Rust开发中。本文将…...

机器学习第一课
1.背景 有监督学习:有标签(连续变量(回归问题:时间序列等)、分类变量(分类)) 无监督学习:没有标签(聚类、关联(相关性分析:哪些相关…...

C语言典型例题32
《C程序设计教程(第四版)——谭浩强》 习题2.9 编程序用getchar函数读入两个字符给c1,c2,然后分别用putchar函数和printf函数输出这两个字符。 (1)变量c1,c2应该定义为字符型或者整型吗&#x…...

第二十五天学习笔记2024.8.9
1、通过frp内网穿透共享数据库信息 [root1 ~]# mysql -p密码 mysql> create user li% identified by 1; mysql> create database test; mysql> grant all on test.* to li; [root1 ~]# tar -xf frp_0.33.0_linux_amd64.tar.gz [root1 ~]# cd frp_0.33.0_linux_a…...

sqlserver将一张表导出成txt
bcp zjwb_sb_20111122.dbo.ep_pb_groupvisitplace out c:/1.txt -n -U sa -P sa...