Spring的事务传播性与隔离级别以及实现事物回滚

时间:2021-03-03 11:23:40

一、事务的四个特性(ACID)

原子性(Atomicity):一个事务中所有对数据库的操作是一个不可分割的操作序列,要么全做,要么全部做。

一致性(Consistency): 数据不会因为事务的执行而遭到破坏。

隔离性(Isolation):一个事务的执行,不受其他事务(进程)的干扰。既并发执行的个事务之间互不干扰。

持久性(Durability):一个事务一旦提交,它对数据库的改变将是永久的。

二、事务的实现方式

实现方式共有两种:编码方式和声明式事务管理方式。

基于AOP技术实现的声明式事务管理,实质就是:在方法执行前后进行拦截,然后在目标方法开始之前创建并加入事务,执行完目标方法后根据执行情况提交或回滚事务。

三、创建事务的时机

  是否需要创建事务,是由事务传播行为控制的。读数据不需要或只为其指定只读事务,而数据的插入、修改、删除就需要进行事务管理了,这是由事务的隔离级别控制的。

事务具有7个传播级别和4个隔离级别,传播级别定义的是事务创建的时机,隔离级别定义的是对并发事务数据读取的控制。

四、事务的传播级别

事务的传播级别一共有7种:

1.PROPAGATION_REQUIRED

默认的Spring事务传播级别,使用该级别的特点是:如果上下文中已经存在事务,那么就加入到事务中执行;如果当前上下文中不存在事务,则新建事务执行。所以这个级别通常能满足处理大多数的业务场景。

2.PROPAGATION_SUPPORTS

从字面意思就知道,supports,支持,该传播级别的特点是:如果上下文存在事务,则支持事务加入事务,如果没有事务,则使用非事务的方式执行。

所以说,并非所有的包含在TransactionTemplate.execute方法中的代码都会有事务支持。这个通常是用来处理那些并非原子性的非核心业务逻辑操作。应用场景较少。

3.PROPAGATION_MANDATORY

该级别的事务要求上下文中必须要存在事务,否则就会抛出异常!配置该方式的传播级别是有效的控制上下文调用代码遗漏添加事务控制的保证手段。

比如一段代码不能单独被调用执行,但是一旦被调用,就必须有事务包含的情况,就可以使用这个传播级别。

4.PROPAGATION_REQUIRES_NEW

从字面即可知道,new,每次都要一个新事务,该传播级别的特点是:每次都会新建一个事务,并且同时将上下文中的事务挂起,执行当前新建事务完成以后,上下文事务恢复再执行。

这是一个很有用的传播级别,举一个应用场景:现在有一个发送100个红包的操作,在发送之前,要做一些系统的初始化、验证、数据记录操作,然后发送100封红包,然后再记录发送日志,发送日志要求100%的准确,如果日志不准确,那么整个父事务逻辑需要回滚。

怎么处理整个业务需求呢?就是通过这个PROPAGATION_REQUIRES_NEW级别的事务传播控制就可以完成。发送红包的子事务不会直接影响到父事务的提交和回滚。

5.PROPAGATION_NOT_SUPPORTED

这个也可以从字面得知,not supported,不支持,当前级别的特点是:若上下文中存在事务,则挂起事务,执行当前逻辑,结束后恢复上下文的事务。

这个级别有什么好处?可以帮助你将事务尽可能的缩小。我们知道一个事务越大,它存在的风险也就越多。所以在处理事务的过程中,要保证尽可能的缩小范围。比如一段代码,是每次逻辑操作都必须调用的,比如循环1000次的某个非核心业务逻辑操作。这样的代码如果包在事务中,势必造成事务太大,导致出现一些难以考虑周全的异常情况,所以事务的这个传播级别就派上用场了。用当前级别的事务模板包含起来就可以了。

6.PROPAGATION_NEVER

该事务更严格,上面一个事务传播级别只是不支持而已,有事务就挂起,而PROPAGATION_NEVER传播级别要求上下文中不能存在事务,一旦有事务,就抛出runtime异常,强制停止执行!这个级别上辈子跟事务有仇。

7.PROPAGATION_NESTED

从字面也可知道,nested,嵌套级别事务。该传播级别的特征是:如果上下文中存在事务,则嵌套事务执行,如果不存在事务,则新建事务。

那么什么是嵌套事务呢?很多人都不理解,我看过一些博客,都是有些理解偏差。

嵌套是子事务嵌套在父事务中执行,子事务是父事务的一部分,在进入子事务之前,父事务建立一个回滚点,叫save point,然后执行子事务,这个子事务的执行也算是父事务的一部分,然后子事务执行结束,父事务继续执行。重点就在于那个save point。看几个问题就明了了:

(1)   如果子事务回滚,会发生什么?

父事务会回滚到进入子事务前建立的save point,然后尝试其他的事务或者其他的业务逻辑,父事务之前的操作不会受到影响,更不会自动回滚。

(2)   如果父事务回滚,会发生什么?

父事务回滚,子事务也会跟着回滚!为什么呢,因为父事务结束之前,子事务是不会提交的,我们说子事务是父事务的一部分,正是这个道理。

(3)   事务的提交,是什么情况?

是父事务先提交,然后子事务提交,还是子事务先提交,父事务再提交?答案是第二种情况,还是那句话,子事务是父事务的一部分,由父事务统一提交。

现在你再体会一下这个”嵌套“,是不是有那么点意思了?

以上是事务的7个传播级别,在日常应用中,通常可以满足各种业务需求,但是除了传播级别,在读取数据库的过程中,如果两个事务并发执行,那么彼此之间的数据是如何影响的呢?这就需要了解一下事务的另一个特性:数据隔离级别。

五、事务隔离级别

1.SERIALIZABLE(串行化)

最严格的级别,事务串行执行,资源消耗最大。

2.REPEATABLE_READ(可重复读)

保证了一个事务不会修改已经由另一个事务读取但未提交(回滚)的数据。避免了“脏读取”和“不可重复读取”的情况,但是带来了更多的性能损失。

3.READ_COMMITTED(提交读)

大多数主流数据库的默认事务等级,保证了一个事务不会读到另一个并行事务已修改但未提交的数据,避免了“脏读取”。该级别适用于大多数系统。

4.READ_UNCOMMITTED(未提交读)

保证了读取过程中不会读取到非法数据。

上面的解释其实每个定义都有一些拗口,其中涉及到几个术语:脏读、不可重复读、幻读。这里解释一下:

脏读(Dirty Reads)

所谓的脏读,其实就是读到了别的事务回滚前的脏数据。比如事务B执行过程中修改了数据X,在未提交前,事务A读取了X,而事务B却回滚了,这样事务A就形成了脏读。

不可重复读

不可重复读字面含义已经很明了了,比如事务A首先读取了一条数据,然后执行逻辑的时候,事务B将这条数据改变了,然后事务A再次读取的时候,发现数据不匹配了,就是所谓的不可重复读了。

幻读

小的时候数手指,第一次数十10个,第二次数是11个,怎么回事?产生幻觉了?

幻读也是这样子,事务A首先根据条件索引得到10条数据,然后事务B改变了数据库一条数据,导致也符合事务A当时的搜索条件,这样事务A再次搜索发现有11条数据了,就产生了幻读。

事务隔离级别对照关系表:

Spring的事务传播性与隔离级别以及实现事物回滚

所以最安全的,是Serializable,但是伴随而来也是高昂的性能开销。

另外,事务常用的两个属性:① readonly,设置事务为只读以提升性能;② timeout,设置事务的超时时间,一般用于防止大事务的发生。

六、Spring管理声明式事务的配置

需要注意的是:

spring 的默认事务机制,当出现unchecked异常时候回滚,checked异常的时候不会回滚,也就是默认对RuntimeException()异常或是其子类进行事务回滚,

当有try catch后捕获了异常(不管是哪种异常),事务不会回滚,如果不得不在service层写try catch 需要catch后 throw new RuntimeException 让事务回滚。

1.基于XML的配置

applicationContext.xml配置如下

 <?xml version="1.0" encoding="gbk"?>

 <beans xmlns="http://www.springframework.org/schema/beans"

     xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"

     xmlns:aop="http://www.springframework.org/schema/aop"

     xmlns:tx="http://www.springframework.org/schema/tx"

     xsi:schemaLocation="http://www.springframework.org/schema/beans

     http://www.springframework.org/schema/beans/spring-beans-2.0.xsd

     http://www.springframework.org/schema/tx

     http://www.springframework.org/schema/tx/spring-tx-2.0.xsd

     http://www.springframework.org/schema/aop

     http://www.springframework.org/schema/aop/spring-aop-2.0.xsd">

     <bean id="dataSource"

         class="org.apache.commons.dbcp.BasicDataSource">

         <property name="driverClassName"

             value="com.microsoft.sqlserver.jdbc.SQLServerDriver">

         </property>

         <property name="url"

             value="jdbc:sqlserver://localhost:1500;databaseName=ssh">

         </property>

         <property name="username" value="sa"></property>

         <property name="password" value="sa"></property>

     </bean>

     <bean id="sessionFactory"

         class="org.springframework.orm.hibernate3.LocalSessionFactoryBean">

         <property name="dataSource">

             <ref bean="dataSource" />

         </property>

         <property name="hibernateProperties">

             <props>

                 <prop key="hibernate.dialect">

                     org.hibernate.dialect.SQLServerDialect

                 </prop>

             </props>

         </property>

         <property name="mappingResources">

             <list>

                 <value>bank/entity/Account.hbm.xml</value>

             </list>

         </property>

     </bean>

     <bean id="AccountDAO" class="bank.dao.AccountDAO">

         <property name="sessionFactory">

             <ref bean="sessionFactory" />

         </property>

     </bean>

     <bean id="AccountManager" class="bank.biz.AccountManager">

         <property name="dao">

             <ref bean="AccountDAO" />

         </property>

     </bean>

     <bean name="/account" class="bank.action.AccountAction">

         <property name="accountManager">

             <ref bean="AccountManager" />

         </property>

     </bean>  

     <!--通用事务管理器-->

     <bean id="TransactionManager"

         class="org.springframework.orm.hibernate3.HibernateTransactionManager">

         <property name="sessionFactory" ref="sessionFactory" />

     </bean>

     <!--指定事务策略,声明一个通知,用以指出要管理哪些事务方法及如何管理-->

     <tx:advice id="txAdvice" transaction-manager="TransactionManager">

         <tx:attributes>

             <!-- 对get/load/search开头的方法要求只读事务 -->

             <tx:method name="find*" propagation="SUPPORTS"

                 read-only="true" />

             <!-- 对其它方法要求事务 -->

             <tx:method name="*" propagation="REQUIRED" />

         </tx:attributes>

     </tx:advice>

     <!--声明一个config,用以将事务策略和业务类关联起来-->

     <aop:config>

         <!-- 添加事务支持,因为前面配置的transactionManager是专对Hibernate的事务管理器-->

         <aop:pointcut id="bizMethods" expression="execution(* bank.biz..*.*(..))" />

         <!-- 织入 -->

         <aop:advisor advice-ref="txAdvice" pointcut-ref="bizMethods" />

     </aop:config>          

 </beans>

2.基于注解@Transactional的配置

一般来说我们在Service层的方法上打上@Transactional注解就可以启用事务了,但是有时候需要特别设置其中的一些属性。

 @Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface Transactional { @AliasFor("transactionManager")
String value() default ""; @AliasFor("value")
String transactionManager() default "";
//设置事务传播级别
Propagation propagation() default Propagation.REQUIRED;
//设置事务隔离级别
Isolation isolation() default Isolation.DEFAULT;
//设置事务超时时间
int timeout() default TransactionDefinition.TIMEOUT_DEFAULT;
//是否为只读
boolean readOnly() default false;
//如果抛出指定类型的异常则回滚
Class<? extends Throwable>[] rollbackFor() default {};
//如果抛出数组里任意一个异常则回滚
String[] rollbackForClassName() default {};
//针对特定类型的异常不会滚
Class<? extends Throwable>[] noRollbackFor() default {};
//针对一组类型的异常不会滚
String[] noRollbackForClassName() default {}; }

如果配置了rollbackFor 和 noRollbackFor 且两个都是用同样的异常,那么遇到该异常,还是回滚。

参考地址:https://blog.csdn.net/yang1982_0907/article/details/44408809

https://blog.csdn.net/weixin_38379125/article/details/78727303