事务及Spring事务传播机制

2022-07-22 14:16:22

Spring事务

--洱涷zZ


事务的四大特征

  1. 原子性:是不可分割的最小操作单位,要么同时成功,要么同时失败
  2. 持久性:如果事务一旦提交或者回滚,那么数据库表的数据将被持久的更新
  3. 隔离性:多个事务之间相互独立,但是真实情况下,多个事务之间会产生影响
  4. 一致性:事务操作前后,数据总量不变

事务的隔离级别

多个事务之间是是隔离的,相互独立的,如果多个事务操作同一批数据,则会引发一些问题,设置不同的隔离级别,就可以解决这些问题。

存在问题:

  1. 脏读:一个事务读取到另一个事务没有提交的数据

    栗子:问李四接了500,李四将事务隔离级别设置成读未提交,对张三进行转账后但是没有提交,张三查看自己确实多了500,然后给李四写了借条,李四收到借条并将数据回滚,张三的500又回到李四账户,张三还得给李四还500。

  2. 不可重复读(虚读):在同一个事务中,两次读取到的数据不一样

  3. 幻读:一个事务操作(DML)数据表中的所有记录,另一个事务添加了一条数据,则第一个事务查询不到自己的修改。

隔离级别:

1.read uncommitted:读未提交

产生的问题:脏读,虚读,幻读

2.read committed:读已提交(Oracle默认的)

产生的问题:虚读,幻读

3.repeatable read:可重复读(MySQL默认的)

产生的问题:幻读

4.serializable:串行化(其实就是锁表 当一个事务在操作这个表的时候,其他事务是无法操作这个表的)可以解决所有的问题。

注意:隔离级别从小到大 安全性越来越高,但是效率越来越低

事务注解@Transactional

事务管理对于企业应用来说是至关重要的,即使出现异常情况,它也可以保证数据的一致性。

spring支持编程式事务管理和声明式事务管理两种方式。

编程式事务管理使用TransactionTemplate或者直接使用底层的PlatformTransactionManager。对于编程式事务管理,spring推荐使用TransactionTemplate。

声明式事务管理建立在AOP之上的。其本质是对方法前后进行拦截,然后在目标方法开始之前创建或者加入一个事务,在执行完目标方法之后根据执行情况提交或者回滚事务。

声明式事务管理也有两种常用的方式,一种是基于tx和aop名字空间的xml配置文件,另一种就是基于@Transactional注解。显然基于注解的方式更简单易用,更清爽。

当@Transactional作用于类上时,该类的所有 public 方法将都具有该类型的事务属性,同时,我们也可以在方法级别使用该标注来覆盖类级别的定义。

在项目中,@Transactional(rollbackFor=Exception.class),如果类加了这个注解,那么这个类里面的方法抛出异常,就会回滚,数据库里面的数据也会回滚。

在@Transactional注解中如果不配置rollbackFor属性,那么事物只会在遇到RuntimeException的时候才会回滚,加上rollbackFor=Exception.class,可以让事物在遇到非运行时异常时也回滚

@Transactional属性:

属性类型描述
valueString可选的限定描述符,指定使用的事务管理器
propagationenum: Propagation可选的事务传播行为设置
isolationenum: Isolation可选的事务隔离级别设置
readOnlyboolean读写或只读事务,默认读写
timeoutint (in seconds granularity)事务超时时间设置
rollbackForClass对象数组,必须继承自Throwable导致事务回滚的异常类数组
rollbackForClassName类名数组,必须继承自Throwable导致事务回滚的异常类名字数组
noRollbackForClass对象数组,必须继承自Throwable不会导致事务回滚的异常类数组
noRollbackForClassName类名数组,必须继承自Throwable不会导致事务回滚的异常类名字数组

事务的传播机制

在Spring中对于事务的传播行为定义了七种类型分别是:REQUIRED、SUPPORTS、MANDATORY、REQUIRES_NEW、NOT_SUPPORTED、NEVER、NESTED

Spring中事务的默认实现使用的是AOP,也就是代理的方式。

先创建一个事务应用的场景

publicvoidtestMain(){A(a1);testB();}publicvoidtestB(){B(b1);throw Exception;B(b2);}

如果说给这两个方法都没有添加事务,那么此时运行testMain()方法,会发生什么情况呢,a1,b1分别存入对应的数据库表tableA1,tableB1,然后抛出异常,程序停止执行,b2不会被插入tableB2

REQUIRED

此种事务传播类型是Spring的默认事务传播类型

意思是:如果当前没有事务,则自己新建一个事务,如果当前存在事务,则加入

举栗子:

@Transational(propagation=Propagation.REQUIRED)publicvoidtestMain(){A(a1);testB();}@Transational(propagation=Propagation.REQUIRED)publicvoidtestB(){B(b1);throw Exception;B(b2);}

此场景下,此时数据库表数据为空,如果执行testMain(),先插入a1,然后插入b1,然后方法抛出异常后,事务发生回滚,因为testMain已经有事务,所以testB就直接加入了testMain的事务,那么当前testMain和testB是同一个事务,testB抛出异常事务回滚,testMain也会发生事务回滚,那么此时数据库表还是为空,所有数据都没有插入。

再举个栗子:

publicvoidtestMain(){A(a1);testB();}@Transational(propagation=Propagation.REQUIRED)publicvoidtestB(){B(b1);throw Exception;B(b2);}

此场景下,testB有事务,testMain没有事务,那么在调用testMain时,testB判断testMain没有事务后自己就会新建一个事务,此时调用testMain,在testB抛出异常后,testB会发生回滚,而testMain就不会,所以此时a1就会被插入,b1会插入后因为事务回滚消失,b1,b2都不会被插入。

REQUIRES_NEW

意思是:创建一个新事务,如果存在当前事务,则挂起该事务

举栗子:为了说明requires_new会开启一个新事务,我把异常的位置放在testMain,然后给testMain声明事务,传播类型为required,testB也设置事务,传播方式为requires_new,如下

@Transational(propagation=Propagation.REQUIRED)publicvoidtestMain(){A(a1);testB();throw Exception;}@Transational(propagation=Propagation.REQUIRES_NEW)publicvoidtestB(){B(b1);B(b2);}

此时调用testMain,a1不会被插入,b1,b2会被插入,因为testMain和testB是两个事务,testMain抛出异常之后,事务回滚并不会影响到testB。
与该场景相对应的就是两个传播方式都是REQUIRED,那么上面执行时,数据都不会被存储,因为是同一个事务,事务发生回滚时,所有数据都会发生回滚。

如果说异常抛出点还是在testB的b1插入后,那么这个栗子,运行后结果是a1,b1,b2都不会被存储成功,此时并不是因为是一个事务所以一回滚都回滚,而是testB抛出异常后,testMain检测到它调用的方法出现异常,那么它也会回滚。

SUPPORTS

意思是:如果当前存在事务,那么就加入该事务,如果不存在事务,就以非事务方法执行

举栗子:

public void testMain(){
    A(a1);
    testB();
}
@Transational(propagation=Propagation.SUPPORTS)
public void testB(){
    B(b1);
	throw Exception;
    B(b2);
}

此时只在testB上声明事务传播机制为SUPPORTS,testMain无事务,当调用testMain时,a1存储,然后调用testB,testB判断testMain无事务,所以它也会按照无事务方法执行,结果就是a1,b1都被存入数据库,b2没有存入,因为抛出异常时,程序已停止运行,b2未被执行。

那么当我们在testMain上声明事务传播机制为REQUIRED,testB上声明事务传播机制为SUPPORTS,此时testB就会加入testMain的事务,最终结果就是a1,b1,b2都没有被存储。

NOT_SUPPORTED

意思是:始终以非事务方式执行,如果当前存在事务,则挂起该事务

可以理解为设置事务传播类型为NOT_SUPPORTED的方法,在执行时,不管调用它的方法事务传播机制是啥,它都会按照非事务方法执行

举个栗子:

@Transational(propagation=Propagation.REQUIRED)publicvoidtestMain(){A(a1);testB();}@Transational(propagation=Propagation.NOT_SUPPORTED)publicvoidtestB(){B(b1);throw Exception;B(b2);}

调用testMain,最终结果为只有b1存储成功,因为testB是按照非事务方法执行,所以b1存储成功,b2未存储,此时testMain事务传播机制为REQUIRED,其检测到调用有异常被抛出,因为testMain和testB不是一个事务,所以只有testMain会发生回滚,a1也不会被存储成功。

MANDATORY

意思是:如果当前存在事务,那么就加入,如果当前事务不存在,则抛出异常

举个栗子

publicvoidtestMain(){A(a1);testB();}@Transactional(propagation= Propagation.MANDATORY)publicvoidtestB(){B(b1);throw Exception;B(b2);}

这种场景下最终结果为a1插入成功,b1,b2没有插入,此时未插入并不是因为testB数据回滚,而是因为testMain没有声明事务,所以testB会直接抛出事务要求的异常,所以testB中的方法就没执行

如果testMain声明了事务,且设置为REQUIRED,那么在调用testB时就会正常的数据回滚,a1,b1,b2都不会被存储。

NEVER

意思是:不使用事务,如果当前事务存在则抛出异常

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

举个栗子:

@Transational(propagation=Propagation.REQUIRED)
public void testMain(){
    A(a1);
    testB();
}
@Transational(propagation=Propagation.NEVER)
public void testB(){
    B(b1);
    B(b2);
}

此种场景运行testMain会直接爆出异常,因为testB的事务传播机制是不允许调用它的事务存在事务的,此时testMain检测发现方法内部有异常就会发生回滚,所以数据库数据是没有发生更新的。

NESTED

意思是:如果当前事务存在,那就在事务中进行嵌套执行,如果不存在,则开启一个事务,传播机制为REQUIRED。

  • 和REQUIRES_NEW的区别

REQUIRES_NEW是新建一个事务并且新开启的这个事务与原有事务无关,而NESTED则是当前存在事务时(我们把当前事务称之为父事务)会开启一个嵌套事务(称之为一个子事务)。
在NESTED情况下父事务回滚时,子事务也会回滚,而在REQUIRES_NEW情况下,原有事务回滚,不会影响新开启的事务。

  • 和REQUIRED的区别

REQUIRED情况下,调用方存在事务时,则被调用方和调用方使用同一事务,那么被调用方出现异常时,由于共用一个事务,所以无论调用方是否catch其异常,事务都会回滚
而在NESTED情况下,被调用方发生异常时,调用方可以catch其异常,这样只有子事务回滚,父事务不受影响

举个栗子:

@Transactional(propagation= Propagation.REQUIRED)publicvoidtestMain(){A(a1);testB();throw Exception;}@Transactional(propagation= Propagation.NESTED)publicvoidtestB(){B(b1);B(b2);}

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

再举个栗子:

@Transactional(propagation= Propagation.REQUIRED)publicvoidtestMain(){A(a1);try{testB();}catch(Exception e){}A(a2);}@Transactional(propagation= Propagation.NESTED)publicvoidtestB(){B(b1);throw Exception;B(b2);}

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

同样的代码,如果我们把testB的传播类型改为REQUIRED,结果也就变成了:没有数据存储成功。就算在调用方catch了异常,整个事务还是会回滚,因为,调用方和被调方共用的同一个事务。

  • 作者:圆圆的球
  • 原文链接:https://blog.csdn.net/qq_43803129/article/details/118961264
    更新时间:2022-07-22 14:16:22