对于本地事务的概念,在这里就不做过多的解释了,本篇文章主要是实例讲解本地事务的传播机制。
对事务没有多少接触的人来说,可能对事务的传播机制理解的不那么透彻,下面通过4个表,2个事务方法,来实例验证一下事务的传播
声明式事务设置
@Transactional(propagation=Propagation.REQUIRED)
事务的特性
- 原子性(Atomicity):事务是一个原子操作,由一系列动作组成。事务的原子性确保动作要么全部完成,要么完全不起作用。
- 一致性(Consistency):一旦事务完成(不管成功还是失败),系统必须确保它所建模的业务处于一致的状态,而不会是部分完成部分失败。在现实中的数据不应该被破坏。
- 隔离性(Isolation):可能有许多事务会同时处理相同的数据,因此每个事务都应该与其他事务隔离开来,防止数据损坏。
- 持久性(Durability):一旦事务完成,无论发生什么系统错误,它的结果都不应该受到影响,这样就能从任何系统崩溃中恢复过来。通常情况下,事务的结果被写到持久化存储器中。
事务七大传播机制
事务的传播性一般用在事务嵌套的场景,比如一个事务方法里面调用了另外一个事务方法,那么两个方法是各自作为独立的方法提交还是内层的事务合并到外层的事务一起提交,这就是需要事务传播机制的配置来确定怎么样执行。
先来总体看一下传播机制,下面用代码实验各个传播机制:
- PROPAGATION_REQUIRED
Spring默认的传播机制,能满足绝大部分业务需求,如果外层有事务,则当前事务加入到外层事务,一块提交,一块回滚。如果外层没有事务,新建一个事务执行
- PROPAGATION_REQUES_NEW
该事务传播机制是每次都会新开启一个事务,同时把外层事务挂起,当当前事务执行完毕,恢复上层事务的执行。如果外层没有事务,执行当前新开启的事务即可
- PROPAGATION_SUPPORT
如果外层有事务,则加入外层事务,如果外层没有事务,则直接使用非事务方式执行。完全依赖外层的事务
- PROPAGATION_NOT_SUPPORT
该传播机制不支持事务,如果外层存在事务则挂起,执行完当前代码,则恢复外层事务,无论是否异常都不会回滚当前的代码
- PROPAGATION_NEVER
该传播机制不支持外层事务,即如果外层有事务就抛出异常
- PROPAGATION_MANDATORY
与NEVER相反,如果外层没有事务,则抛出异常
- PROPAGATION_NESTED
该传播机制的特点是可以保存状态保存点,当前事务回滚到某一个点,从而避免所有的嵌套事务都回滚,即各自回滚各自的,如果子事务没有把异常吃掉,基本还是会引起全部回滚的。
传播规则回答了这样一个问题:一个新的事务应该被启动还是被挂起,或者是一个方法是否应该在事务性上下文中运行。
场景介绍
上面已经介绍过了,事务的传播其实就是一个事务方法里面调用了另外一个事务方法。
在springboot里,一般分为controller层,service层,dao层。我们的逻辑处理,一般都是在service层。
所以事务一般情况下都会加在service层里,当然也会有在controller层的情况。springboot用声明式事务@Transactional注解来实现事务,可以加在类上,也可以加在方法上。加在类上,就等同于类内部的所有方法,都加入了@Transactional注解。一般情况下是加在方法上。
这里注意一点,在同一个service中,2个方法互相调用,是无法触发事务的。比如说,A类里有方法1和方法2,他们都加入了@Transactional注解,方法1里面直接调用方法2,这样是无法使事务生效的,因为Spring中事务管理是使用AOP代理技术实现的,目标对象自身并没有事务管理功能的,而是通过代理对象动态增强功能对事务进行增强的。因此当我们在同一个service类中通过一个方法调用另一个方法时,是通过目标对象this对象调用的,目标对象自身并没有事务管理功能,因此事务不能生效。
所以这种操作的话,需要做特殊处理,才可以使之生效。具体方法见
Spring中同一个service中方法相互调用事务不生效问题解决方案 还有很多种@Transactional不生效的情况,不一一介绍了,遇到什么问题解决什么问题吧,大部分问题都可以百度到。这里推荐一篇文章Spring事务不生效的原因大解读
下面进行事务传播的一个实验。
首先准备4个表,4个表里都只有一个字段,我们想做的是,对这4张表进行insert操作,注意我的表名含义,方法1_表1…方法1_表2…第一service里的方法,操作Method1_table1和Method1_table2,第二个service里的方法操作Method2_table1和Method2_table2,都只做一个添加操作。然后service1嵌套service2,这就形成了一个事务方法调用另一个事务方法的场景。懂了吧,有点乱,但是肯定能看懂吧?
表:
首先启动类加入@EnableTransactionManagement注解开启事务
下面创建好各自的pojo实体类和dao层,我用的是mybatis-plus。
然后service直接操作、
下面开始按照每一种传播机制,从代码上入手,每一种情况看一下是如何传播
PROPAGATION_REQUIRED(默认)
这种传播机制的意思是,如果外层有事务,则当前事务加入到外层事务,一块提交,一块回滚。如果外层没有事务,新建一个事务执行,这是默认机制,就是@Transactional后面啥都不加,就是默认这种机制,下面先来看一下这种传播机制实现的场景和运行结果
1. 内层方法抛出异常,外层方法正常执行
service1代码
@Service
public class Service1Impl implements IService1 {
@Resource
private Method1Table1Mapper method1Table1Mapper;
@Resource
private Method1Table2Mapper method1Table2Mapper;
@Autowired
private IService2 iService2;
@Override
@Transactional
public int insert1() throws RuntimeException {
//本方法里面的事务==============================
//表1操作
Method1Tabale1 method1Tabale1 = new Method1Tabale1();
method1Tabale1.setMethod1_tabale1("1");
method1Table1Mapper.insert(method1Tabale1);
//表2操作
Method1Tabale2 method1Tabale2 = new Method1Tabale2();
method1Tabale2.setMethod1_tabale2("2");
method1Table2Mapper.insert(method1Tabale2);
//================================================
//嵌套进第二个事务方法==============================
iService2.insert2();
return 0;
}
}
service2代码
@Service
public class Service2Impl implements IService2 {
@Resource
private Method2Table1Mapper method2Table1Mapper;
@Resource
private Method2Table2Mapper method2Table2Mapper;
@Override
@Transactional
public int insert2() throws RuntimeException {
//表1操作
Method2Tabale1 method2Tabale1 = new Method2Tabale1();
method2Tabale1.setMethod2_tabale1("1");
method2Table1Mapper.insert(method2Tabale1);
//表2操作
Method2Tabale2 method2Tabale2 = new Method2Tabale2();
method2Tabale2.setMethod2_tabale2("2");
method2Table2Mapper.insert(method2Tabale2);
//抛出一个异常
if(1==1){
throw new RuntimeException();
}
return 0;
}
}
serve1方法做2个表的添加操作,service2方法做2个表的添加操作,
- serve1的方法,嵌套进service2的方法
- 2个方法都加入@Transactional注解,开启事务
- service2抛出一个异常
service1方法正常执行,service2方法出现异常,service2方法肯定是回滚了,那么service1方法,会不会回滚呢?执行一下,我们看一下日志
图点击放大,注意看日志。日志已经执行了4个insert语句,但是遇到异常,产生了回滚,下面那3句翻译过来就是: 释放事务性SqlSession,事务同步取消注册SqlSession。
我们再看一下数据库,4张表,都没有数据,都是空。
(加入外层事务,一块提交,一块回滚)
2. 外层方法抛出异常,内层方法正常执行
下面换一种情况,让service1方法抛出异常,service2方法正常执行,看一下运行情况。
(这里代码就不贴了,大家都知道怎么操作)
和上面一样,回滚了,数据库中,4张表,依然全是空。
(加入外层事务,一块提交,一块回滚)
3. 外层方法删掉事务注解,内层方法抛出异常
然后第三种情况,service1方法删掉事务注解,service2依然是一个事务,但是抛出异常,也就是所说的外层没有事务,看一下运行情况
依然出现了回滚,但是去看数据库,service1方法控制的2个表,已经成功的加入了数据,service2控制的2个表没有加入数据。
(如果外层没有事务,内层自己新建一个事务执行)
结论
经过实验,印证了概念所说的,如果外层有事务,则当前事务加入到外层事务,一块提交,一块回滚。如果外层没有事务,新建一个事务执行
PROPAGATION_REQUES_NEW
这种传播机制的意思是每次都会新开启一个事务,同时把外层事务挂起,当当前事务执行完毕,恢复上层事务的执行。如果外层没有事务,执行当前新开启的事务即可
我们只给内层方法的事务注解加入 propagation = Propagation.REQUIRES_NEW,这个例子中外层事务没必要加。
1. 内层方法抛出异常,外层方法正常执行
Service1
@Service
public class Service1Impl implements IService1 {
@Resource
private Method1Table1Mapper method1Table1Mapper;
@Resource
private Method1Table2Mapper method1Table2Mapper;
@Autowired
private IService2 iService2;
@Override
@Transactional
public int insert1() {
//本方法里面的事务==============================
//表1操作
Method1Table1 method1Tabale1 = new Method1Table1();
method1Tabale1.setMethod1_tabale1("1");
method1Table1Mapper.insert(method1Tabale1);
//表2操作
Method1Table2 method1Tabale2 = new Method1Table2();
method1Tabale2.setMethod1_tabale2("2");
method1Table2Mapper.insert(method1Tabale2);
//================================================
//嵌套进第二个事务方法==============================
iService2.insert2();
return 0;
}
}
Service2
@Service
public class Service2Impl implements IService2 {
@Resource
private Method2Table1Mapper method2Table1Mapper;
@Resource
private Method2Table2Mapper method2Table2Mapper;
@Override
@Transactional(propagation = Propagation.REQUIRES_NEW)
public int insert2() throws RuntimeException {
//表1操作
Method2Table1 method2Tabale1 = new Method2Table1();
method2Tabale1.setMethod2_tabale1("1");
method2Table1Mapper.insert(method2Tabale1);
//表2操作
Method2Table2 method2Tabale2 = new Method2Table2();
method2Tabale2.setMethod2_tabale2("2");
method2Table2Mapper.insert(method2Tabale2);
if(1==1){
throw new RuntimeException("抛出一个运行时异常");
}
return 0;
}
}
让我们来执行一哈,看一哈结果
日志就不截图了,指定是回滚了。看一下数据库。
数据中4个表都是空的,都没有加进去。
2. 外层方法抛出异常,内层方法正常执行
改下代码,外层抛异常,内层正常执行,看一下结果。
直接看数据库,结果就是,外层操作的2个表没加进去,内层方法都加进去了。
3.外层方法删掉事务注解,内层方法抛出异常
改下代码,外层方法删掉事务的注解,内层的事务方法抛出异常。
结果显而易见,外层方法没加事务,肯定是加进去了,内层事务方法抛出异常肯定是回滚了。
结论
内层事务方法的回滚会影响到外层事务方法一起回滚
外层事务的回滚不会影响到内层事务,内层事务回滚 / 提交完全看自己,而外层事务的回滚提交不仅要看自己也看内层事务
印证了概念上的,先将外层事务挂起,内层事务执行完毕以后,再执行外层事务
PROPAGATION_SUPPORT
这种传播机制的意思是如果外层有事务,则加入外层事务,如果外层没有事务,则直接使用非事务方式执行。完全依赖外层的事务
(代码就不上了,还是上面的2个service,只是按照场景稍作改动)
1. 内层方法抛出异常,外层方法正常执行
直接看数据库中执行结果
4个表都没有加入数据,一起做了回滚
2.外层方法抛出异常,内层方法正常执行
直接看数据库中执行结果,结果显而易见,完全依赖外层事务,外层回滚,肯定都回滚
4个表都没有加入数据,一起做了回滚
3. 外层方法删掉事务注解,内层方法抛出异常
既然是完全依赖于外层事务,现在外层的事务去掉了,看一下结果
4个表都成功加入数据,没有回滚,也就是说,外层有事务,就执行事务,外层没事务,就当做非事务去执行
结论
印证概念说的,完全依赖于外层,外层有事务,就跟着外层一起提交/回滚,外层没有事务,就当做一个非事务的方法执行
PROPAGATION_NOT_SUPPORT
这个传播机制的意思是该传播机制不支持事务,如果外层存在事务则挂起,执行完当前代码,则恢复外层事务,无论是否异常都不会回滚当前的代码
(代码就不上了,还是上面的2个service,只是按照场景稍作改动)
1. 内层方法抛出异常,外层方法正常执行
直接看数据库结果
service1外层方法操作的2个表,回滚了,没有加入数据
service2内层事务方法,加入了数据,没有回滚。
外层方法依然是一个事务,内层方法被嵌套进了外层中,内层方法出现异常,外层操作也是被回滚,但是内层方法不支持事务,无论怎样都不会回滚。内层:我犯了错误,外层遭罪,但是我自己不遭罪,我的一切操作都不会回滚
2. 外层方法抛出异常,内层方法正常执行
结果:service1控制的2个表都被回滚了,service2控制的2个表都提交了
还是内层方法无论如何都不会回滚的
3.外层方法删掉事务注解,内层方法抛出异常
这个就不用考虑了吧,既然内层不支持事务,外层事务也删了,那就是2个没有事务的方法,都不会回滚
结论
该机制不支持事务,无论如何,都不会回滚。外层事务会被先挂起,执行完当前代码,则恢复外层事务,但是如果方法执行中出现异常,产生回滚条件,会影响到外层事务,外层事务会被回滚,也就是说内层事务会影响外层事务的提交/回滚,但是外层事务影响不到内层事务的提交/回滚,内层事务是绝对不会回滚的。
PROPAGATION_NEVER
这种传播机制的意思是该传播机制不支持外层事务,即如果外层有事务就抛出异常
这种情况很单一了,也不用外层内层的抛异常去测试传播关系了,就是外层有事务就不行,咱们试一下
(代码就不上了,还是上面的2个service,只是按照场景稍作改动)
1.外层内层都加事务,都正常执行
看一下执行结果
外层添加事务,确实报错了!
然后我们看日志,Transaction synchronization deregistering SqlSession。好像也回滚了?
看一眼数据,确实,4个表全都没加入数据,是回滚了。
所以,老老实实的听话,外层不加事务了。
外层不加事务,内层抛出个异常
这个和预想的不一样,按理说,内层有事务,内层抛出异常,会回滚,但是数据库里内层事务操作的2个表,都加入数据了。。
结论
外层加事务,确实会报异常,并且内外层的操作都会被回滚、
外层不加事务,内层抛出个异常,很神奇的是内层和外层都没有回滚!这点没想通
PROPAGATION_MANDATORY
这种传播机制的意思是如果外层没有事务,则抛出异常,和上面的恰恰相反
(代码就不上了,还是上面的2个service,只是按照场景稍作改动)
1.外层不加事务
预期之内的报异常了,外层要加事务。
由于外层没加事务,外层的操作肯定是没有回滚了。内层的事务,回滚了。
2.外层正常执行,内层抛出一个异常
看一下数据库,4个表数据都是空,都回滚了,内层的回滚会影响外层的回滚。
3. 外层事务抛出一个异常,内层事务正常执行
看一下数据库,4个表数据都是空,都回滚了,内层的回滚会影响外层的回滚。
结论
首先,外层一定要加事务注解,否则会报错。
其次,其实和第一个默认事务差不多了,都是属于内层加入外层事务,一起提交,一起回滚
PROPAGATION_NESTED
该传播机制的特点是可以保存状态保存点,当前事务回滚到某一个点,从而避免所有的嵌套事务都回滚,即各自回滚各自的,如果子事务没有把异常吃掉,基本还是会引起全部回滚的。
只看描述,我不是很懂,一开始我也是很懵,这到底是个啥玩意?后来看着这篇文章
解惑 spring 嵌套事务 就明白过来了。这是制定了一个嵌套子事务,当嵌套的子事务开始执行时,会获得一个"保存点",这个保存点呢,就是这个机制的精髓所在。嵌套子事务,会跟着外层事务的回滚一起回滚;外层提交,子事务没有抛出异常,就一起提交,如果抛出异常,子事务单独回滚。当嵌套子事务抛出异常时,外层事务会用try捕获,子事务受到异常,回滚,外层事务会回到这个“保存点”,继续向下执行。如果外层事务没有出现异常,外层事务是会正常提交的。但是外层事务出现异常,子事务无论如何都会跟着外层事务回滚。潜套事务是外部事务的一部分, 只有外部事务结束后它才会被提交.
这种方式也是潜套事务最有价值的地方, 它起到了分支执行的效果, 如果 ServiceB.methodB 失败, 那么执行 ServiceC.methodC(), 而 ServiceB.methodB 已经回滚到它执行之前的 “保存点”, 所以不会产生脏数据(相当于此方法从未执行过), 这种特性可以用在某些特殊的业务中, 而 PROPAGATION_REQUIRED 和 PROPAGATION_REQUIRES_NEW 都没有办法做到这一点.
上代码,这里写法与上面略有不同,需要try包裹住内层事务
1.外层事务正常执行,内层事务出现异常
Service1
@Service
public class Service1Impl implements IService1 {
@Resource
private Method1Table1Mapper method1Table1Mapper;
@Resource
private Method1Table2Mapper method1Table2Mapper;
@Autowired
private IService2 iService2;
@Override
@Transactional(propagation = Propagation.REQUIRED)
public int insert1() {
//本方法里面的事务==============================
//表1操作
Method1Table1 method1Tabale1 = new Method1Table1();
method1Tabale1.setMethod1_tabale1("1");
method1Table1Mapper.insert(method1Tabale1);
//表2操作
Method1Table2 method1Tabale2 = new Method1Table2();
method1Tabale2.setMethod1_tabale2("2");
method1Table2Mapper.insert(method1Tabale2);
//================================================
//嵌套进第二个事务方法==============================
try{
iService2.insert2();
}catch (RuntimeException e){
System.out.println("可以执行第二个嵌套子事务");
}
return 0;
}
}
Service2
@Service
public class Service2Impl implements IService2 {
@Resource
private Method2Table1Mapper method2Table1Mapper;
@Resource
private Method2Table2Mapper method2Table2Mapper;
@Override
@Transactional(propagation = Propagation.NESTED)
public int insert2() {
//表1操作
Method2Table1 method2Tabale1 = new Method2Table1();
method2Tabale1.setMethod2_tabale1("1");
method2Table1Mapper.insert(method2Tabale1);
//表2操作
Method2Table2 method2Tabale2 = new Method2Table2();
method2Tabale2.setMethod2_tabale2("2");
method2Table2Mapper.insert(method2Tabale2);
if(1==1){
throw new RuntimeException("抛出一个运行时异常");
}
return 0;
}
}
此时,内层事务里抛出一个异常,外层事务正常执行,看一下结果
数据库中结果,Service1方法控制的2个表,加入了数据,Service2控制的2个表没有加入数据,操作被回滚。
2.外层事务抛出异常,内层事务正常执行
代码就不上了,直接看数据库中结果
Service1控制的2个表,没有数据,操作被回滚
Service2控制的2个表,没有数据,操作被回滚
结论
内层事务会跟着外层事务的回滚而回滚,无论内层事务抛出异常回滚与否
外层事务如果没有抛出异常,外层事务必定会提交。
内层事务抛出异常,外层事务会回到savepoint保存点,内层事务单独回滚,就像没有执行过一样。
核心还是这个保存点