首页 > 其他分享 >Spring中事务嵌套这么用一定得注意了!!

Spring中事务嵌套这么用一定得注意了!!

时间:2023-04-22 09:12:55浏览次数:28  
标签:事务 transaction1 transaction2 嵌套 Spring PROPAGATION 当前 catch

前言

最近项目上有一个使用事务相对复杂的业务场景报错了。在绝大多数情况下,都是风平浪静,没有问题。其实内在暗流涌动,在有些异常情况下就会报错,这种偶然性的问题很有可能就会在暴露到生产上造成事故,那究竟是怎么回事呢?

问题描述

我们用一个简单的例子模拟下,大家也可以看看下面这段代码输出的结果是什么。

  1. 在类SecondTransactionService定义一个简单接口transaction2,插入一个用户,同时必然会抛出错误
@Override
@Transactional(rollbackFor = Exception.class)
public void transaction2() {
    System.out.println("do transaction2.....");
    User user = new User("tx2", "111", 18);
    // 插入一个用户
    userService.insertUser(user);
    // 跑错了
    throw new RuntimeException();
}
  1. 在另外一个类FirstTransactionService定义一个接口transaction1,它调用transaction2方法,同时做了try catch处理
@Override
@Transactional(rollbackFor = Exception.class)
public void transaction1() {
    System.out.println("do transaction1 .......");
    try {
        // 调用另外一个事务,try catch住
        secondTransactionService.transaction2();
    } catch (Exception e) {
        e.printStackTrace();
    }

    // 插入当前用户tx1
    User user = new User("tx1", "111", 18);
    userService.insertUser(user);
}
  1. 定义一个controller,调用transaction1方法
@GetMapping("/testNestedTx")
public String testNestedTx() {
    firstTransactionService.transaction1();
    return "success";
}

大家觉得调用这个http接口,最终数据库插入的是几条数据呢?

问题结果

正确答案是数据库插入了0条数据。

同时控制台也报错了,报错原因是:org.springframework.transaction.UnexpectedRollbackException: Transaction rolled back because it has been marked as rollback-only

是否和你预想的一样呢?你知道是为什么吗?

原因追溯

其实原因很简单,我们都知道,一个事务要么全成功提交事务,要么失败全部回滚。如果出现在一个事务中部分SQL要回滚,部分SQL要提交,这不就主打的一个”前后矛盾,精神分裂“吗?

controller.testNestedTx() 
  || 
  / 
FirstTransactionService.transaction1()   REQUIRED隔离级别
       || 
       || 
       || 捕获异常,提交事务,出错啦
       / || 
FirstTransactionService.transaction2()   REQUIRED隔离级别
       || || 
       || 抛出异常,标记事务为rollback only
       =======================
  1. 事务的隔离级别为REQUIRED,那么发现没有事务开启一个事务操作,有的话,就合并到这个事务中,所以transaction1()transaction2()是在同一个事务中。
  2. transaction2()抛出异常,那么事务会被标记为rollback only, 源码如下所示:

  1. transaction1()由于try catch 异常,正常运行,想必就要可以提交事务了,在提交事务的时候,会检查rollback标记,如果是true, 这时候就会抛出上面的异常了。源码如下图所示:

这下,是不是很清楚知道报错的原因了,那想想该怎么处理呢?

解决之道

知道了根本原因之后,是不是解决的方案就很明朗了,我们可以通过调整事务的传播方式分拆多个事务管理,或者让一个事务"前后一致",做一个诚信的好事务。

  • try catch放到内层事务中,也就是transaction2()方法中,这样内层事务会跟着外部事务进行提交或者回滚。
@Override
    @Transactional(rollbackFor = Exception.class)
    public void transaction2() {
        try {
            System.out.println("do transaction2.....");
            User user = new User("tx2", "111", 18);
            userService.insertUser2(user);
            throw new RuntimeException();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
  • 如果希望内层事务抛出异常时中断程序执行,直接在外层事务的catch代码块中抛出e,这样同一个事务就都会回滚。
  • 如果希望内层事务回滚,但不影响外层事务提交,需要将内层事务的传播方式指定为PROPAGATION_NESTEDPROPAGATION_NESTED基于数据库savepoint实现的嵌套事务,外层事务的提交和回滚能够控制嵌内层事务,而内层事务报错时,可以返回原始savepoint,外层事务可以继续提交。

事务的传播机制

前面提到了事务的传播机制,我们再看都有哪几种。

  • PROPAGATION_REQUIRED:加入到当前事务中,如果当前没有事务,就新建一个事务。这是最常见的选择,也是Spring中默认采用的方式。
  • PROPAGATION_SUPPORTS:支持当前事务,如果当前没有事务,就以非事务方式执行。
  • PROPAGATION_MANDATORY :支持当前事务,如果当前没有事务,就抛出异常。
  • PROPAGATION_REQUIRES_NEW:新建一个事务,如果当前存在事务,把当前事务挂起。
  • PROPAGATION_NOT_SUPPORTED :以非事务方式执行操作,如果当前存在事务,就把当前事务挂起。
  • PROPAGATION_NEVER: 以非事务方式执行,如果当前存在事务,则抛出异常。
  • PROPAGATION_NESTED :如果当前存在事务,则在嵌套事务内执行。如果当前没有事务,则进行与PROPAGATION_REQUIRED类似的操作。

如何理解PROPAGATION_NESTED的传播机制呢,和PROPAGATION_REQUIRES_NEW又有什么区别呢?我们用一个例子说明白。

  • 定义serviceA.methodA()PROPAGATION_REQUIRED修饰;
  • 定义serviceB.methodB()以表格中三种方式修饰;
  • methodA中调用methodB;

总结

在我的项目中之所以会报“rollback-only”异常的根本原因是代码风格不一致的原因。外层事务对错误的处理方式是返回true或false来告诉上游执行结果,而内层事务是通过抛出异常来告诉上游(这里指外层事务)执行结果,这种差异就导致了“rollback-only”异常。大家也可以去review自己项目中的代码,是不是也偷偷犯下同样的错误了。

欢迎关注个人公众号【JAVA旭阳】交流学习

标签:事务,transaction1,transaction2,嵌套,Spring,PROPAGATION,当前,catch
From: https://www.cnblogs.com/alvinscript/p/17342419.html

相关文章

  • SpringSecurity课程介绍、SpringSecurity课程简介
    课程介绍课程简介 SpringSecurity是Spring家族中的一个安全管理框架。相比与另外一个安全框架Shiro,它提供了更丰富的功能,社区资源也比Shiro丰富。一般来说中大型的项目都是使用SpringSecurity来做安全框架。小项目有Shiro的比较多,因为相比与SpringSecurity,Shiro......
  • springboot+bootstraptable
    springboot+bootstraptable项目采用的是springboot+bootstraptable搭建的demo  https://blog.csdn.net/weixin_43373818/article/details/114714016基础的增删改查已经实现html页面<!DOCTYPEhtml><htmllang="zh-CN"xmlns:th="http://www.thymeleaf.org"><......
  • Springboot 使用nacos鉴权的简单步骤
    Springboot使用nacos鉴权的简单步骤背景前端时间nacos爆出了漏洞.因为他的默认token固定,容易被利用.具体的问题为:QVD-2023-6271漏洞描述:开源服务管理平台Nacos中存在身份认证绕过漏洞,在默认配置下未token.secret.key进行修改,导致远程攻击者可以绕过密钥认证进入......
  • redis springboot
    【springboot进阶】SpringBoot整合RedisTemplate配置多个redis库RedisTemplate及4种序列化方式  springboot笔记 ......
  • SpringDataRedis的序列化方式和StringRedisTemplate手动序列化详解
    一.SpringDataRedis之前新创建一个Spring项目,在进行配置完成redis和common-pool依赖:1.引入依赖redis:<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis-reactive</artifactId></dependency>......
  • Spring Data Jpa 接口方法名
    SpringDataJpa系列教程(一)--------入门 现在开始,我们来学习船新的一门技术,SpringDataJpa,它是一个。。额。。(我百度一下) JPA(JavaPersistenceAPI)是Sun官方提出的Java持久化规范。它为Java开发人员提供了一种对象/关联映射工具来管理Java应用中的关系数据。他的出现主要是为了......
  • 【Spring】静态方法(工具类)中调用Spring管理的Bean
    背景在一些业务开发,经常会写一些工具类,但这些工具类时常需要调用到Spring管理的bean,这些Spring管理的bean注入,平常用的都是@Autowired注解一个成员变量,问题就来了:(1)成员变量(即Spring管理的bean)是非静态的,但工具类都是想写静态方法,静态方法不能引入一个非静态的变量。所以......
  • SpringSecurity
     https://www.cnblogs.com/SjhCode/p/SpringSecurity.htmlpermitAll() :无条件允许任何形式访问,不管你登录还是没有登录。anonymous() :允许匿名访问,也就是没有登录才可以访问。denyAll() :无条件决绝任何形式的访问。authenticated():只允许已认证的用户访问。fullyAuthe......
  • 部署Spring Boot + React前后端分离中的奇特发现
    今天在尝试在服务器上部署SpringBoot+React前后端分离应用时有一个奇特的发现。服务器首先是前端npmrunbuild之后,将构建好的应用移动到/var/www/html文件夹,nginx启动可以正常访问,axios接口访问地址为localhost:8080;后端maven构建并运行。本地电脑同样开始运行前后端。......
  • springboot定时同步数据,从sqlserver到mysql
    https://www.cnblogs.com/SjhCode/p/sqlserverToMysql.html定时同步数据,从sqlserver到mysql 注意事项:一.primary:master #设置默认的数据源或者数据源组,默认值即为master二.@Scheduled()和 @DS("slave_1")注解 步骤:1.在原先运行的程序外,新建多加一个springboot程序......