掌握 Spring 事务管理:深入理解 @Transactional 注解

在业务方法上使用@Transactional开启声明式事务时,很有可能由于使用方式有误,导致事务没有生效。

环境准备

表结构

CREATE TABLE `admin` (`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,`username` varchar(255)  DEFAULT NULL,`password` varchar(255)  DEFAULT NULL,`name` varchar(255)  DEFAULT NULL,`phone` int(11) DEFAULT NULL,`power` int(11) DEFAULT NULL,PRIMARY KEY (`id`)
) ENGINE=InnoDB;

实体类

@Entity
@Data
public class Admin {@Id@GeneratedValue(strategy = GenerationType.IDENTITY)private  Long id;private  String  username;private  String  password;private  String  name;private  Integer phone;private  Integer power;
}

DAO 层

@Repository
public interface AdminRepository extends JpaRepository<Admin,Long> {List<Admin> findByName(String name);
}

上面这些类都是不变的,主要是 service 类。

事务失效

非 public

当被@Transactional注解修饰的方法为非public时,事务将失效。

@Service
@Slf4j
public class AdminService {@Autowiredprivate AdminRepository adminRepository;@Transactionalprotected void saveAdmin(Admin admin) {adminRepository.save(admin);if (admin.getName().contains("@")) {throw new RuntimeException("不合法");}}
}

在同包下新建一个测试类。

@Autowired
private AdminService adminService;@GetMapping("/addAdminWrong")
public void add(@RequestParam("name") String name) {Admin admin = new Admin();admin.setName(name);adminService.saveAdmin(admin);
}

测试接口发现,即使用户名不合法,用户也能创建成功。

@Transactional 生效原则(一):只有定义在 public 方法上的 @Transactional 才能生效。原因是,Spring 默认通过动态代理的方式实现 AOP,对目标方法进行增强,private 方法无法代理到,Spring 自然也无法动态增强事务处理逻辑。

所以,将 saveAdmin 方法,修改为 public,就可以了。

自调用

saveAdmin 方法是 public时,事务一定能生效吗?

答案是不一定,比如下面这个例子。

@Service
@Slf4j
public class AdminService {@Autowiredprivate AdminRepository adminRepository;public int addAdminWrong(String name) {Admin admin = new Admin();admin.setName(name);try {/*** 一些其他业务处理*/this.saveAdmin(admin);} catch (Exception e) {log.error("添加失败:{}",e);}return adminRepository.findByName(name).size();}@Transactionalpublic void saveAdmin(Admin admin) {adminRepository.save(admin);if (admin.getName().contains("@")) {throw new RuntimeException("不合法");}}
}

在上面代码中,我们新定义了一个addAdminWrong方法,并在它内部调用了本类的saveAdmin方法。

测试代码如下:

@GetMapping("/addAdminWrong")
public void add(@RequestParam("name") String name) {adminService.addAdminWrong(name);
}

测试后发现,不合法的用户,还是被创建成功了。

Creating new transaction with name [org.springframework.data.jpa.repository.support.SimpleJpaRepository.save]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT

观察日志发现,自调用因为没有走代理,事务没有在 saveAdmin 方法上生效,只在 SimpleJpaRepository 上的 save 方法层面生效。


最简单的修改方案是,在AdminService类中,自己注入自己,比如:

@Autowired
private AdminService self;

然后通过 self 实例去调用 self.saveAdmin(admin)


还有一种优雅的方案,是通过AopContext在代理对象中获取自身。

比如:

AdminService adminService = (AdminService) AopContext.currentProxy();
adminService.saveAdmin(admin);

然后就会发现一个异常:

Cannot find current proxy: Set 'exposeProxy' property on Advised to 'true' to make it available, and ensure that AopContext.currentProxy() is invoked in the same thread as the AOP invocation context.

它的意思是:没有开启一个 exposeProxy 的属性,导致无法暴露出代理对象,从而无法获取。

所以我们在启动类上加上这个注解 @EnableAspectJAutoProxy(exposeProxy=true)即可。

@SpringBootApplication
@EnableAspectJAutoProxy(exposeProxy=true) //暴露代理对象
public class StarterDemoApplication {public static void main(String[] args) {SpringApplication.run(StarterDemoApplication.class, args);}
}

然后,再观察日志,发现事务在AdminService.saveAdmin方法上生效了

Creating new transaction with name [com.starter.demo.controller.AdminService.saveAdmin]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT

@Transactional 生效原则(二):需要确保方法调用是通过Spring的代理对象进行的,而不是直接在类内部调用。

异常处理不当

上面的两个例子,是由于事务失效导致回滚失败。

接下来,我们来看下,即使事务生效也会回滚失败的场景。

(一):被@Transactional注解标记的方法抛出了异常,事务才会回滚。

意思就是说,得把异常抛出来才行。

在 Spring 的 TransactionAspectSupport.invokeWithinTransaction 方法中,可以找到处理事务的逻辑,可以看到只有捕获到异常才能进行后续事务处理。

在这里插入图片描述

比如这段代码,虽然在方法中抛出了异常,但又被它自己给捕获了。

 @Transactionalpublic void saveAdminWrong1(String name) {Admin admin = new Admin();admin.setName(name);try {adminRepository.save(admin);throw new RuntimeException("模拟错误");} catch (Exception e) {log.error("save admin error:",e);}}

同时再次观察日志可以发现,虽然事务在AdminService.saveAdminWrong1上是生效的,但由于异常没有被传播出去,所以无法回滚。

Creating new transaction with name [com.starter.demo.controller.AdminService.saveAdminWrong1]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT

我们可以手动回滚当前事务,在 catch 代码块中加上TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();

通过日志可以看到回滚的信息。

2024-11-22 17:13:08.537 DEBUG 7219 --- [nio-8080-exec-1] o.s.orm.jpa.JpaTransactionManager        : Transactional code has requested rollback
2024-11-22 17:13:08.537 DEBUG 7219 --- [nio-8080-exec-1] o.s.orm.jpa.JpaTransactionManager        : Initiating transaction rollback
2024-11-22 17:13:08.537 DEBUG 7219 --- [nio-8080-exec-1] o.s.orm.jpa.JpaTransactionManager        : Rolling back JPA transaction on EntityManager [SessionImpl(1495642341<open>)]

(二):默认情况下,出现 RuntimeException 或 Error 的时候,Spring 才会回滚事务。

追踪completeTransactionAfterThrowing方法,可以看到,它是根据异常的类型来决定是否回滚的。

在这里插入图片描述

点进 rollbackOn 方法,可以看到,它只会在RuntimeExceptionError 的时候返回 true。

在这里插入图片描述

比如这段代码,我们希望保存用户的时候,同时去加载一个文件,如果加载文件失败,则事务需要回滚。

@Transactional
public void saveAdminWrong2(String name) throws IOException {Admin admin = new Admin();admin.setName(name);adminRepository.save(admin);otherTask(); //额外的操作}private void otherTask() throws IOException{Files.readAllLines(Paths.get("admin.txt"));}

同时观察日志可以发现,虽然事务在AdminService.saveAdminWrong2上是生效的,也没有去捕获异常,但是由于传播出去的是 checked exception,所以事务也不会回滚。

Creating new transaction with name [com.starter.demo.controller.AdminService.saveAdminWrong2]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT

可以在注解中声明,希望遇到所有的 Exception 都回滚事务。

@Transactional(rollbackFor = Exception.class)
public void saveAdminWrong2(String name) throws IOException {Admin admin = new Admin();admin.setName(name);adminRepository.save(admin);otherTask(); //额外的操作}private void otherTask() throws IOException{Files.readAllLines(Paths.get("test.sql"));
}

同时观察日志可以发现,事务在AdminService.saveAdminWrong2上是生效的,还看到了回滚的日志信息。

2024-11-22 17:29:24.846 DEBUG 7560 --- [nio-8080-exec-1] o.s.orm.jpa.JpaTransactionManager        : Creating new transaction with name [com.starter.demo.controller.AdminService.saveAdminWrong2]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT,-java.lang.Exception
2024-11-22 17:29:24.948 DEBUG 7560 --- [nio-8080-exec-1] o.s.orm.jpa.JpaTransactionManager        : Initiating transaction rollback
2024-11-22 17:29:24.948 DEBUG 7560 --- [nio-8080-exec-1] o.s.orm.jpa.JpaTransactionManager        : Rolling back JPA transaction on EntityManager [SessionImpl(1844074856<open>)]

事务传播行为不当

比如,在插入用户信息的时候,也插入一份扩展信息,但由于扩展信息不是很重要,即使它失败了,也不要影响到我们的主逻辑(把插入扩展信息的操作当成一个独立的事务)。

@Service
@Slf4j
public class AdminService {@Autowiredprivate AdminRepository adminRepository;@Autowiredprivate AddressService addressService;@Transactionalpublic void saveAdminWrong3(String name) {//1.保存用户信息saveAdmin(name);//2.保存扩展信息addressService.saveAddress(name);}private void saveAdmin(String name) {Admin admin = new Admin();admin.setName(name);adminRepository.save(admin);log.info("save admin success");}
}
@Service
@Slf4j
public class AddressService {@Autowiredprivate AddressRepository addressRepository;@Transactionalpublic void saveAddress(String name) {Address address = new Address();address.setName(name);log.info("saveAddress start");addressRepository.save(address);throw new RuntimeException("模拟 save address 失败");}
}

可以看到,saveAddress的操作是失败的,按照我们的期望,saveAdmin方法不能受到影响,能够正常插入成功。

测试执行后发现,saveAdmin方法出现了回滚,不符合我们的预期。

2024-11-22 18:02:18.171 DEBUG 8251 --- [nio-8080-exec-1] o.s.orm.jpa.JpaTransactionManager        : Rolling back JPA transaction on EntityManager [SessionImpl(809976208<open>)]
2024-11-22 18:02:18.188 DEBUG 8251 --- [nio-8080-exec-1] o.s.orm.jpa.JpaTransactionManager        : Not closing pre-bound JPA EntityManager after transaction
2024-11-22 18:02:18.188 ERROR 8251 --- [nio-8080-exec-1] c.s.demo.controller.TestController       : 模拟 save address 失败
2024-11-22 18:02:18.205 DEBUG 8251 --- [nio-8080-exec-1] o.j.s.OpenEntityManagerInViewInterceptor : Closing JPA EntityManager in OpenEntityManagerInViewInterceptor

我们来猜想一下,是不是因为saveAddress抛出的异常,没有在saveAdminWrong3中捕获,而saveAdminWrong3也会接着往上层抛,导致被回滚了呢?

所以呢,我们先在saveAdminWrong3方法中捕获一下saveAddress抛出的异常试试。

@Transactional
public void saveAdminWrong3(String name) {//1.保存用户信息saveAdmin(name);//2.保存扩展信息try {addressService.saveAddress(name);} catch (Exception e) {log.error("save address error:{}",e.getMessage());}
}

运行程序,再次观察日志:

2024-11-22 18:39:41.741 DEBUG 8496 --- [nio-8080-exec-1] o.s.orm.jpa.JpaTransactionManager        : Creating new transaction with name [com.starter.demo.service.AdminService.saveAdminWrong3]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT2024-11-22 18:39:41.850 DEBUG 8496 --- [nio-8080-exec-1] o.s.orm.jpa.JpaTransactionManager        : Participating transaction failed - marking existing transaction as rollback-only2024-11-22 18:39:41.851 ERROR 8496 --- [nio-8080-exec-1] com.starter.demo.service.AdminService    : save address error:模拟 save address 失败2024-11-22 18:39:41.851 DEBUG 8496 --- [nio-8080-exec-1] o.s.orm.jpa.JpaTransactionManager        : Committing JPA transaction on EntityManager [SessionImpl(98155953<open>)]2024-11-22 18:39:41.884 ERROR 8496 --- [nio-8080-exec-1] c.s.demo.controller.TestController       : org.springframework.transaction.UnexpectedRollbackException: Transaction silently rolled back because it has been marked as rollback-only

通过日志,可以发现:

  1. AdminService.saveAdminWrong3上开启了事务处理;
  2. 当前事务被标记为了回滚;
  3. saveAdminWrong3中打印出了saveAddress的异常信息;
  4. 主方法已经提交了事务;
  5. TestController中打印了一个UnexpectedRollbackException,提示这个事务要静默回滚了。

UnexpectedRollbackException 是 Spring 框架抛出的一个异常,表明事务由于某些原因被静默地标记为只能回滚(rollback-only),意味着事务不会正常提交,而是会在结束时被回滚。

saveAdminWrong3方法中并没有出现异常,所以在事务提交时,发现当前事务已经被子方法设置成了回滚,导致无法正常提交,进而证实了saveAdminWrong3saveAddress使用了同一个事务。

@Transactional注解中,propagation 属性决定了事务的传播行为,默认是REQUIRED

REQUIRED:如果当前存在事务,则加入该事务;如果当前没有事务,则创建一个新的事务。

所以,这也说明了saveAddress方法不会开启一个新事务,而是会加入到saveAdminWrong3的事务中。

所以,需要将propagation 设置为 REQUIRES_NEW

REQUIRES_NEW:它会创建一个新事务,如果当前存在事务,把当前事务挂起,直到新事务完成。这种传播行为适用于需要独立于当前事务的场景。

修改AddressService的代码,其他不变。

@Service
@Slf4j
public class AddressService {@Autowiredprivate AddressRepository addressRepository;@Transactional(propagation = Propagation.REQUIRES_NEW)public void saveAddress(String name) {Address address = new Address();address.setName(name);log.info("saveAddress start");addressRepository.save(address);throw new RuntimeException("模拟 save address 失败");}
}

再次执行,查看日志:

2024-11-22 19:13:56.643 DEBUG 9201 --- [nio-8080-exec-1] o.s.orm.jpa.JpaTransactionManager        : Creating new transaction with name [com.starter.demo.service.AdminService.saveAdminWrong3]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT2024-11-22 19:13:56.734  INFO 9201 --- [nio-8080-exec-1] com.starter.demo.service.AdminService    : save admin success2024-11-22 19:13:56.734 DEBUG 9201 --- [nio-8080-exec-1] o.s.orm.jpa.JpaTransactionManager        : Suspending current transaction, creating new transaction with name [com.starter.demo.service.AddressService.saveAddress]2024-11-22 19:13:56.781 DEBUG 9201 --- [nio-8080-exec-1] o.s.orm.jpa.JpaTransactionManager        : Initiating transaction rollback2024-11-22 19:13:56.833 DEBUG 9201 --- [nio-8080-exec-1] o.s.orm.jpa.JpaTransactionManager        : Resuming suspended transaction after completion of inner transaction2024-11-22 19:13:56.833 ERROR 9201 --- [nio-8080-exec-1] com.starter.demo.service.AdminService    : save address error:模拟 save address 失败2024-11-22 19:13:56.834 DEBUG 9201 --- [nio-8080-exec-1] o.s.orm.jpa.JpaTransactionManager        : Committing JPA transaction on EntityManager [SessionImpl(1073120187<open>)]

通过日志可以看到

  1. AdminService.saveAdminWrong3上开启了事务处理;
  2. admin 创建完成;
  3. 主事务挂起了,在AddressService.saveAddress上开启了一个新的子事务;
  4. 子事务回滚了;
  5. 子事务完成,继续被挂起的主事务;
  6. 捕获到了saveAddress的异常;
  7. 主事务提交了,没有看到静默回滚的异常。

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

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

设计模式之 观察者模式

观察者模式&#xff08;Observer Pattern&#xff09;是一种行为型设计模式&#xff0c;它定义了一种一对多的依赖关系&#xff0c;让多个观察者对象同时监听一个主题对象&#xff08;Subject&#xff09;。当主题对象的状态发生变化时&#xff0c;所有依赖于它的观察者都会得到…

【python】将word文档内容转换为excel表格

在日常工作中&#xff0c;我们经常需要将Word文档中的内容提取并转换为Excel表格&#xff0c;以便进行数据分析和处理。本文将介绍如何使用Python编写一个简单的程序&#xff0c;将Word文档中的内容转换为Excel表格。 一.实例 使用以下word文档作为例子&#xff1a; 工具界面如…

Linux|进程程序替换

目录 什么是进程替换 替换原理 exec函数 exec* 函数的共性 什么是进程替换 进程程序替换是指将一个进程中正在运行的程序替换为另一个全新的程序的过程&#xff0c;但替换不是创建新进程&#xff0c;只是将对应程序的代码和数据进行替换。具体来说&#xff0c;这个替换过程涉…

大数运算(加减乘除和输入、输出模块)

为什么会有大数呢&#xff1f;因为long long通常为64位范围约为 -9,223,372,036,854,775,808 到 9,223,372,036,854,775,807&#xff0c;最多也就19位&#xff0c;那么超过19位的如何计算呢&#xff1f;这就引申出来大数了。 本博客适合思考过这道题&#xff0c;但是没做出来或…

IntelliJ+SpringBoot项目实战(四)--快速上手数据库开发

对于新手学习SpringBoot开发&#xff0c;可能最急迫的事情就是尽快掌握数据库的开发。目前数据库开发主要流行使用Mybatis和Mybatis Plus,不过这2个框架对于新手而言需要一定的时间掌握&#xff0c;如果快速上手数据库开发&#xff0c;可以先按照本文介绍的方式使用JdbcTemplat…

flex布局 昵图网【案例】

效果展示 只是个大概&#xff0c;可自己完善。 昵图网 代码展示 <body><!-- https://static.ntimg.cn/original/images/soso.png --><div class"container"><div class"header"><!-- <div class"logo"><i…

[第五空间 2021]pklovecloud 详细题解

知识点: 构造POP链 PHP类的作用域 NULL强比较 目录穿越 源码如下: <?php include flag.php; class pkshow { function echo_name() { return "Pk very safe^.^"; } } class acp { protected $cinder; public $neutron;public $n…

dockerfile构建Nginx镜像练习二(5-2)

环境准备&#xff1a; (1)保证拥有centos基础镜像 docker images | grep centos (2)服务器保证可以连接外网 1.创建工作目录 mkdir nginx cd nginx 2.在工作目录中创建并编写Dockerfile文件 vim dockerfile #定义基础镜像 FROM centos:7#维护者信息(可缺省) MAINTAINER d…

Android Surfaceflinger显示图层合成方式

Android SurfaceFlinger是Android系统中负责窗口管理和图像合成的核心组件。它接收来自不同应用的图层数据&#xff0c;并将这些图层合并成一个单一的图像&#xff0c;然后输出到显示设备上。SurfaceFlinger的合成方式主要涉及两种&#xff1a;Client合成和Device合成。 adb s…

wsl安装

一. wsl简介 1. wsl和wsl2的区别 wsl需要把linux命令翻译为windows命令&#xff0c;性能差一些。 wsl2直接使用linux内核&#xff0c;不需要翻译&#xff0c;性能好&#xff0c;但开销相对大一点&#xff0c;因为需要多运行一个hyper-v虚拟机 (并非完整的虚拟机&#xff0c;是…

任务通知的本质(任务通知车辆运行) 软件定时器的本质(增加游戏音效)

任务通知的本质 没有任务通知 所谓"任务通知"&#xff0c;你可以反过来读"通知任务"。 我们使用队列、信号量、事件组等等方法时&#xff0c;并不知道对方是谁。使用任务通知时&#xff0c;可 以明确指定&#xff1a;通知哪个任务。 使用队列、信号量、…

Kubernetes的pod控制器

文章目录 一&#xff0c;什么是pod控制器二&#xff0c;pod控制器类型&#xff08;重点&#xff09;1.ReplicaSet2.Deployment3.DaemonSet4.StatefulSet5.Job6.Cronjob 三&#xff0c;pod与控制器的关系1.Deployment2.SatefulSet2.1StatefulSet组成2.2headless的由来2.3有状态服…

【单元测试】【Android】JUnit 4 和 JUnit 5 的差异记录

背景 Jetbrain IDE 支持生成 Test 类&#xff0c;其中选择JUnit5 和 JUnit&#xff0c;但是感觉这不是标准的单元测试&#xff0c;因为接口命名吧。 差异对比 两者生成的单测API名称同原API&#xff0c;没加test前缀的。使用差异主要表现在&#xff1a; setUp &#xff06; …

知识中台在多语言客户中的应用

在全球化的商业环境中&#xff0c;企业面临着多语言客户服务的挑战。HelpLook知识中台作为一种智能化解决方案&#xff0c;为企业提供了一个强大的工具&#xff0c;以实现多语言客户服务的自动化和优化。 一、多语言客户服务的重要性 多语言客户服务对于跨国企业至关重要&…

使用 Elastic AI Assistant for Search 和 Azure OpenAI 实现从 0 到 60 的转变

作者&#xff1a;来自 Elastic Greg Crist Elasticsearch 推出了一项新功能&#xff1a;Elastic AI Assistant for Search。你可以将其视为 Elasticsearch 和 Kibana 开发人员的内置指南&#xff0c;旨在回答问题、引导你了解功能并让你的生活更轻松。在 Microsoft AI Services…

【K8S问题系列 |18 】如何解决 imagePullSecrets配置正确,但docker pull仍然失败问题

如果 imagePullSecrets 配置正确&#xff0c;但在执行 docker pull 命令时仍然失败&#xff0c;可能存在以下几种原因。以下是详细的排查步骤和解决方案。 1. 检查 Docker 登录凭证 确保你使用的是与 imagePullSecrets 中相同的凭证进行 Docker 登录&#xff1a; 1.1 直接登录…

Redis的特性ubuntu进行安装

文章目录 1.六大特性1.1内存存储数据1.2可编程1.3可扩展1.4持久化1.5集群1.6高可用1.7速度快 2.具体应用场景&#xff08;了解&#xff09;3.Ubuntu安装Redis3.1安装指令3.2查看状态3.3查找配置文件3.4修改文件内容3.5重启服务器生效3.6安装客户端并进行检查 4.Redis客户端介绍…

【ASE】第八课_冰(ice)的效果

今天我们一起来学习ASE插件&#xff0c;希望各位点个关注&#xff0c;一起跟随我的步伐 今天我们来学习一个简单的冰的效果&#xff0c;这个是根据油管上的视频制作的 可在我的资源里下载模型&#xff0c;贴图&#xff0c;材质 思路 1.物体表面结冰的效果&#xff0c;也就是…

回溯法基础入门解析

回溯法 前 言 回溯法也可以叫做回溯搜索法&#xff0c;它是一种搜索的方式。回溯是递归的副产品&#xff0c;只要有递归就会有回溯。回溯法&#xff0c;一般可以解决如下几种问题&#xff1a; 组合问题&#xff1a;N个数里面按一定规则找出k个数的集合切割问题&#xff1a;一…

Redis原理及应用

Redis简介 Redis是开源的&#xff08;BSD许可&#xff09;&#xff0c;数据结构存储于内存中&#xff0c;被用来作为数据库&#xff0c;缓存和消息代理。它支持多种数据结构&#xff0c;例如&#xff1a;字符串&#xff08;string&#xff09;&#xff0c;哈希&#xff08;hash…