掌握 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 {
    @Autowired
    private AdminRepository adminRepository;
    
    @Transactional
    protected 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 {

    @Autowired
    private 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();
    }


    @Transactional
    public 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 方法中,可以找到处理事务的逻辑,可以看到只有捕获到异常才能进行后续事务处理。

在这里插入图片描述

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

 @Transactional
 public 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 {

  @Autowired
  private AdminRepository adminRepository;

  @Autowired
  private AddressService addressService;

  @Transactional
  public 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 {

    @Autowired
    private AddressRepository addressRepository;

    @Transactional
    public 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_DEFAULT

2024-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-only

2024-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 {

    @Autowired
    private 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_DEFAULT

2024-11-22 19:13:56.734  INFO 9201 --- [nio-8080-exec-1] com.starter.demo.service.AdminService    : save admin success

2024-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 rollback

2024-11-22 19:13:56.833 DEBUG 9201 --- [nio-8080-exec-1] o.s.orm.jpa.JpaTransactionManager        : Resuming suspended transaction after completion of inner transaction

2024-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. 主事务提交了,没有看到静默回滚的异常。

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

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

相关文章

Docker Seata分布式事务保护搭建 DB数据源版搭建 结合Nacos服务注册

介绍 Seata&#xff08;Simple Extensible Autonomous Transaction Architecture&#xff09;是一个开源的分布式事务解决方案&#xff0c;旨在为微服务架构中的分布式系统提供事务管理支持。Seata 通过提供全局事务管理&#xff0c;帮助开发者在分布式环境中保持数据一致性 …

【设计模式系列】责任链模式(十六)

一、什么是责任链模式 责任链模式&#xff08;Chain of Responsibility Pattern&#xff09;是一种行为型设计模式。其核心思想是将请求的发送者和接收者解耦&#xff0c;通过一个中介链来传递请求&#xff0c;使得多个对象都有可能接收请求&#xff0c;从而避免请求发送者和接…

实时数据研发 | Flink技术栈

下周要开始接触一些实时的内容了&#xff0c;想来是很幸运的&#xff0c;这是我在新人培训上提问过技术前辈的问题&#xff1a;“想学习实时相关技术&#xff0c;但是部门没有类似的需求&#xff0c;应该如何提升&#xff1f;”当时师姐说先用心去学&#xff0c;然后向主管证明…

python对tif数据重投影

一、不同投影坐标系的区别 地理坐标系&#xff08;Geographic Coordinate System, GCS&#xff09;和投影坐标系&#xff08;Projected Coordinate System, PCS&#xff09;是两种常见的坐标系统&#xff0c;它们在表示地理信息时有着不同的方式。以下是它们的主要区别&#x…

Django+Nginx+uwsgi网站使用Channels+redis+daphne实现简单的多人在线聊天及消息存储功能

网站部署在华为云服务器上&#xff0c;Debian系统&#xff0c;使用DjangoNginxuwsgi搭建。最终效果如下图所示。 一、响应逻辑顺序 1. 聊天页面请求 客户端请求/chat/&#xff08;输入聊天室房间号界面&#xff09;和/chat/room_name&#xff08;某个聊天室页面&#xff09;链…

多目标粒子群优化(Multi-Objective Particle Swarm Optimization, MOPSO)算法

概述 多目标粒子群优化&#xff08;MOPSO&#xff09; 是粒子群优化&#xff08;PSO&#xff09;的一种扩展&#xff0c;用于解决具有多个目标函数的优化问题。MOPSO的目标是找到一组非支配解&#xff08;Pareto最优解&#xff09;&#xff0c;这些解在不同目标之间达到平衡。…

oracle会话追踪

一 跟踪当前会话 1.1 查看当前会话的SID,SERIAL# #在当前会话里执行 示例&#xff1a; SQL> select distinct userenv(sid) from v$mystat; USERENV(SID) -------------- 1945 SQL> select distinct sid,serial# from v$session where sid1945; SID SERIAL# …

python 画图例子

目录 多组折线图点坐标的折线图 多组折线图 数据: 第1行为x轴标签第2/3/…行等为数据,其中第一列为标签&#xff0c;后面为y值 图片: 代码: import matplotlib.pyplot as plt# 原始数据字符串 # 第1行为x轴标签 # 第2/3/...行等为数据,其中第一列为标签&#xff0c;后面…

未来已来:少儿编程竞赛聚焦物联网,激发创新潜力

随着人工智能与物联网技术&#xff08;IoT&#xff09;的快速发展&#xff0c;少儿编程教育正在迎来新的变革浪潮。近年来&#xff0c;各类少儿编程竞赛纷纷增加了物联网相关主题&#xff0c;要求学生结合编程知识和硬件设备设计智能家居、智慧城市等创新项目。这一趋势不仅丰富…

Java-08 深入浅出 MyBatis - 多对多模型 SqlMapConfig 与 Mapper 详细讲解测试

点一下关注吧&#xff01;&#xff01;&#xff01;非常感谢&#xff01;&#xff01;持续更新&#xff01;&#xff01;&#xff01; 大数据篇正在更新&#xff01;https://blog.csdn.net/w776341482/category_12713819.html 目前已经更新到了&#xff1a; MyBatis&#xff…

字符串专题 算法小题

感觉很久不做题了, 本身自己虽然就没水平就是啦哈哈~ 那下面分享几道最近写的几道题, 都很简单, 是关于"字符串"的, 只不过会稍微用到一点代码能力就是了, 算是比较基础的题目. 目录 1.最长公共区域(⭐⭐⭐ 代码)1.1 题目描述1.2 题目思路方法1: 两两求公共区域方法2…

虚拟化的三种方式

1.前言 Virtualization(虚拟化)是让公开的虚拟资源等同于被虚拟化的底层物理资源。虚拟化在各个领域应用很广泛&#xff0c;不局限于计算机科学领域。无论是在硬件、软件还是在嵌入式子系统中&#xff0c;虚拟化总是使用或组合三种简单的技术来实现的&#xff1a;多路复用(Mul…

使用yolov5查看模式标注情况

import cv2 from ultralytics import YOLO# 加载模型 model YOLO(E:\\yolov\\yolov9\\runs\\detect\\train4\\weights\\best.pt) # 替换为您的模型路径# 读取视频文件 cap cv2.VideoCapture(5.mp4) # 替换为您的视频文件路径# 定义输出视频的编码器和创建VideoWriter对象 f…

Rust 力扣 - 198. 打家劫舍

文章目录 题目描述题解思路题解代码题目链接 题目描述 题解思路 假设f(i)表示在[1, i]号内的房屋内进行偷盗能够获得的最高金额 存在递推公式 f(i) max(f(i - 1), f(i - 2) nums[i]) 即f(i)为选择i - 1号房屋的最大金额 和 选择i - 2号房屋的最大金额 的最大值 题解代码 …

Redis持久化、主从及哨兵架构详解

Redis持久化 RDB快照&#xff08;snapshot&#xff09; 在默认情况下&#xff0c;Redis将内存数据库快照保存在名字为dump.rdb的二进制文件中。 你可以对Redis进行设置&#xff0c;让它在“N秒内数据集至少有M个改动”这一条件被满足时&#xff0c;自动保存一次数据集。 比…

解决启动Tomcat时出现的乱码问题

日志乱码 日志乱码就是启动Tomcat时红色的字体出现乱码&#xff08;下图没有乱码&#xff09;。 解决方案 &#xff1a; 找到Tomcat的安装目录&#xff0c;点进conf目录 点进logging.properties文件 找到java.util.logging.ConsoleHandler.encoding字段&#xff0c;修改成GBK…

网络爬虫——常见问题与调试技巧

在开发网络爬虫的过程中&#xff0c;开发者常常会遇到各种问题&#xff0c;例如网页加载失败、数据提取错误、反爬机制限制等。以下内容将结合实际经验和技术方案&#xff0c;详细介绍解决常见错误的方法&#xff0c;以及如何高效调试和优化爬虫代码。 1. 爬虫过程中常见的错误…

初识Linux(3):Linux基础环境工具(上)

目录 1. yum 1.1 软件的生态 1.2 yum使用 2. vim 4. vim三种模式的更详细命令 5. gcc 6. 重要概念&#xff1a;函数库 7. 动态库与静态库 8. 自动化构建工具&#xff1a;make/Makefile .PHONY 9. make 与 是否执行 %通识符 生成两个可执行程序 10.练习 &#xff…

负载均衡在线OJ项目

OnlineJudge 前言所用技术开发环境 1. 需求分析2. 项目宏观结构3. compile_server服务设计3.1 compiler服务设计3.2 runner服务设计3.3 compile_run3.4 compile_server.cpp 4. oj_server服务设计4.1 model设计4.2 view设计4.3 control设计4.3.1 获取题目列表功能4.3.2 获取单个…

Kafka 分区分配及再平衡策略深度解析与消费者事务和数据积压的简单介绍

Kafka&#xff1a;分布式消息系统的核心原理与安装部署-CSDN博客 自定义 Kafka 脚本 kf-use.sh 的解析与功能与应用示例-CSDN博客 Kafka 生产者全面解析&#xff1a;从基础原理到高级实践-CSDN博客 Kafka 生产者优化与数据处理经验-CSDN博客 Kafka 工作流程解析&#xff1a…