事务的隔离级别和传播机制
更新日期:
事务
什么是事务?
要么全部都要执行,要么就都不执行。
事务所具有的四种特性
原子性 (Atimicty)
个人理解,就是事务执行不可分割,要么全部完成,要么全部拉倒不干。
一致性(Consistency)
关于一致性这个概念我们来举个例子说明吧,假设张三给李四转了100元,那么需要先从张三那边扣除100,然后李四那边增加100,这个转账的过程对于其他事务而言是无法看到的,这种状态始终都在保持一致,这个过程我们称之为一致性。
隔离性(Isolation)
并发访问数据库时,一个用户的事务不被其他事务所干扰,各并发事务之间数据是独立的;
持久性(Durability)
一个事务被提交之后。它对数据库中数据的改变是持久的,即使数据库发生故障也不应该对其有任何影响。
事务并发问题
脏读: 事务A读取了事务B更新的数据,然后B回滚操作,那么A读取到的数据是脏数据
不可重复读:事务 A 多次读取同一数据,事务 B 在事务A多次读取的过程中,对数据作了更新并提交,导致事务A多次读取同一数据时,结果不一致。
幻读: 用户A将数据库中所有学生的成绩从具体分数改为ABCDE等级,但是用户B就在这个时候插入了一条具体分数的记录,当修改A改结束后发现还有一条记录没有改过来,就好像发生了幻觉一样,这就叫幻读。(一个事务读到另一个事务已提交的insert数据)
概念:
1 | 脏读: 指当一个事务正在访问数据,并且对数据进行了修改,而这种修改还没有提交到数据库中,这时,另外一个事务也访问这个数据,然后使用了这个数据。因为这个数据是还没有提交的数据, 那么另外一 个事务读到的这个数据是脏数据,依据脏数据所做的操作可能是不正确的。 |
小结:不可重复读的和幻读很容易混淆,不可重复读侧重于修改,幻读侧重于新增或删除。解决不可重复读的问题只需锁住满足条件的行,解决幻读需要锁表
思考:mysql的不可重复读解决了幻读?https://www.cnblogs.com/liyus/p/10556563.html
事务隔离级别
事务隔离级别 脏读 不可重复读 幻读
读未提交(read-uncommitted) 是 是 是
读已提交(read-committed) 否 是 是
可重复读(repeatable-read) 否 否 是
串行化(serializable) 否 否 否
MYSQL: 默认为REPEATABLE_READ级别
SQLSERVER: 默认为READ_COMMITTED
Spring对事务的处理
编程式事务和声明式事务
编程式事务需要你在代码中直接加入处理事务的逻辑,可能需要在代码中显式调用beginTransaction()、commit()、rollback()等事务管理相关的方法,如在执行a方法时候需要事务处理,你需要在a方法开始时候开启事务,处理完后。在方法结束时候,关闭事务.
声明式的事务的做法是在a方法外围添加注解或者直接在配置文件中定义,a方法需要事务处理,在spring中会通过配置文件在a方法前后拦截,并添加事务.
二者区别.编程式事务侵入性比较强,但处理粒度更细.
声明式事务:通过AOP(面向切面)方式在方法前使用编程式事务的方法开启事务,在方法后提交或回滚。用配置文件的方法或注解方法(如:@Transactional)控制事务。
编程式事务:手动开启、提交、回滚事务。
spring在事务方面进行了各种操作的封装,特别是声明式事务的出现,让开发变得更加的舒心.
spring事务定义及状态描述
从事务管理器PlatformTransactionManager中可以看出,spring完成事务管理还需2个关键元素:事务定义TransactionDefinition及事务状态TransactionStatus描述。
事务定义
1 | public interface TransactionDefinition { |
事务状态
1 | public interface TransactionStatus extends SavepointManager { |
什么是事务的传播行为
Spring在TransactionDefinition接口中规定了7种类型的事务传播行为。事务传播行为是Spring框架独有的事务增强特性,他不属于的事务实际提供方数据库行为。这是Spring为我们提供的强大的工具箱,使用事务传播行可以为我们的开发工作提供许多便利。但是人们对他的误解也颇多,你一定也听过“service方法事务最好不要嵌套”的传言。要想正确的使用工具首先需要了解工具。
什么是事务传播行为?
事务传播行为用来描述由某一个事务传播行为修饰的方法被嵌套进另一个方法的时事务如何传播。
事务的传播行为
1)PROPAGATION_REQUIRED
说明: 如果当前已经存在事务,那么加入该事务,如果不存在事务,创建一个事务,这是默认的传播属性值。
2)PROPAGATION_SUPPORTS
说明: 如果当前已经存在事务,那么加入该事务,否则创建一个所谓的空事务(可以认为无事务执行)。
3) PROPAGATION_MANDATORY
说明:当前必须存在一个事务,否则抛出异常。
4)PROPAGATN_REQUIRES_NEW
说明:如果当前存在事务,先把当前事务相关内容封装到一个实体,然后重新创建一个新事务,接受这个实体为参数,用于事务的恢复。更直白的说法就是暂停当前事务(当前无事务则不需要),创建一个新事务。 针对这种情况,两个事务没有依赖关系,可以实现新事务回滚了,但外部事务继续执行。
5)Propagation.NOT_SUPPORTED
说明:如果当前存在事务,挂起当前事务,然后新的方法在没有事务的环境中执行,没有spring事务的环境下,sql的提交完全依赖于 defaultAutoCommit属性值 。
6) PROPAGATION_NEVER
说明: 如果当前存在事务,则抛出异常,否则在无事务环境上执行代码。
7)PROPAGATION_NESTED
说明: 如果当前存在事务,则使用 SavePoint 技术把当前事务状态进行保存,然后底层共用一个连接,当NESTED内部出错的时候,自行回滚到 SavePoint这个状态,只要外部捕获到了异常,就可以继续进行外部的事务提交,而不会受到内嵌业务的干扰,但是,如果外部事务抛出了异常,整个大事务都会回滚。
注意: spring配置事务管理器要主动指定 nestedTransactionAllowed=true,如下所示:
1 | <bean id="dataTransactionManager" |
例子:
1 | @Transactional |
serviceB是一个内嵌的业务,内部抛出了运行时异常,所以serviceB整个被回滚了,由于service捕获了异常,所以serviceA是可以正常提交的。
1 | @Transactional |
由于service抛出了异常,所以会导致整个service方法被回滚。(这就是跟PROPAGATION_REQUIRES_NEW不一样的地方了,NESTED方式下的内嵌业务会受到外部事务的异常而回滚。)
实现原理浅析
前面举例说明了spring事务提供的几种传播属性,用于满足多种不同的业务需求,大家可以依业务而定。接着我们再来看看spring实现这些传播属性最重要的技术依赖是什么。本小节列举 PROPAGATION_REQUIRES_NEW 和 Propagation.NESTED 分别进行简要说明。
1、 PROPAGATION_REQUIRES_NEW 实现原理
1 | @Transactional |
执行原理图如下
1 | before service,执行a和b |
a. 创建事务状态对象,获取一个新的连接,重置连接的 autoCommit,fetchSize,timeout等属性
b. 把连接绑定到ThreadLocal变量
c. 挂起当前事务,把当前事务状态对象,连接等信息封装成一SuspendedResources对象,可用于恢复
d. 创建新的事务状态对象,重新获取新的连接,重置新连接的 autoCommit,fetchSize,timeout等属性,同时,保存SuspendedResources对象,用于事务的恢复,把新的连接绑定到ThreadLocal变量(覆盖操作)
e. 捕获到异常,回滚ThreadLocal中的连接,恢复连接参数,关闭连接,恢复SuspendedResources
f. 提交ThreadLocal变量中的连接(导致serviceB被提交),还原连接参数,关闭连接,连接归还数据源
所以程序执行的结果就是 serviceA被回滚了,serviceB成功提交了。
2、 PROPAGATION_NESTED 实现原理
1 | @Transactional |
执行原理图如下:
a. 创建事务状态对象,获取一个新的连接,重置连接的 autoCommit,fetchSize,timeout等属性
b. 把连接绑定到ThreadLocal变量
c. 标记使用当前事务状态对象,获取ThreadLocal连接对象,保存当前连接的SavePoint,用于异常恢复,此时的SavePoint就是执行完serviceA后的状态
d. 捕获到异常,使用c中的SavePoint进行事务回滚,也就是把状态回滚到执行serviceA后的状态,serviceB方法所有执行不生效
e. 获取ThreadLocal中的连接对象,提交事务,恢复连接属性,关闭连接
其他:
spring在底层数据源的基础上,利用 ThreadLocal,SavePoint等技术点实现了多种事务传播属性,便于实现各种复杂的业务。只有理解了传播属性的原理才能更好的驾驭spring事务。Spring回滚事务依赖于对异常的捕获,默认情况下,只有抛出RuntimeException和Error才会回滚事务,当然可以进行配置,更多信息可以查看 @Transactional 这个注解。
Spring事务的几个传播机制先假设A是大方法,B是小方法
required,A没有事务时调用B方法(B的事务隔离级别是required),B就会开启自己单独的事务,B要回滚的话,也是回滚B。A有事务时,B就加入到A这个事务(B不开启自己的事务了),这里发生回滚的话,AB的事务就都被回滚了。
new , A没有事务时调用B方法(B的事务隔离级别是new),B就会开启自己单独的事务,B要回滚的话,也是回滚B。A有事务时,B还是会开启自己的事务,B事务发生回滚时,AB事务都会一起回滚,但是当执行了B事务时,A事务时挂起的,当执行完B事务且commit成功后,B事务结束,A事务恢复,若此时A事务发生回滚,那只是A事务回滚,不影响B事务的提交了。
nested,A没有事务调用B方法时(B的事务隔离级别是nested),此时就相当于是required了。A有事务时,B事务也开启,但B事务此时是相当于A事务的嵌套子事务,B事务发生回滚时,会回到B事务开启执行时的savepoint(保存点),此时B事务已经结束了,然后继续从B事务开始时的保存点开始,继续A事务。如果期间B事务没回滚,那B事务的提交也是要和A事务一起提交,而且此时B事务没回滚执行完之后,但A事务出现回滚,AB事务就没法提交,那就都回滚了(这就是嵌套子事务的概念吧?)
require回滚整个事务,nested回滚到创建回滚点的地方
参考:https://www.cnblogs.com/tartis/p/9232660.html
https://blog.csdn.net/f45056231p/article/details/83510291
https://blog.csdn.net/chuangxin/article/details/80921704
只读事务的概念
从这一点设置的时间点开始(时间点a)到这个事务结束的过程中,其他事务所提交的数据,该事务将看不见!(查询中不会出现别人在时间点a之后提交的数据)
应用场合:
如果你一次执行单条查询语句,则没有必要启用事务支持,数据库默认支持SQL执行期间的读一致性;
如果你一次执行多条查询语句,例如统计查询,报表查询,在这种场景下,多条查询SQL必须保证整体的读一致性,否则,在前条SQL查询之后,后条SQL查询之前,数据被其他用户改变,则该次整体的统计查询将会出现读数据不一致的状态,此时,应该启用事务支持。
【注意是一次执行多次查询来统计某些信息,这时为了保证数据整体的一致性,要用只读事务】
怎样设置:
对于只读查询,可以指定事务类型为readonly,即只读事务。
由于只读事务不存在数据的修改,因此数据库将会为只读事务提供一些优化手段,例如Oracle对于只读事务,不启动回滚段,不记录回滚log。
(1)在JDBC中,指定只读事务的办法为: connection.setReadOnly(true);
(2)在Hibernate中,指定只读事务的办法为: session.setFlushMode(FlushMode.NEVER);
此时,Hibernate也会为只读事务提供Session方面的一些优化手段
(3)在Spring的Hibernate封装中,指定只读事务的办法为: bean配置文件中,prop属性增加“readOnly”
或者用注解方式@Transactional(readOnly=true)
【 if the transaction is marked as read-only, Spring will set the Hibernate Session’s flush mode to FLUSH_NEVER,
and will set the JDBC transaction to read-only】也就是说在Spring中设置只读事务是利用上面两种方式
在将事务设置成只读后,相当于将数据库设置成只读数据库,此时若要进行写的操作,会出现错误
参考:https://blog.csdn.net/andyzhaojianhui/article/details/51984157