浅谈Spring的事务隔离级别和事务传播机制
Spring在TransactionDefinition接口中定义事务属性,其中就包括了事务的隔离级别和传播机制。
事务属性可以理解成事务的一些基本配置,描述了事务策略如何应用到方法上。事务属性包含了5个方面,如图所示:
// 它的常用实现类为:DefaultTransactionDefinition
// TransactionAttribute接口也继承自此接口~~~
public interface TransactionDefinition {
// 7种类型的事务传播行为
int PROPAGATION_REQUIRED = 0; // 如果当前没有事务,就新建一个事务,如果已经存在一个事务中,加入到这个事务中。这是最常见的选择。
int PROPAGATION_SUPPORTS = 1; //支持当前事务,如果当前没有事务,就以**非事务**方式执行
int PROPAGATION_MANDATORY = 2; //使用当前的事务,如果当前没有事务,就抛出异常
int PROPAGATION_REQUIRES_NEW = 3; //新建事务,如果当前存在事务,把当前事务挂起 ===== 这个也是使用较多的 相当于另起炉灶
int PROPAGATION_NOT_SUPPORTED = 4; //以非事务方式执行操作,如果当前存在事务,就把当前事务挂起。
int PROPAGATION_NEVER = 5; //以非事务方式执行,如果当前存在事务,则抛出异常
int PROPAGATION_NESTED = 6; //如果当前存在事务,则在嵌套事务(它是一个子事务,但他仍还是外部事务的一部分,外部事务提交了它才提交。。。注意和REQUIRES_NEW的区别~~~)内执行。
//如果当前没有事务,则执行与PROPAGATION_REQUIRED类似的操作
// 4种:隔离级别
// PlatformTransactionManager的默认隔离级别(对大多数数据库来说就是ISOLATION_ READ_COMMITTED
// MySQL默认采用ISOLATION_REPEATABLE_READ,Oracle采用READ__COMMITTED级别
int ISOLATION_DEFAULT = -1;
//读未提交 最低的隔离级别。(事实上我们不应该称其为隔离级别) 可能导致脏,幻,不可重复读
int ISOLATION_READ_UNCOMMITTED = Connection.TRANSACTION_READ_UNCOMMITTED;
// 读已提交,大多数数据库的默认级别。可防止脏读,但幻读和不可重复读仍可以发生。
int ISOLATION_READ_COMMITTED = Connection.TRANSACTION_READ_COMMITTED;
// 可重复读。该隔离级别确保如果在事务中查询了某个数据集,你至少还能再次查询到相同的数据集,即使其他事务修改了所查询的数据。
// 可防止脏读,不可重复读,但幻读仍可能发生。
int ISOLATION_REPEATABLE_READ = Connection.TRANSACTION_REPEATABLE_READ;
// 序列化。代价最大、可靠性最高的隔离级别 所有的事务都是按顺序一个接一个地执行。避免所有不安全读取。
int ISOLATION_SERIALIZABLE = Connection.TRANSACTION_SERIALIZABLE;
// 默认的超时时间 -1表示不超时(单位是秒)
// 如果超过该时间限制但事务还没有完成,则自动回滚事务
int TIMEOUT_DEFAULT = -1;
int getPropagationBehavior(); //返回事务的传播行为(一共7种)
int getIsolationLevel(); // 返回事务的隔离级别。 事务管理器根据它来控制另外一个事务可以看到本事务内的哪些数据
int getTimeout(); // 返回超时时间(事务必须在多少秒内完成)
boolean isReadOnly(); // 事务是否只读(事务管理器能够根据这个返回值进行优化,确保事务是只读的)
@Nullable
String getName(); // 事务的名字 可以为null
}
事务隔离级别
并发下的三种状态
要理解隔离级别,必须要先知道在并发的情况下,引起的三种状况。
Dirty Reads 脏读
一个事务正在对数据进行更新操作,但是更新还未提交,另一个事务这时也来操作这组数据,并且读取了前一个事务还未提交的数据,而前一个事务如果操作失败进行了回滚,后一个事务读取的就是错误数据,这样就造成了脏读。
Non-Repeatable Reads 不可重复读
一个事务多次读取同一数据,在该事务还未结束时,另一个事务也对该数据进行了操作,而且在第一个事务两次读取之间,第二个事务对数据进行了更新,那么第一个事务前后两次读取到的数据是不同的,这样就造成了不可重复读。
Phantom Reads 幻像读
第一个数据正在查询符合某一条件的数据,这时,另一个事务又插入了一条符合条件的数据,第一个事务在第二次查询符合同一条件的数据时,发现多了一条前一次查询时没有的数据,仿佛幻觉一样,这就是幻像读。
非重复读和幻象读的区别
- 非重复读是指同一查询在同一事务中多次进行,由于其他提交事务所做的修改或删除,每次返回不同的结果集,此时发生非重复读。
- 幻像读是指同一查询在同一事务中多次进行,由于其他提交事务所做的插入操作,每次返回不同的结果集,此时发生幻像读。
表面上看,区别就在于非重复读能看见其他事务提交的修改和删除,而幻像能看见其他事务提交的插入。
归纳:
名称 | 数据状态 | 实际行为 | 产生原因 |
---|---|---|---|
脏读 | 未提交 | 打算提交但是数据回滚了,读取了提交的数据 | 数据的读取 |
不可重复读 | 已提交 | 读取了修改前的数据 | 数据的修改 |
幻读 | 已提交 | 读取了插入前的数据 | 数据的插入 |
四种事务隔离级别
READ_UNCOMMITTED (读未提交)
这是事务最低的隔离级别,它允许另外一个事务可以看到这个事务未提交的数据。这种隔离级别会产生脏读,不可重复读和幻读。
READ_COMMITTED (读已提交)
保证一个事务修改的数据提交后才能被另外一个事务读取,另外一个事务不能读取该事务未提交的数据。这种事务隔离级别可以避免脏读出现,但是可能会出现不可重复读和幻像读。
REPEATABLE_READ (可重复读)
这种事务隔离级别可以防止脏读、不可重复读,但是可能出现幻像读。它除了保证一个事务不能读取另一个事务未提交的数据外,还保证了不可重复读。
SERIALIZABLE(串行化)
这是花费最高代价但是最可靠的事务隔离级别,事务被处理为顺序执行。除了防止脏读、不可重复读外,还避免了幻像读。
归纳:
名称 | 结果 | 脏读 | 不可重复读 | 幻读 |
---|---|---|---|---|
Read UnCommitted(读未提交) | 什么都不解决 | √ | √ | √ |
Read Committed(读提交) | 解决了脏读的问题 | - | √ | √ |
Repeatable Read(重复读) | (mysql的默认级别)解决了不可重复读 ) | - | - | √ |
Serializable(序列化) | 解决所有问题 | - | - | - |
Spring事务传播特性
比如 A 方法开启了事务,A 方法中调用了 B 方法,B 方法也开启了事务,那么这时候 A 和 B 之间的事务是独立进行的还是一起作用的?
Spring 在这方面为我们提供了管理的方法,也就是下面我们要讨论的 Spring 的事务传播机制。
七种传播特性
PROPAGATION_REQUIRED
默认的spring事务传播级别,使用该级别的特点是,如果上下文中已经存在事务,那么就加入到事务中执行,如果当前上下文中不存在事务,则新建事务执行。所以这个级别通常能满足处理大多数的业务场景。
PROPAGATION_SUPPORTS
从字面意思就知道,supports,支持,该传播级别的特点是,如果上下文存在事务,则支持事务加入事务,如果没有事务,则使用非事务的方式执行。所以说,并非所有的包在transactionTemplate.execute中的代码都会有事务支持。这个通常是用来处理那些并非原子性的非核心业务逻辑操作。应用场景较少。
PROPAGATION_MANDATORY
该级别的事务要求上下文中必须要存在事务,否则就会抛出异常!配置该方式的传播级别是有效的控制上下文调用代码遗漏添加事务控制的保证手段。比如一段代码不能单独被调用执行,但是一旦被调用,就必须有事务包含的情况,就可以使用这个传播级别。
PROPAGATION_REQUIRES_NEW
从字面即可知道,new,每次都要一个新事务,该传播级别的特点是,每次都会新建一个事务,并且同时将上下文中的事务挂起,执行当前新建事务完成以后,上下文事务恢复再执行。
这是一个很有用的传播级别,举一个应用场景:现在有一个发送100个红包的操作,在发送之前,要做一些系统的初始化、验证、数据记录操作,然后发送100封红包,然后再记录发送日志,发送日志要求100%的准确,如果日志不准确,那么整个父事务逻辑需要回滚。
怎么处理整个业务需求呢?就是通过这个PROPAGATION_REQUIRES_NEW 级别的事务传播控制就可以完成。发送红包的子事务不会直接影响到父事务的提交和回滚。
PROPAGATION_NOT_SUPPORTED
这个也可以从字面得知,not supported ,不支持,当前级别的特点就是上下文中存在事务,则挂起事务,执行当前逻辑,结束后恢复上下文的事务。
这个级别有什么好处?可以帮助你将事务极可能的缩小。我们知道一个事务越大,它存在的风险也就越多。所以在处理事务的过程中,要保证尽可能的缩小范围。比如一段代码,是每次逻辑操作都必须调用的,比如循环1000次的某个非核心业务逻辑操作。这样的代码如果包在事务中,势必造成事务太大,导致出现一些难以考虑周全的异常情况。所以这个事务这个级别的传播级别就派上用场了。用当前级别的事务模板抱起来就可以了。
PROPAGATION_NEVER
该事务更严格,上面一个事务传播级别只是不支持而已,有事务就挂起,而PROPAGATION_NEVER传播级别要求上下文中不能存在事务,一旦有事务,就抛出runtime异常,强制停止执行!这个级别上辈子跟事务有仇。
PROPAGATION_NESTED
字面也可知道,nested,嵌套级别事务。该传播级别特征是,如果上下文中存在事务,则嵌套事务执行,如果不存在事务,则新建事务。
嵌套事务和事务挂起
嵌套事务
什么是嵌套事务呢?
嵌套是子事务套在父事务中执行,子事务是父事务的一部分,在进入子事务之前,父事务建立一个回滚点,叫save point,然后执行子事务,这个子事务的执行也算是父事务的一部分,然后子事务执行结束,父事务继续执行。重点就在于那个save point。看几个问题就明了了:
1、如果子事务回滚,会发生什么?
父事务会回滚到进入子事务前建立的save point,然后尝试其他的事务或者其他的业务逻辑,父事务之前的操作不会受到影响,更不会自动回滚。
2、如果父事务回滚,会发生什么?
父事务回滚,子事务也会跟着回滚!为什么呢,因为父事务结束之前,子事务是不会提交的,我们说子事务是父事务的一部分,正是这个道理。那么:
3、事务的提交,是什么情况?
是父事务先提交,然后子事务提交,还是子事务先提交,父事务再提交?答案是第二种情况,还是那句话,子事务是父事务的一部分,由父事务统一提交。
事务挂起
实际上Spring在实现的时候,是通过对应的bind 和 unbind 操作,是在ThreadLocal 对象里,将资源对象绑定或移出当前线程对应的 resources 来实现的。
NESTED 的特性,本质上还是同一个事务的不同保存点,如果涉及到外层事务回滚,则内层的也将会被回滚;
REQUIRED_NEW 的实现对应的是一个新的事务,拿到的是新的资源,所以外层事务回滚时,不影响内层事务。
以上是事务的7个传播级别,在日常应用中,通常可以满足各种业务需求,但是除了传播级别,在读取数据库的过程中,如果两个事务并发执行,那么彼此之间的数据是如何影响的呢?————请看上面的隔离级别