Spring声明式事务不生效?

news/2024/11/16 22:01:50/文章来源:https://www.cnblogs.com/xyuanzi/p/18407909

背景

本篇博文将会讲一讲Spring中使用@Transactional注解会出现的不生效问题。事务的生效与否,一般不是我们冒烟自测的范围,测试也不会去测,但是一旦上线后,事务出现不生效的情况,就可能引发较大的问题,甚至会带来损失。所以,使用好事务注解是非常重要的,尤其是注意哪些场景下会出现事务失效。

事务失效效常见情况分析

  • 1、Transactional注解必须修饰在public方法上面,如果不是public方法,则事务不会生效。
    原理说明
    因为声明式事务是通过SpringAOP代理来实现的,当在类中声明一个事务方法时,Spring会创建一个代理类,以便在调用该类的方法时应用事务管理。
    SpringAOP的实现有JDK动态代理和CGLIB代理,对于实现了接口的方法使用JDK动态代理,那些没有实现接口的方法,则使用CGLIB代理。但是不管哪种代理,都必须保证目标方法是public方法;
    因为Spring AOP在运行时会创建代理对象来拦截目标方法的调用,并在代理对象中添加事务管理的逻辑;如果不是public,那么代理对象就无法直接调用目标方法。
    举例说明
package com.example.demo3.commonpitfalls;
import com.example.demo3.commonpitfalls.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("transactional")
public class TransactionalPit {@Autowiredprivate UserService userService;@GetMapping("wrong")public int wrong1(@RequestParam("name") String name) {return userService.createUserWrong1(name);}
}package com.example.demo3.commonpitfalls.service;import com.example.demo3.commonpitfalls.dto.UserEntity;
import com.example.demo3.commonpitfalls.dto.UserRepository;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;import javax.transaction.Transactional;@Service
@Slf4j
public class UserService {@Autowiredprivate UserRepository userRepository;// 一个公共方法供Controller调用,内部调用事务性的私有方法public int createUserWrong1(String name) {try {this.createUserPrivate(new UserEntity(name));} catch (Exception ex) {log.error("create user failed because {}", ex.getMessage());}return userRepository.findByName(name).size();}//标记了@Transactional的private方法@Transactionalprivate void createUserPrivate(UserEntity entity) {userRepository.save(entity);if (entity.getName().contains("test"))throw new RuntimeException("invalid username!");}
}
package com.example.demo3.commonpitfalls.dto;import lombok.Data;import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.Table;import static javax.persistence.GenerationType.AUTO;
import static javax.persistence.GenerationType.IDENTITY;@Entity
@Data
@Table(name = "user_entity")
public class UserEntity {@Id@GeneratedValue(strategy = IDENTITY)private Long id;private String name;public UserEntity() {}public UserEntity(String name) {this.name = name;}
}
package com.example.demo3.commonpitfalls.dto;import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;import java.util.List;@Repository
public interface UserRepository extends JpaRepository<UserEntity, Long> {List<UserEntity> findByName(String name);
}



当我们传入一个name ,按照代码中的逻辑,会抛出一个异常,然后会期望触发事务的回滚,但我们的执行结果却是数据库成功保存了这个name。那就说明事务并未生效。
仔细看我们的事务注解,即没有指定回滚异常,而且还用在了private方法上面。Spring AOP代理对象无法访问private的方法,从而导致事务没有生效。
修改为方法修饰符为public

controller增加一个方法@GetMapping("wrong2")public int wrong2(@RequestParam("name") String name) {return userService.createUserWrong2(name);}
service中增加两个方法// 一个公共方法供Controller调用,内部调用事务性的公有方法public int createUserWrong2(String name) {try {this.createUserPublic(new UserEntity(name));} catch (Exception ex) {log.error("create user failed because {}", ex.getMessage());}return userRepository.findByName(name).size();}//标记了@Transactional的public方法@Transactionalpublic void createUserPublic(UserEntity entity) {userRepository.save(entity);if (entity.getName().contains("test"))throw new RuntimeException("invalid username!");}


从结果发现,这次非法name又保存成功了,说明事务依然没有生效。仔细观察我们的调用方式,使用了this, 在一个方法的内部自调用了另一个带有事务注解的方法。那么,这里给出事务的第二个失效情况。
2、Transactional注解必须通过代理过的类从外部调用目标方法才能生效, 否则会事务失效。
这是因为 Spring 使用代理来实现事务,自调用会绕过代理,导致事务不生效!
修改代码

service
首先注入本类的service@AutowiredUserService userService;
然后,使用userService去调用。
// 一个公共方法供Controller调用,外部调用事务性的公有方法public int createUserWrong2(String name) {try {userService.createUserPublic(new UserEntity(name));} catch (Exception ex) {log.error("create user failed because {}", ex.getMessage());}return userRepository.findByName(name).size();}

经过调试发现,userService是由SpringCglib增强过的类,故访问的方法也是代理后的方法,具有事务的特性。

再次测试,控制台抛出了异常,数据库已经无法保存这个非法的name了。

说明事务生效了,之前执行过的save, 也在后面有执行了回滚。
这边说的外部调用,也可以是直接从另外一个类调用本来UserService类的方法!
3、事务生效后,如何捕获异常,并保证一定回滚。
有些错误的理解,认为只要有异常,事务一定会回滚,实则不然。
一般,我们写代码的时候,都这样定义事务,即@Transactional(rollbackFor = Exception.class),这个含义指的是只要方法中遇到异常,那么就执行回滚。
因为默认情况下,出现RuntimeException(非受检异常)或Error的时候,Spring才会回滚事务。如果是受检异常,Spring认为受检异常一般是业务异常,或者说
是类似另一种方法的返回值,出现这样的异常可能业务还能完成,所以不会主动回滚;而 Error或RuntimeException 代表了非预期的结果,应该回滚。

public class UserService {@Autowiredprivate UserRepository userRepository;@AutowiredUserService userService;// 异常无法传播出方法,导致事务无法回滚@Transactionalpublic void createUserWrong1(String name) {try {userRepository.save(new UserEntity(name));throw new RuntimeException("error");} catch (Exception ex) {log.error("create user failed", ex);}}@Transactionalpublic void createUserWrong2(String name) throws IOException {userRepository.save(new UserEntity(name));otherTask();}//因为文件不存在,一定会抛出一个 IOExceptionprivate void otherTask() throws IOException {Files.readAllLines(Paths.get("file-that-not-exist"));}

以上代码中的两种情况, 事务都不会回滚。第一种情况是异常没有传播出方法,是由于方法内catch了所有异常,所以异常RuntimeException无法从方法传播出去,事务自然无法回滚;第二种情况是属于受检异常,createUserWrong2能将这个受检异常传播出去,但事务看到了这种异常,默认不会进行回滚。
针对第一种情况的解决方式可以是,我们手动在catch中让事务执行回滚。运行后,debug日志中,我们会看到Transactional code has requested rollback, 这个就表示是手动回滚。
针对第二种情况,既然不能回滚受检异常,那么我们就改变这个模式,让事务遇到不再区分受检还是不受检,只要是异常,那么就进行回滚。

  @Transactional(rollbackOn = Exception.class)public void createUserWrong2(String name) throws IOException {userRepository.save(new UserEntity(name));otherTask();}

改完以后,会发现可以正常回滚。一般,建议用这个方式。@Transactional(rollbackOn = Exception.class)
总结
这个例子中,我们展现的是一个复杂的业务逻辑,其中有数据库操作、IO 操作,在 IO 操作出现问题时,希望让数据库事务也回滚,以确保逻辑的一致性。在有些业务逻辑中,可能
会包含多次数据库操作,我们不一定希望将两次操作作为一个事务来处理,这时候就需要仔细考虑事务传播的配置了,否则也可能踩坑。
3、事务传播配置是否符合自己的业务逻辑
有时,事务传播级别没有设置正确,业务逻辑和我们预期的不一致,会在排查问题的时候,一下子不容易看出来是什么地方出现的错误。

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

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

相关文章

UE4(5)逆向学习笔记(二)——寻找GWorld,GName和GUObjectArray

目录0.前言1.准备1.1 下载游戏《死寂(DeathlyStillness)》1.2 下载UE源码2.寻找GWorld3.寻找GName4.寻找GUObjectArray5.开始Dump5.结尾 0.前言 笔记(一)中我们了解了GWorld,GName和GUObjectArray是什么,也知道了想要使用UEDumper要获取到它们的偏移。 这次我们就以游戏《…

一个用于管理多个 Node.js 版本的安装和切换开源工具

大家好,今天给大家分享一个用于管理多个Node.js版本的工具 NVM(Node Version Manager),它允许开发者在同一台机器上安装和使用不同版本的Node.js,解决了版本兼容性问题,为开发者提供了极大的便利。在开发环境中,特别是在处理多个项目时,每个项目可能依赖于不同版本的 N…

2552.统计上升四元组

题目描述: 给你一个长度为 n 下标从 0 开始的整数数组 nums ,它包含 1 到 n 的所有数字,请你返回上升四元组的数目。 如果一个四元组 (i, j, k, l) 满足以下条件,我们称它是上升的: 0 <= i < j < k < l < n 且 nums[i] < nums[k] < nums[j] < num…

屏幕画笔、截图工具

水豚鼠标助手 用于做屏幕指导比较方便,鼠标换肤,屏幕画笔 https://shuitunapp.com/?from=txc //官网下载pixpin下载地址 这是一款好用的屏幕截图,贴图,gif制作 小工具 https://pixpinapp.com/

屏幕画笔和鼠标点击工具

https://shuitunapp.com/?from=txc //官网下载

disk-Linux磁盘IO性能测试方法-fio

disk-Linux磁盘IO性能测试方法-fio 测试随机写IOPS: fio -direct=1 -iodepth=128 -rw=randwrite -ioengine=libaio -bs=4k -size=10G -numjobs=1 -runtime=1000 -group_reporting -name=/path/testfile测试随机读IOPS: fio -direct=1 -iodepth=128 -rw=randread -ioengine=li…

单实例-oracle巡检模版 -20240912

单实例-oracle巡检模版 -20240912 —————————————————————————————————————————— ---- 2024年9月12日16:38:47 ---- bayaim ---- 以下内容纯属个人原创,纯属个人多年经验总结,非喜勿喷,Gun~ ——————————————————…

Yolo第Y2周:如何正确解读YOLO算法训练结果的各项指标

Yolo第Y2周:如何正确解读YOLO算法训练结果的各项指标🍨 本文为🔗IT男的一人企业中的学习记录博客 🍖 原作者:[IT男的一人企业]上一篇《详解YOLO检测算法的训练参数》讲了该如何设置参数训练。这一篇说说模型训练完,怎么看它训练的好不好。这就像开车跑长途,5个小时跑完…

微服务引擎 MSE 及云原生 API 网关 2024 年 8 月产品动态

微服务引擎 MSE 及云原生 API 网关 2024 年 8 月产品动态

MAT(Memory Analyzer Tool)下载和安装

一、下载 官网地址:https://www.eclipse.org/mat/点击download 由于要在本地电脑中运行,故选择window64位点击,进入如下页面点击DownLoad可能由于网速的原因,下载失败,故用以前的安装波,如下:解压:二、安装 点击MemoryAnalyzer.exe,就可以启动MAT工具。启动后界面如下…

pbootcms一个网站如何绑定两个域名

在PbootCMS中,一个网站绑定两个域名的方法主要涉及到在PbootCMS官网进行域名授权,并将获取的授权码填写到网站后台的相应位置。以下是详细的步骤说明: 1. 域名授权 首先,您需要登录到PbootCMS的官方网站,对需要绑定的两个域名进行授权。授权的具体流程可能因PbootCMS的版本…

PBOOTCMS网站程序提示“执行SQL发生错误!错误:DISK I/O ERROR”

当 PBOOTCMS 网站程序提示“执行SQL发生错误!错误:DISK I/O ERROR”时,这通常意味着磁盘输入输出(I/O)出现了问题,可能是由于磁盘空间不足、文件系统损坏或是磁盘硬件故障等原因导致的。以下是一些可能的解决方案: 解决方案检查磁盘空间 清理缓存文件 修复文件系统 检查…