参考资料: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