一文全面读懂分布式事务

Posted 码上观世界

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了一文全面读懂分布式事务相关的知识,希望对你有一定的参考价值。

事务是一组操作序列的集合。作为并发控制的单元,事务要么完成,要么回滚到初始状态,不允许存在中间状态。

其处理逻辑步骤可以表示为:

BEGIN   

     //do something

COMMIT   

     //do commit

ROLLBACK    

    //do rollback

只有当成功地执行完commit动作,该事务才正常结束,否则回滚到初始状态。

本文从经典的数据库事务理论讲起。


事务的特性(ACID

  • 原子性(Atomicity) 事务是数据库的逻辑工作单位,事务中包括的诸操作要么全做,要么全不做。

  • 一致性(Consistency) 事务执行的结果必须是使数据库从一个一致性状态变到另一个一致性状态。一致性与原子性是密切相关的。

  • 隔离性(Isolation) 一个事务的执行不能被其他事务干扰。

  • 持久性(Durability) 一个事务一旦提交,它对数据库中数据的改变就应该是永久性的。


常见并发问题

实际环境中因为要考虑执行效率,很难做到完全的隔离性。因此当多个事务并发执行会导致如下几类问题:

  • 脏读 当一个事务正在访问数据,并且对数据进行了修改,而这种修改还没有提交到数据库中,这时,另外一个事务也访问这个数据,然后使用了这个数据,如果前一个事务出现回滚,则后一个事务读取的数据即为脏数据。

  • 不可重复读 在一个事务内,多次读同一数据。在这个事务还没有结束时,另外一个事务也访问该同一数据。那么,在第一个事务中的两次读数据之间,由于第二个事务的修改,那么第一个事务两次读到的的数据可能是不一样的。这样在一个事务内两次读到的数据是不一样的,因此称为是不可重复读。

  • 幻读 当事务不是独立执行时发生的一种现象,例如第一个事务对一个表中的数据进行了修改,这种修改涉及到表中的全部数据行。 同时,第二个事务也修改这个表中的数据,这种修改是向表中插入一行新数据。那么,以后就会发生操作第一个事务的用户发现表中还有没有修改的数据行,就好象发生了幻觉一样。

不可重复读和幻读的区别

简单来说,不可重复读是由于数据修改引起的,幻读是由数据插入或者删除引起的。 不可重复读,是指在数据库访问中,一个事务范围内两个相同的查询却返回了不同数据。这是由于查询时系统中其他事务修改的提交而引起的。比如事务T1读取某一数据,事务T2读取并修改了该数据,T1为了对读取值进行检验而再次读取该数据,便得到了不同的结果。 

一种更易理解的说法是:在一个事务内,多次读同一个数据。在这个事务还没有结束时,另一个事务也访问该同一数据。那么,在第一个事务的两次读数据之间。由于第二个事务的修改,那么第一个事务读到的数据可能不一样,这样就发生了在一个事务内两次读到的数据是不一样的,因此称为不可重复读,即原始读取不可重复。 所谓幻读,是指事务A读取与搜索条件相匹配的若干行。事务B以插入或删除行等方式来修改事务A的结果集,然后再提交。


事务隔离级别

为了解决以上并发问题,SQL标准定义了4类隔离级别,包括了一些具体规则,用来限定事务内外的哪些改变是可见的,哪些是不可见的。低级别的隔离级一般支持更高的并发处理,并拥有更低的系统开销。

  • READ UNCOMMITTED(读未提交数据) 一个事务在执行过程中可以看到其他事务没有提交的新插入的记录,而且还能看到其他事务没有提交的对已有记录的更新。

  • READ COMMITTED(读已提交数据)一个事务在执行过程中可以看到其他事务已经提交的新插入的记录,而且还能看到其他事务已经提交的对已有记录的更新。

  • REPEATABLE(可重复读) READ(可重复读)一个事务在执行过程中可以看到其他事务已经提交的新增(删)的记录,但是不能看到其他事务对已有记录的更新。

  • SERIALIZABLE(串行化) 一个事务在执行过程中完全看不到其他事务对数据库所做的更新。当两个事务同时操作数据库中相同数据时,如果第一个事务已经在访问该数据,第二个事务只能停下来等待,必须等到第一个事务结束后才能恢复运行。因此这两个事务实际上是串行化方式运行。

依据上述隔离规则,可以列出隔离级别与上述问题的解决与否对应关系:

隔离级别

脏读

不可重复读

幻读

READ UNCOMMITTED

不可避免

不可避免

不可避免

READ COMMITTED

不可避免

可避免

不可避免

REPEATABLE READ

可避免

可避免

不可避免

SERIALIZABLE

可避免

可避免

可避免

比如,数据库事务隔离级别设为READ COMMITTED,虽然可以解决脏读问题,但对不可重复读和幻读无能为力。


事务隔离级别的锁机制

实际上上述4种隔离级别对应了锁的实现粒度:

  • READ UNCOMMITTED 不加锁或者只加共享锁

  • READ COMMITTED 对行记录加排他锁

  • REPEATABLE READ 对符合条件的记录加区间锁;

  • SERIALIZABLE 对整个表加锁

锁的粒度对数据库的性能影响巨大,所以不同的数据库实现使用的默认事务隔离级别也不一样,如Oracle默认Read Commited,mysql InnoDB为Repeatable Read。

关于锁的详细介绍,参见『浅入浅出』MySQL 和 InnoDB


事务传播行为

事务传播行为是Spring独有的事务行为控制机制,主要规定事务嵌套调用时的行为。 Spring定义了7种传播行为

public enum Propagation {

    REQUIRED(0),   

    SUPPORTS(1),   

    MANDATORY(2),    

    REQUIRES_NEW(3),    

    NOT_SUPPORTED(4),    

    NEVER(5),    

    NESTED(6

}

  • REQUIRED 当前方法必须在一个具有事务的上下文中运行,如有客户端有事务在进行,那么被调用端将在该事务中运行,否则的话重新开启一个事务。(如果被调用端发生异常,那么调用端和被调用端事务都将回滚)

  • SUPPORTS 当前方法不必需要具有一个事务上下文,但是如果有一个事务的话,它也可以在这个事务中运行

  • MANDATORY 当前方法必须在一个事务中运行,如果没有事务,将抛出异常

  • REQUIRES_NEW 当前方法必须运行在它自己的事务中。一个新的事务将启动,而且如果有一个现有的事务在运行的话,则这个方法将在运行期被挂起,直到新的事务提交或者回滚才恢复执行。

  • NOT_SUPPORTED 该方法不应该在一个事务中运行。如果有一个事务正在运行,他将在运行期被挂起,直到这个事务提交或者回滚才恢复执行

  • NEVER 当前方法不应该在一个事务中运行,如果存在一个事务,则抛出异常

  • NESTED 如果当前方法正有一个事务在运行中,则该方法应该运行在一个嵌套事务中,被嵌套的事务可以独立于被封装的事务中进行提交或者回滚。如果封装事务存在,并且外层事务抛出异常回滚,那么内层事务必须回滚,反之,内层事务并不影响外层事务。如果封装事务不存在,则同PROPAGATION_REQUIRED的一样


事务传播使用举例

以spring-data jpa实现为例,通过几种特殊场景说明下两种传播机制的含义:

JobDraftInfoRepository和JobInfoRepository属于同一数据库的两张mysql表,其中commandType 属性不可为空。

1. 演示场景一:testTransactionPropagation调用两个分别设置为REQUIRED的事务方法。

一文全面读懂分布式事务


根据官方定义,此时外围调用方法没有开启事务,则被调用方开启新的事务,相互独立执行,但是jobDraftInfoRepository.save因为调用失败,本事务回滚,前者成功保存。

2. 演示场景二:外围调用方法开启事务,内部被调用方不变。

一文全面读懂分布式事务

根据官方定义,此时被调用方开启的事务跟外围事务合并,即在同一个事务中运行。因为jobDraftInfoRepository.save调用失败导致整个方法回滚,两个本地事务都保存失败。

3. 演示场景三:外围调用方法开启事务,内部被调用方事务传播行为都调整为:REQUIRES_NEW

一文全面读懂分布式事务

根据官方定义,内部方法会单独开启独立事务,与外部方法事务独立,内部方法之间、内部方法和外部方法事务均相互独立,互不干扰。jobInfoRepository.save保存成功,jobDraftInfoRepository.save保存失败。

使用JPA事务注意事项

  • JPA默认每个方法在事务中执行,Transactional 的属性 readOnly默认为false,propagation默认为REQUIRED。在执行读取操作时,避免使用 @Transactional 注释。因为事务会在不必要的情况下启动。根据使用的数据库,这会引起不必要的共享锁,可能会使数据库中出现死锁的情况。此外,启动和停止事务将消耗不必要的处理时间和资源。而只读标志的事务基本上毫无用处,在大多数情况下会被忽略。但如果坚持使用,请记得将传播模式设置为 SUPPORTS,这样就不会启动事务.

    @Transactional(readOnly = true, propagation=Propagation.SUPPORTS)

  • 使用 REQUIRES_NEW 事务属性时,如果存在现有事务上下文,当前的事务会被挂起并启动一个新事务。方法结束后,新的事务被提交,原来的事务继续执行。由于这种行为,只有在被调用方法中的数据库操作需要保存到数据库中,而不管覆盖事务的结果如何时,才应该使用 REQUIRES_NEW 事务属性。比如,假设尝试的所有股票交易都必须被记录在一个审计数据库中。出于验证错误、资金不足或其他原因,不管交易是否失败,这条信息都需要被持久化。如果没有对审计方法使用 REQUIRES_NEW 属性,审计记录就会连同尝试执行的交易一起回滚。使用 REQUIRES_NEW 属性可以确保不管初始事务的结果如何,审计数据都会被保存。这里要注意的一点是,要始终使用 MANDATORY 或 REQUIRED 属性,而不是 REQUIRES_NEW,除非您有足够的理由来使用它,类似审计示例中的那些理由。

  • 受控异常(checked exception)不会导致事务回滚,非受控异常会!。下面实例中的jobDraftInfoRepository.save会成功保存。

一文全面读懂分布式事务

分布式事务

分布式事务就是指事务的参与者、支持事务的服务器、资源服务器以及事务管理器分别位于不同的分布式系统的不同节点之上。本文上部分理论和实践都是基于本地事务,然而实际中一个事务操作往往涉及到不同节点的数据源。随着微服务的普及,这种现象越来越常见,所以分布式事务是任何微服务架构绕不过去的技术难点和障碍。下面介绍几种常见的分布式事务解决方案实现原理。


1. X/Open XA 协议

最早的分布式事务模型是 X/Open 国际联盟提出的 X/Open Distributed Transaction Processing(DTP)模型,也就是大家常说的 X/Open XA 协议,简称XA 协议。

DTP 模型中包含一个全局事务管理器(TM,Transaction Manager)和多个资源管理器(RM,Resource Manager)。

全局事务管理器负责管理全局事务状态与参与的资源,协同资源一起提交或回滚;

资源管理器则负责具体的资源操作。

XA 协议描述了 TM 与 RM 之间的接口,允许多个资源在同一分布式事务中访问。

XA 协议使用 2PC(Two Phase Commit,两阶段提交)原子提交协议来保证分布式事务原子性。

两阶段提交是指将提交过程分为两个阶段,即准备阶段(投票阶段)和提交阶段(执行阶段):

一文全面读懂分布式事务

准备阶段:

TM 向每个 RM 发送准备消息。如果 RM 的本地事务操作执行成功,则返回成功;如果 RM 的本地事务操作执行失败,则返回失败。

提交阶段

如果 TM 收到了所有 RM 回复的成功消息,则向每个 RM 发送提交消息;否则发送回滚消息;RM 根据 TM 的指令执行提交或者回滚本地事务操作,释放所有事务处理过程中使用的锁资源。

因为XA 协议严格保障事务 ACID 特性在事务执行过程中所有的资源都被锁定,因此XA事务并发性能较低,适用于并发量较低的场景。


2. 基于消息中间件的分布式事务

所谓的消息事务就是基于消息中间件的两阶段提交,本质上是对消息中间件的一种特殊利用,它是将本地事务和发消息放在了一个分布式事务里,保证要么本地操作成功成功并且对外发消息成功,要么两者都失败,具体原理如下:

具体流程为:

1. A系统向消息中间件发送预备消息

    1.1 消息中间件保存预备消息

    1.2 消息中间件返回确认消息

如果步骤1失败,事务结束;

2. A系统执行本地事务,如果步骤2失败,事务结束;

3. A系统向消息中间件发送提交消息

    3.1 消息中间件保存提交消息

    3.2 消息中间件向B系统发送消息

        3.2.1 B系统执行本地消息

步骤3,3.1,3.2不是必须的,如果步骤3发消息失败,消息中间价同样可以通过A系统的回调接口探知其本地事务的执行状况,

如果A本地事务执行成功,就可以直接向B系统发消息。同样的道理,如果3.2发消息失败,消息会重试直到成功。

4. 消息中间件回调A系统

消息方案从本质上讲是将分布式事务转换为两个本地事务,然后依靠下游业务的重试机制达到最终一致性。基于消息的最终一致性方案对应用侵入性较高,应用需要进行大量业务改造,成本较高。这种模式在大型互联网行业应用较多,适合于高并发且对即时一致性有一定容忍度的场景。


3. TCC(Try-Confirm-Cancel)

TCC(Try-Confirm-Cancel)分布式事务模型相对于 XA 等传统模型,其特征在于它不依赖资源管理器(RM)对分布式事务的支持,而是通过对业务逻辑的分解来实现分布式事务。

TCC 模型认为对于业务系统中一个特定的业务逻辑,其对外提供服务时,必须接受一些不确定性,即对业务逻辑初步操作的调用仅是一个临时性操作,调用它的主业务服务保留了后续的取消权。如果主业务服务认为全局事务应该回滚,它会要求取消之前的临时性操作,这就对应从业务服务的取消操作。而当主业务服务认为全局事务应该提交时,它会放弃之前临时性操作的取消权,这对应从业务服务的确认操作。每一个初步操作,最终都会被确认或取消。

因此,针对一个具体的业务服务,TCC 分布式事务模型需要业务系统提供三段业务逻辑:

  • 初步操作 Try:完成所有业务检查,预留必须的业务资源。

  • 确认操作 Confirm:真正执行的业务逻辑,不作任何业务检查,只使用 Try 阶段预留的业务资源。因此,只要 Try 操作成功,Confirm 必须能成功。另外,Confirm 操作需满足幂等性,保证一笔分布式事务有且只能成功一次。

  • 取消操作 Cancel:释放 Try 阶段预留的业务资源。同样的,Cancel 操作也需要满足幂等性。

一个完整的 TCC 分布式事务流程如下:

1. 主业务服务首先开启本地事务;

2. 主业务服务向业务活动管理器申请启动分布式事务主业务活动;

3. 然后针对要调用的从业务服务,主业务活动先向业务活动管理器注册从业务活动,然后调用从业务服务的 Try 接口;

4. 当所有从业务服务的 Try 接口调用成功,主业务服务提交本地事务;若调用失败,主业务服务回滚本地事务;

5. 若主业务服务提交本地事务,则 TCC 模型分别调用所有从业务服务的 Confirm 接口;若主业务服务回滚本地事务,则分别调用 Cancel 接口;

6. 所有从业务服务的 Confirm 或 Cancel 操作完成后,全局事务结束。

TCC方案让应用自己定义数据库操作的粒度,使得降低锁冲突、提高吞吐量成为可能,但同时需要根据不同场景编写自己的TCC逻辑,开发成本和代码维护都是一大问题。


4. SAGA模型

Saga事务模型又叫做长时间运行的事务(Long-running-transaction), 它是由普林斯顿大学的H.Garcia-Molina等人提出,它描述的是另外一种在没有两阶段提交的的情况下解决分布式系统中复杂的业务事务问题。其核心思想就是拆分分布式系统中的长事务为多个短事务,或者叫多个本地事务,然后由 Sagas 工作流引擎负责协调,如果整个流程正常结束,那么就算是业务成功完成,如果在这过程中实现失败,那么Sagas工作流引擎就会以相反的顺序调用补偿操作,重新进行业务回滚。

更多关于SAGA的信息,可以参考论文:https://www.cs.cornell.edu/andru/cs711/2002fa/reading/sagas.pdf


分布式系统设计理论

通过上面分布式事务实现方案我们已经发现,完全基于传统的事务特性要求,很难设计符合现代大型应用场景的分布式事务方案,必须在几个特性之间做出平衡和取舍。比如基于消息的分布式事务实现方案就在一致性方面做了容忍。

关于这方面的指导理论,主要有两套:

1. CAP定理

CAP定理是由加州大学伯克利分校Eric Brewer教授提出来的,他指出WEB服务无法同时满足一下3个属性:

  • Consistency(一致性):指数据在多个副本之间能够保持一致的特性(严格的一致性)

  • Availability(可用性)指系统提供的服务必须一直处于可用的状态,每次请求都能获取到非错的响应(不保证获取的数据为最新数据)

  • Partition tolerance(分区容错性)布式系统在遇到任何网络分区故障的时候,仍然能够对外提供满足一致性和可用性的服务,除非整个网络环境都发生了故障


2. BASE理论

在分布式系统中,我们往往追求的是可用性,它的重要程序比一致性要高,那么如何实现高可用性呢? 前人已经给我们提出来了另外一个理论,就是BASE理论,它是用来对CAP定理进行进一步扩充的。

BASE理论指的是:

  • Basically Available(基本可用)

  • Soft state(软状态)

  • Eventually consistent(最终一致性)

BASE理论是对CAP中的一致性和可用性进行一个权衡的结果,理论的核心思想就是:我们无法做到强一致,但每个应用都可以根据自身的业务特点,采用适当的方式来使系统达到最终一致性(Eventual consistency)。


写在最后

本文回顾了经典事务理论,并通过实践的方式介绍了spring中事务传播方式和常见注意事项,然后介绍了分布式事务常见实现方案和分布式系统设计的一些理论。从中我们了解到在实际系统设计中不得不在CAP中做出权衡,而分布式一致性是其中重点之一。作为结尾,暂且提出一个小问题:

1. CAP和ACID中的C是一回事吗?

2. 区块链中达成共识问题采用的一致性又是怎么回事?

关于分布式系统一致性更多问题,且听下回分解。


参考链接

  1. 了解事务陷阱

  2. Innodb中的事务隔离级别和锁的关系

  3. 数据库事务隔离级别

  4. Spring事务传播行为

  5. Transaction Management

  6. 深入理解高并发下分布式事务的解决方案

  7. 分布式事务原理及解决方

  8. 分布式事务通用解决方案


以上是关于一文全面读懂分布式事务的主要内容,如果未能解决你的问题,请参考以下文章

一文读懂kafka的事务机制

一文让你搞懂分布式事务

一文解读分布式事务 (转)

一文看懂分布式事务

一文看懂分布式事务

一文搞懂MySQL XA如何实现分布式事务