首页 > 其他分享 >Spring的事务传播机制

Spring的事务传播机制

时间:2022-12-15 21:12:38浏览次数:54  
标签:事务 testMain Spring 调用 传播 a1 testB b1

参考资料:https://zhuanlan.zhihu.com/p/148504094

什么是事务的传播

简单的理解就是多个事务方法相互调用时,事务如何在这些方法间传播。

举个栗子,方法A是一个事务的方法,方法A执行过程中调用了方法B,那么方法B有无事务以及方法B对事务的要求不同都会对方法A的事务具体执行造成影响,同时方法A的事务对方法B的事务执行也有影响,这种影响具体是什么就由两个方法所定义的事务传播类型所决定。

Spring 事务失效的一些情况

  • @Transactional 注解只有作用到 public 方法上事务才生效。

  • 被 @Transactional 注解的方法所在的类必须被 Spring 管理。

  • 底层使用的数据库必须支持事务机制,否则不生效。

  • 若同一类中的其他没有 @Transactional 注解的方法内部调用有 @Transactional 注解的方法,有 @Transactional 注解的方法的事务会失效。

这是由于 Spring AOP 代理的原因造成的,因为只有当 @Transactional 注解的方法在类以外被调用的时候,Spring 事务管理才生效。
原因:Spring中事务管理是使用AOP代理技术实现的,目标对象自身并没有事务管理功能的,而是通过代理对象动态增强功能对事务进行增强的。因此当我们在同一个service类中通过一个方法调用另一个方法时,是通过目标对象this对象调用的,目标对象自身并没有事务管理功能,因此事务不能生效。

用代码演示下:

public class UserService{
   ...
   public User getUserByName(String name) {
      return userDao.getUserByName(name);
   }

如果配置了事务, 就相当于又创建了一个类:

public class UserServiceProxy extends UserService{
    private UserService userService;
    ...
    public User getUserByName(String name){
        User user = null;
        try{
            // 在这里开启事务
            user = userService.getUserByName(name);
            // 在这里提交事务
        }
        catch(Exception e){
            // 在这里回滚事务

            // 这块应该需要向外抛异常, 否则我们就无法获取异常信息了. 
            // 至于方法声明没有添加异常声明, 是因为覆写方法, 异常必须和父类声明的异常"兼容". 
            // 这块应该是利用的java虚拟机并不区分普通异常和运行时异常的特点.
            throw e;
        }
        return user;
    }
    ...
}

七种事务传播行为

为了帮助理解,假设有如下一个场景:

有两个方法A和B,方法A执行会在数据库ATable插入一条数据,方法B执行会在数据库BTable插入一条数据,伪代码如下:

//将传入参数a存入ATable
pubilc void A(a){
    insertIntoATable(a);    
}
//将传入参数b存入BTable
public void B(b){
    insertIntoBTable(b);
}

接下来,我们看看在如下场景下,没有事务,情况会怎样

public void testMain(){
    A(a1);  //调用A入参a1
    testB();    //调用testB
}

public void testB(){
    B(b1);  //调用B入参b1
    throw Exception;     //发生异常抛出
    B(b2);  //调用B入参b2
}

在这里要做一个重要提示:Spring中事务的默认实现使用的是AOP,也就是代理的方式,如果大家在使用代码测试时,同一个Service类中的方法相互调用需要使用注入的对象来调用,不要直接使用this.方法名来调用,this.方法名调用是对象内部方法调用,不会通过Spring代理,也就是事务不会起作用


以上伪代码描述的一个场景,方法testMain和testB都没有事务,执行testMain方法,那么结果会怎么样呢?

就是a1数据成功存入ATable表,b1数据成功存入BTable表,而在抛出异常后b2数据存储就不会执行,也就是b2数据不会存入数据库

REQUIRED(Spring默认的事务传播类型)

特点:当前方法存在事务时,子方法加入该事务。此时父子方法共用一个事务,无论父子方法哪个发生异常回滚,整个事务都回滚。即使父方法捕捉了异常,也是会回滚。而当前方法不存在事务时,子方法新建一个事务

在testMain和testB上声明事务,设置传播行为REQUIRED,伪代码如下:

@Transactional(propagation = Propagation.REQUIRED)
public void testMain(){
    A(a1);  //调用A入参a1
    testB();    //调用testB
}
@Transactional(propagation = Propagation.REQUIRED)
public void testB(){
    B(b1);  //调用B入参b1
    throw Exception;     //发生异常抛出
    B(b2);  //调用B入参b2
}

执行结果:testMain上声明了事务,在执行testB方法时就加入了testMain的事务(当前存在事务,则加入这个事务),在执行testB方法抛出异常后事务会发生回滚,又testMain和testB使用的同一个事务,所以事务回滚后testMain和testB中的操作都会回滚,也就使得数据库仍然保持初始状态


**在testMain和testB上声明事务,testMain捕获testB的异常:** ```java @Transactional(propagation = Propagation.REQUIRED) public void testMain(){ A(a1); //调用A入参a1 try{ testB(); //调用testB } catch (Exception e) { System.out.println("methodb occur exp."); }

}
@Transactional(propagation = Propagation.REQUIRED)
public void testB(){
B(b1); //调用B入参b1
throw Exception; //发生异常抛出
B(b2); //调用B入参b2
}

和上面一样的结果,A和B都没有插入,即:子事务回滚时,父事务也回滚了
<br/>
**只在testB上声明事务:**
```java
public void testMain(){
    A(a1);  //调用A入参a1
    testB();    //调用testB
}
@Transactional(propagation = Propagation.REQUIRED)
public void testB(){
    B(b1);  //调用B入参b1
    throw Exception;     //发生异常抛出
    B(b2);  //调用B入参b2
}

结果:数据a1存储成功,数据b1和b2没有存储。由于testMain没有声明事务,testB有声明事务且传播行为是REQUIRED,所以在执行testB时会自己新建一个事务(如果当前没有事务,则自己新建一个事务),testB抛出异常则只有testB中的操作发生了回滚,也就是b1的存储会发生回滚,但a1数据不会回滚,所以最终a1数据存储成功,b1和b2数据没有存储

SUPPORTS

特点:当前存在事务,则加入当前事务,如果当前没有事务,就以非事务方法执行


只在testB上声明事务,设置传播行为SUPPORTS

public void testMain(){
    A(a1);  //调用A入参a1
    testB();    //调用testB
}
@Transactional(propagation = Propagation.SUPPORTS)
public void testB(){
    B(b1);  //调用B入参b1
    throw Exception;     //发生异常抛出
    B(b2);  //调用B入参b2
}

最终结果就是: a1,b1存入数据库,b2没有存入数据库。由于testMain没有声明事务,且testB的事务传播行为是SUPPORTS,所以执行testB时就是没有事务的(如果当前没有事务,就以非事务方法执行),则在testB抛出异常时也不会发生回滚,所以最终结果就是a1和b1存储成功,b2没有存储。


那么当我们在testMain上声明事务且使用REQUIRED传播方式的时候,这个时候执行testB就满足当前存在事务,则加入当前事务,在testB抛出异常时事务就会回滚,最终结果就是a1,b1和b2都不会存储到数据库

MANDATORY

特点:当前存在事务,则加入当前事务,如果当前事务不存在,则抛出异常。


只在testB上声明事务,设置传播行为MANDATORY:

public void testMain(){
    A(a1);  //调用A入参a1
    testB();    //调用testB
}
@Transactional(propagation = Propagation.MANDATORY)
public void testB(){
    B(b1);  //调用B入参b1
    throw Exception;     //发生异常抛出
    B(b2);  //调用B入参b2
}

执行结果就是a1存储成功,而b1和b2没有存储。b1和b2没有存储,并不是事务回滚的原因,而是因为testMain方法没有声明事务,在去执行testB方法时就直接抛出事务要求的异常(如果当前事务不存在,则抛出异常),所以testB方法里的内容就没有执行。

那么如果在testMain方法进行事务声明,并且设置为REQUIRED,则执行testB时就会使用testMain已经开启的事务,遇到异常就正常的回滚了。

REQUIRES_NEW

特点:无论当前方法是否存在事务,子方法都新建一个事务。此时父子方法的事务时独立的,它们都不会相互影响。但父方法需要注意子方法抛出的异常,避免因子方法抛出异常,而导致父方法回滚

但我们不是说父子事务都是独立的,不会相互影响么?怎么结果与此相反呢?

其实是因为子方法抛出了异常,而父方法并没有做异常捕捉,此时父方法同时也抛出异常了,于是 Spring 就会将父方法事务也回滚了。如果我们在父方法中捕捉异常,那么父方法的事务就不会回滚了


给testMain声明事务,传播类型设置为REQUIRED,testB也声明事务,设置传播类型为REQUIRES_NEW:

@Transactional(propagation = Propagation.REQUIRED)
public void testMain(){
    A(a1);  //调用A入参a1
    testB();    //调用testB
    throw Exception;     //发生异常抛出
}
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void testB(){
    B(b1);  //调用B入参b1
    B(b2);  //调用B入参b2
}

执行结果就是a1没有存储,而b1和b2存储成功,因为testB的事务传播设置为REQUIRES_NEW,所以在执行testB时会开启一个新的事务,testMain中发生的异常时在testMain所开启的事务中,所以这个异常不会影响testB的事务提交,testMain中的事务会发生回滚,所以最终a1就没有存储,而b1和b2就存储成功了。

与这个场景对比的一个场景就是testMain和testB都设置为REQUIRED,那么上面的代码执行结果就是所有数据都不会存储,因为testMain和testMain是在同一个事务下的,所以事务发生回滚时,所有的数据都会回滚

NOT_SUPPORTED

特点:始终以非事务方式执行,如果当前存在事务,则挂起当前事务

可以理解为设置事务传播类型为NOT_SUPPORTED的方法,在执行时,不论当前是否存在事务,都会以非事务的方式运行。


@Transactional(propagation = Propagation.REQUIRED)
public void testMain(){
    A(a1);  //调用A入参a1
    testB();    //调用testB
}
@Transactional(propagation = Propagation.NOT_SUPPORTED)
public void testB(){
    B(b1);  //调用B入参b1
    throw Exception;     //发生异常抛出
    B(b2);  //调用B入参b2
}

执行结果就是a1和b2没有存储,而b1存储成功。testMain有事务,而testB不使用事务,所以执行中testB的存储b1成功,然后抛出异常,此时testMain检测到异常事务发生回滚,但是由于testB不在事务中,所以只有testMain的存储a1发生了回滚,最终只有b1存储成功,而a1和b1都没有存储

NEVER

特点:不使用事务,如果当前事务存在,则抛出异常

很容易理解,就是我这个方法不使用事务,并且调用我的方法也不允许有事务,如果调用我的方法有事务则我直接抛出异常。


@Transactional(propagation = Propagation.REQUIRED)
public void testMain(){
    A(a1);  //调用A入参a1
    testB();    //调用testB
}
@Transactional(propagation = Propagation.NEVER)
public void testB(){
    B(b1);  //调用B入参b1
    B(b2);  //调用B入参b2
}

执行直接抛出事务异常,且不会有数据存储到数据库。由于testMain事务传播类型为REQUIRED,所以testMain是运行在事务中,而testB事务传播类型为NEVER,所以testB不会执行而是直接抛出事务异常,此时testMain检测到异常就发生了回滚,所以最终数据库不会有数据存入。

NESTED

特点:与 REQUIRED 非常相似,其特性是:当前方法存在事务时,子方法加入在嵌套事务执行。当父方法事务回滚时,子方法事务也跟着回滚。当子方法事务发送回滚时,父事务是否回滚取决于是否捕捉了异常。如果捕捉了异常,那么就不回滚,否则回滚。

NESTED 与 REQUIRED 的区别在于:父方法与子方法对于共用事务的描述是不一样的,REQUIRED 说的是共用同一个事务,而 NESTED 说的是在嵌套事务执行。这一个区别的具体体现是:在子方法事务发生异常回滚时,父方法有着不同的反应动作。

对于 REQUIRED 来说,无论父子方法哪个发生异常,全都会回滚。而 REQUIRED 则是:父方法发生异常回滚时,子方法事务会回滚。而子方法事务发送回滚时,父事务是否回滚取决于是否捕捉了异常。


@Transactional(propagation = Propagation.REQUIRED)
public void testMain(){
    A(a1);  //调用A入参a1
    testB();    //调用testB
    throw Exception;     //发生异常抛出
}
@Transactional(propagation = Propagation.NESTED)
public void testB(){
    B(b1);  //调用B入参b1
    B(b2);  //调用B入参b2
}

所有数据都不会存入数据库,因为在testMain发生异常时,父事务回滚则子事务也跟着回滚了


@Transactional(propagation = Propagation.REQUIRED)
public void testMain(){
    A(a1);  //调用A入参a1
    try{
        testB();    //调用testB
    }catch(Exception e){

    }
    A(a2);
}
@Transactional(propagation = Propagation.NESTED)
public void testB(){
    B(b1);  //调用B入参b1
    throw Exception;     //发生异常抛出
    B(b2);  //调用B入参b2
}

这种场景下,结果是a1,a2存储成功,b1和b2存储失败,因为调用方catch了被调方的异常,所以只有子事务回滚了。

标签:事务,testMain,Spring,调用,传播,a1,testB,b1
From: https://www.cnblogs.com/d111991/p/16985830.html

相关文章

  • 分布式事务详解
    XA事务这个唯一一个强一致的事务,效率最低,全局事务执行过程中,任意子事务可提交阶段都只能等待,一直到所有子事务都走到这个节点才能一起提交。因为没有单独的提交,可见......
  • Spring Security 安全框架入门原理及实战
    SpringSecurity入门原理及实战在web应用开发中,安全无疑是十分重要的,选择SpringSecurity来保护web应用是一个非常好的选择。SpringSecurity是spring项目之中的一个安全......
  • 破记录!国产数据库KunDB 单节点TPC-C事务性能超180万tpmC
    近日,星环科技KunDB在TPC-C事务性能测试中,采用常规国产服务器,实现了单节点tpmC超180万,体现其世界级领先的事务处理能力。TPC-C是全球OLTP数据库最权威的性能测试基准,由TP......
  • springMVC07(REST风格)
    一、REST风格的解释:(资源的访问形式)二、总结:2.1-REST:资源访问形式2.2-4个动作(GET、POST、PUT、DELETE)2.3-用"REST"风格开发,我们就叫"RESTful"......
  • springboot启动流程
    主要看下newSpringApplication逻辑和run方法逻辑:newSpringApplication逻辑:进入run方法后,会new一个SpringApplication对象,创建这个对象的构造函数做了一些准备工作,......
  • 随笔(八)『SpringBoot 解决ID和日期,前端显示不一致』
    packagecom.baihua.common.config;importcom.fasterxml.jackson.databind.DeserializationFeature;importcom.fasterxml.jackson.databind.ObjectMapper;importcom......
  • 狂神说 spring5
    Spring51.1、简介Spring:春----->给软件行业带来了春天!2002,首次推出了Spring框架的雏形:interface21框架!Spring框架即以interface21框架为基础,经过重新设计,并不......
  • springMvc23-配置maven环境和创建maven项目(建议收藏,超全超详细)
    1本次歌谣就对如何创建一个maven项目做一个详细的讲解,毕竟卡了我三天,久久不能入眠,也搜了网上很多的博客都没有顺利的解决maven项目的创建。这篇建议大家收藏,总会用到的。不......
  • spring下的restTemplate使用
    首先上配置,由于restTemplate不支持一些返回格式,所以需要自己手动配置/***@Description:restTemplate配置类*@Author:wzkris*@Version:V1.0.0*@Date:......
  • Java:SpringBoot使用EasyExcel实现Excel文件的导出下载和上传导入功能
    SpringBoot使用EasyExcel实现Excel文件的导出下载和上传导入功能文件目录$tree-Itarget.├──README.md├──pom.xml└──src└──main├─......