01.传播与隔离

# 事务的传播级别

是否需要创建事务,是由事务传播行为控制的。读数据不需要或只为其指定只读事务,而数据的插入、修改、删除就需要进行事务管理了,这是由事务的隔离级别控制的。事务具有 7 个传播级别和 4 个隔离级别,传播级别定义的是事务创建的时机,隔离级别定义的是对并发事务数据读取的控制。所谓事务的传播行为是指,如果在开始当前事务之前,一个事务上下文已经存在,此时有若干选项可以指定一个事务性方法的执行行为。在 TransactionDefinition 定义中包括了如下几个表示传播行为的常量:

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

  • PROPAGATION_REQUIRES_NEW:创建一个新的事务,如果当前存在事务,则把当前事务挂起。从字面即可知道,new,每次都要一个新事务,该传播级别的特点是:每次都会新建一个事务,并且同时将上下文中的事务挂起,执行当前新建事务完成以后,上下文事务恢复再执行。那么当执行到 ServiceB.methodB 的时候,ServiceA.methodA 所在的事务就会挂起,ServiceB.methodB 会起一个新的事务,等待 ServiceB.methodB 的事务完成以后, 他才继续执行。他与 PROPAGATION_REQUIRED 的事务区别在于事务的回滚程度了。因为 ServiceB.methodB 是新起一个事务,那么就是存在两个不同的事务。如果 ServiceB.methodB 已经提交,那么 ServiceA.methodA 失败回滚,ServiceB.methodB 是不会回滚的。如果 ServiceB.methodB 失败回滚, 如果他抛出的异常被 ServiceA.methodA 捕获,ServiceA.methodA 事务仍然可能提交。这是一个很有用的传播级别,举一个应用场景:现在有一个发送 100 个红包的操作,在发送之前,要做一些系统的初始化、验证、数据记录操作,然后发送 100 封红包,然后再记录发送日志,发送日志要求 100%的准确,如果日志不准确,那么整个父事务逻辑需要回滚。怎么处理整个业务需求呢?就是通过这个 PROPAGATION_REQUIRES_NEW 级别的事务传播控制就可以完成。发送红包的子事务不会直接影响到父事务的提交和回滚。

  • PROPAGATION_SUPPORTS:如果当前存在事务,则加入该事务;如果当前没有事务,则以非事务的方式继续运行。从字面意思就知道,supports,支持,该传播级别的特点是:如果上下文存在事务,则支持事务加入事务,如果没有事务,则使用非事务的方式执行。所以说,并非所有的包含在 TransactionTemplate.execute 方法中的代码都会有事务支持。这个通常是用来处理那些并非原子性的非核心业务逻辑操作。应用场景较少。

  • PROPAGATION_NOT_SUPPORTED:以非事务方式运行,如果当前存在事务,则把当前事务挂起。这个也可以从字面得知,not supported,不支持,当前级别的特点是:若上下文中存在事务,则挂起事务,执行当前逻辑,结束后恢复上下文的事务。这个级别有什么好处?可以帮助你将事务尽可能的缩小。我们知道一个事务越大,它存在的风险也就越多。所以在处理事务的过程中,要保证尽可能的缩小范围。比如一段代码,是每次逻辑操作都必须调用的,比如循环 1000 次的某个非核心业务逻辑操作。这样的代码如果包在事务中,势必造成事务太大,导致出现一些难以考虑周全的异常情况,所以事务的这个传播级别就派上用场了。用当前级别的事务模板包含起来就可以了。

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

  • PROPAGATION_MANDATORY:如果当前存在事务,则加入该事务;如果当前没有事务,则抛出异常。该级别的事务要求上下文中必须要存在事务,否则就会抛出异常!配置该方式的传播级别是有效的控制上下文调用代码遗漏添加事务控制的保证手段。比如一段代码不能单独被调用执行,但是一旦被调用,就必须有事务包含的情况,就可以使用这个传播级别。

  • PROPAGATION_NESTED:如果当前存在事务,则创建一个事务作为当前事务的嵌套事务来运行;如果当前没有事务,则该取值等价于 PROPAGATION_REQUIRED。如果当前存在事务,则在嵌套事务内执行。如果当前没有事务,则进行与 PROPAGATION_REQUIRED 类似的操作。从字面也可知道,nested,嵌套级别事务。该传播级别的特征是:如果上下文中存在事务,则嵌套事务执行,如果不存在事务,则新建事务。

这里需要指出的是,前面的六种事务传播行为是 Spring 从 EJB 中引入的,他们共享相同的概念。而 PROPAGATION_NESTED 是 Spring 所特有的。以 PROPAGATION_NESTED 启动的事务内嵌于外部事务中(如果存在外部事务的话),此时,内嵌事务并不是一个独立的事务,它依赖于外部事务的存在,只有通过外部的事务提交,才能引起内部事务的提交,嵌套的子事务不能单独提交。如果熟悉 JDBC 中的保存点(SavePoint)的概念,那嵌套事务就很容易理解了,其实嵌套的子事务就是保存点的一个应用,一个事务中可以包括多个保存点,每一个嵌套子事务。另外,外部事务的回滚也会导致嵌套子事务的回滚。

那么什么是嵌套事务呢?很多人都不理解,我看过一些博客,都是有些理解偏差。嵌套是子事务嵌套在父事务中执行,子事务是父事务的一部分,在进入子事务之前,父事务建立一个回滚点,叫 save point,然后执行子事务,这个子事务的执行也算是父事务的一部分,然后子事务执行结束,父事务继续执行。重点就在于那个 save point。看几个问题就明了了:

  • 如果子事务回滚,会发生什么:父事务会回滚到进入子事务前建立的 save point,然后尝试其他的事务或者其他的业务逻辑,父事务之前的操作不会受到影响,更不会自动回滚。
  • 如果父事务回滚,会发生什么:父事务回滚,子事务也会跟着回滚!为什么呢,因为父事务结束之前,子事务是不会提交的,我们说子事务是父事务的一部分,正是这个道理。
  • 事务的提交,是什么情况:是父事务先提交,然后子事务提交,还是子事务先提交,父事务再提交?答案是第二种情况,还是那句话,子事务是父事务的一部分,由父事务统一提交。

我们可以看 org.springframework.transaction.annotation.Propagation 枚举类中定义了 6 个表示传播行为的枚举值:

public enum Propagation {
    REQUIRED(0),
    SUPPORTS(1),
    MANDATORY(2),
    REQUIRES_NEW(3),
    NOT_SUPPORTED(4),
    NEVER(5),
    NESTED(6);
}

事务的隔离级别

隔离级别是指若干个并发的事务之间的隔离程度,与我们开发时候主要相关的场景包括:脏读取、重复读、幻读。

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

  • READ_COMMITTED:大多数主流数据库的默认事务等级,保证了一个事务不会读到另一个并行事务已修改但未提交的数据,避免了“脏读取”。该级别适用于大多数系统。所谓的脏读,其实就是读到了别的事务回滚前的脏数据。比如事务 B 执行过程中修改了数据 X,在未提交前,事务 A 读取了 X,而事务 B 却回滚了,这样事务 A 就形成了脏读。

  • REPEATABLE_READ:保证了一个事务不会修改已经由另一个事务读取但未提交(回滚)的数据。避免了“脏读取”和“不可重复读取”的情况,但是带来了更多的性能损失。不可重复读字面含义已经很明了了,比如事务 A 首先读取了一条数据,然后执行逻辑的时候,事务 B 将这条数据改变了,然后事务 A 再次读取的时候,发现数据不匹配了,就是所谓的不可重复读了。

  • SERIALIZABLE:最严格的级别,事务串行执行,资源消耗最大。小的时候数手指,第一次数十 10 个,第二次数是 11 个,怎么回事?产生幻觉了?幻读也是这样子,事务 A 首先根据条件索引得到 10 条数据,然后事务 B 改变了数据库一条数据,导致也符合事务 A 当时的搜索条件,这样事务 A 再次搜索发现有 11 条数据了,就产生了幻读。

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

脏读 不可重复读 幻读
SERIALIZABLE 不会 不会 不会
REPEATABLE_READ 不会 不会
READ_COMMITTED 不会
READ_UNCOMMITTED

所以最安全的,是 Serializable,但是伴随而来也是高昂的性能开销。我们可以看 org.springframework.transaction.annotation.Isolation 枚举类中定义了五个表示隔离级别的值:

public enum Isolation {
    DEFAULT(-1),
    READ_UNCOMMITTED(1),
    READ_COMMITTED(2),
    REPEATABLE_READ(4),
    SERIALIZABLE(8);
}

另外,事务常用的两个属性:

  • readonly,设置事务为只读以提升性能;
  • timeout,设置事务的超时时间,一般用于防止大事务的发生。
下一页