分布式事务

55 阅读22分钟

0、写在最前面

目前大家开发时面临的大多数场景都是分布式的,而如何解决分布式事务一直分布式场景下一个讨论的话题,许多公司也有开源产品,本文希望通过介绍常用的分布式事务解决方案和实现细节,帮助读者了解相关知识。

1、分布式事务的由来

1.1、什么是事务?

事务(Transaction)通常的定义是:作为单个逻辑单元执行的一组操作,要么全部执行,要么全部不执行。它有四个明显的特征:原子性(Atomicity)、一致性(Correspondence)、隔离性(Isolation)、持久性(Durability),简称ACID【详情见附录】。结合一个简单的例子进行说明:一次用户的操作,涉及到多个不同数据表的数据变更,想要在一次操作内实现要么全部变更完成返回成功,要么全部不变更直接返回失败,如果执行到中途失败了需要将已执行的变更进行撤回,这就是一次典型的事务

分布式事务(Distributed Transaction)最早指的就是涉及多个资源的数据库事务,不过近年来随着SOA、微服务等概念的流行,分布式事务概念也有所泛化,根据上下文的不同分为系统事务(System Transaction)和业务事务(Business Transaction),前者多指数据库事务,而后者多指业务交易,需要关注的是如何保证交易的整体原子性和一致性,特点是时间跨度很长,比如餐厅线上预订,可能从用户点击预定到商户实际确认要一两天才能完成。不过无论哪一种事务,因为事务的持久性特征导致事务实现最终都需要依赖存储介质,因此在谈到事务时很难脱离数据库。

1.2、为什么会产生分布式事务?

产生分布式事务的原因大致可以分为两类,下面分别进行介绍。

1.2.1、服务的多节点

在SOA、微服务场景下,许多一开始大而全的服务都会业务领域进行拆分,这往往会导致原本在一个服务内就可以闭环完成的简单操作变成需要多个不同服务协作完成的复杂操作,而不同服务往往又会对应不同的数据资源,此场景下如果对操作有事务要求自然就产生了分布式事务的问题。

服务多节点图

如上图,一个应用被拆分为了服务A(RemoteServiceA)、服务B(RemoteServiceB)、服务C(RemoteServiceB),分别对应了数据库A、数据库B、数据库C。如下示例,如果服务B某个方法(updateAll)既需要更新本地数据还需要通过调用服务A和服务B提供的服务更新各自数据库数据时,方法如何保证事务性就成了一个分布式事务问题,我们没法简单的通过配置常用的事务管理器管理这种场景下的整体事务,因为传统的事务管理器控制域都是作用在本地的,不支持跨服务传递。

1.2.2、资源的多节点

当业务规模发展到一定程度,除了从业务领域角度进行纵向拆分外,往往还会考虑到数据的水平拆分,会把一些量大且重要的库按水平进行分表,而考虑到容灾和性能问题,还会进行分库分集群,这会导致原本在单个数据库内的简单数据操作变成了涉及多个数据库的跨库操作,此场景下如果对操作有事务要求自然就产生了分布式事务的问题。

多资源节点图

如上图,一个服务(LocalService)对应数据被分散存储到三个不同数据库A、数据库B、数据库C。如下示例,如果服务中某个方法(updateAll)既需要同时更新三个数据库的数据时,方法如何保证事务性就成了一个分布式事务问题,我们没法简单的通过配置常用的事务管理器管理这种场景下的整体事务,因为常用的事务管理器控制域都是作用在本地的,不支持跨数据库传递。

1.3、有哪些理论和问题?

一般来说分布式系统都脱离不开CAP理论【详情见附录】,该理论认为一个分布式系统中, 一致性(Consistency)、可用性(Availability)、分区容错性(Partition Tolerance),三者不可得兼。因此在设计分布式系统时,就存在一定的取舍,那么在解决分布式事务时,如果完全强调事务的ACID特征,按照理论就必然需要牺牲系统的可用性,但是可用性又是现在大型分布式系统的核心关注指标,这种矛盾就是分布式事目前面临的首要问题。

CAP理论和BASE理论关系图

基于这样的问题背景,BASE理论【详情见附录】后来被提出,其强调基本可用(Basically Available)、软状态(Soft State)和最终一致性(Eventually Consistent)三个特点。该理论是对CAP中一致性和可用性权衡的结果,其核心思想是即使系统无法做到数据的实时强一致性(Strong Consistency),但每个应用都可以根据自身的业务特点,采用适当的方式来使系统在经过一段时候后达到最终一致性,以此提供系统整体的可用性。这种理论一定程度上调和了事务在分布式场景下所面临的一致性与可用性之间的矛盾,也正因此涌现了许多基于最终一致性的分布式事务解决方案,下面会着重介绍几个常见的经典解决方案。

2、常见解决方案

2.1、你需要先了解这些概念

名词概念
事务管理器(Transaction Manager)事务管理器是一个独立的服务,用于控制分布式事务的生命周期,包括创建主事务记录、分支事务记录,并根据分布式事务的状态,调用参与者提交或回滚方法。
资源管理器(Resource Manager)任意类型的持久化数据存储,如常见的关系型数据库甚至消息队列。通常情况下,我们因为不直接操作存储资源,所以可以理解为数据源适配器。
事务协调器(Transaction Coordinator)事务协调器是一个独立的模块,用来控制事务的提交和回滚操作。事务协调器可嵌入在客户端中独立运行,也可部署在Server端作为独立服务。
全局事务(Global Transaction)全局事务是一个逻辑上的概念,他并不是具体执行的某段逻辑。一个全局事务包含了多个具体的分支事务,发起方通过修改全局事务的状态,控制分支事物的提交/回滚。
分支事务(Branch Transaction)分支事务是一个逻辑上的概念,一个分布式事务可能包含多个数据库本地事务,在分布式事务框架下,分支事务可能是一个分库上执行的 SQL 语句,或是一个自定义模式服务的调用。
事务发起方(Launcher)分布式事务的发起方负责启动分布式事务,通过调用参与者的服务,将参与者纳入到分布式事务当中,并决定整个分布式事务是提交还是回滚。一个分布式事务有且只能有一个发起方。
事务参与方(Participant)参与者提供分支事务服务。当一个参与者被发起方调用,则被纳入到该发起方启动的分布式事务中,成为该分布式事务的一个分支事务。一个分布式事务可以有多个参与者。

2.2、两阶段提交

两阶段提交(Two-Phase Commit,以下简称2PC)方案,通常也被称为一种协议,从字面意思理解就是事务分两个阶段进行提交,实际方案确实如此,整个方案在执行分布式事务的过程中被分为两个重要阶段:

  • 一阶段——准备阶段(Prepare Phase):事务发起方通过事务管理器发起一个分布式事务后,由事务协调器(事务管理器)通知各个事务参与方(资源管理器),给他们发送一条prepare指令,每个参与者要么返回失败,要么就进行执行本地事务(分支事务),写本地redo和undo日志,但是不进行事务的commit,到达一种临界状态,此时每个参与者持有当前本地事务涉及的锁资源不释放;
  • 二阶段——提交阶段(Commit Phase):事务协调器收集所有事务参与方在一阶段返回的信息,如果都成功则通知所有参与方进行事务提交(Commit),如果有返回失败或超时,则通知所有参与方进行事务回滚(Rollback)。参与方根据协调者的指令执行提交或者回滚操作,释放所有事务处理过程中使用的锁资源。

如下图是2PC方案的简单流程示意图,不难发现事务管理器直接与资源管理器进行交互,中间不需要业务逻辑的介入。

2PC方案流程示意图

针对上面的同步阻塞和单点故障问题,后来又演变除了3PC方案,主要是将原来的准备阶段拆解成了两个阶段减少阻塞问题,同时引入超时机制避免单点故障,但是仍然没有解决数据不一致问题。

2.2.1、优点

  • 数据库层面(资源管理器)层面控制分布式事务,业务层面无感知或低侵入
  • 强一致性

2.2.2、缺陷

  • 同步阻塞:如上图,整个执行过程中,所有参与节点都是事务阻塞型的。当参与者占有公共资源时,其他第三方节点访问公共资源不得不处于阻塞状态。
  • 单点故障:由于协调者的重要性,一旦协调者发生故障。参与者会一直阻塞下去。尤其在第二阶段,协调者发生故障,那么所有的参与者还都处于锁定事务资源的状态中,而无法继续完成事务操作。(如果是协调者挂掉,可以重新选举一个协调者,但是无法解决因为协调者宕机导致的参与者处于阻塞状态的问题)
  • 数据不一致:在二阶段提交的阶段二中,当协调者向参与者发送commit请求之后,发生了局部网络异常或者在发送commit请求过程中协调者发生了故障,这回导致只有一部分参与者接受到了commit请求。而在这部分参与者接到commit请求之后就会执行commit操作。但是其他部分未接到commit请求的机器则无法执行事务提交。于是整个分布式系统便出现了数据部一致性的现象。
  • 二阶段无法解决的问题:协调者发出commit消息之后宕机,而唯一接收到这条消息的参与者同时也宕机了。那么即使协调者通过选举协议产生了新的协调者,这条事务的状态也是不确定的,没人知道事务是否被已经提交。

1、补偿(Compensate)可以理解为“逻辑撤销”,是一次真实的操作,用于撤销之前操作带来的影响,与回滚不同,它是一种当工作流中发生违反规则的情况时,取消已成功完成和提交的事务所产生的影响的有效方法。而回滚则是指取消当前未完成或未提交事务;

举例:下单支付过程中,如果在支付中引发异常,回滚可恢复一致的状态,账户金额不会有变化;如果在支付成功后,因为业务原因不能正常履约,此时需要做退款,这就叫补偿;

2、Cancel和Compensate从语义角度是撤消了事务的行为,但未必能将数据完全返回到执行事务前的状态。(例如,如果事务触发消息推送, 则可能无法撤消此操作,但是可以通过再发送一个消息用于补偿)

3、在分布式场景下,因为延时等网络问题,可能会导致同一个事务内,Cancel请求比Try请求先到达事务参与方,称为空回滚问题,此时如果返回成功,那么当Try后到达后将会导致资源一直被锁定无法释放,称为事务悬挂问题,针对这个问题实现,框架一般通过默认生成一个回滚记录来保证Try请求后进来也不会造成资源锁定

2.2.3、与XA的关系

XA协议【详情见附录】是X/OPEN 提出的分布式事务处理规范,采用两阶段提交方式来管理分布式事务。

2.3、TCC

Try-Confirm-Cancel(以下简称TCC)方案,和严格遵守ACID的2PC方案不同,主要实现的是最终一致性,且主要面向业务层面的事务控制,通过对业务逻辑(业务系统自己实现)的调度来实现分布式事务,整个方案有三段业务逻辑,分别为尝试操作(Try)、确认操作(Confirm)、取消操作(Cancel),按执行步骤分为两个阶段:

  • 一阶段——尝试操作(Try):事务发起方在业务代码中,显示的对所有参与事务的参与方提供的Try操作,进行业务逻辑前的业务检查(一致性),完成业务资源的预留和锁定(准隔离性);
  • 二阶段——确认操作(Confirm):如果一阶段执行成功,不做业务检查,执行业务逻辑,使用Try阶段预留的业务资源;
  • 二阶段——取消操作(Cancel):如果流程中执行失败,执行补偿逻辑,对Try阶段预留的业务资源进行释放;

还有一种常见的变种方案,方案中有两段业务逻辑,分别为执行操作(Do)和补偿操作(Compensate),按执行步骤同样分为两个阶段:

  • 一阶段——执行操作(Do):直接执行业务校验和业务逻辑,并对结果持久化,业务操作结果外部可见;
  • 二阶段——补偿操作(Compensate):逆向逻辑,一阶段操作失败后的抵消和补偿,如果一阶段成功则不需要执行二阶段;

两种方案的区别在于业务逻辑的粒度,Do操作相当于Try + Confirm,如果Try出现失败的情况很多则建议使用标准的TCC实现,如果Try很少出现失败则可以使用变种方案

如下图是TCC方案的简单流程示意图,可以发现事务参与方的Try方法是有发起方在业务代码中进行调用的,而事务参与方的Confirm和Cancel方法的调用则是由事务协调器来调用的。

TCC方案流程示意图

举例:下单支付过程中,如果在支付中引发异常,回滚可恢复一致的状态,账户金额不会有变化;如果在支付成功后,因为业务原因不能正常履约,此时需要做退款,这就叫补偿;

2、Cancel和Compensate从语义角度是撤消了事务的行为,但未必能将数据完全返回到执行事务前的状态。(例如,如果事务触发消息推送, 则可能无法撤消此操作,但是可以通过再发送一个消息用于补偿)

3、在分布式场景下,因为延时等网络问题,可能会导致同一个事务内,Cancel请求比Try请求先到达事务参与方,称为空回滚问题,此时如果返回成功,那么当Try后到达后将会导致资源一直被锁定无法释放,称为事务悬挂问题,针对这个问题实现,框架一般通过默认生成一个回滚记录来保证Try请求后进来也不会造成资源锁定

2.3.1、优点

  • 保证隔离性
  • 方案灵活,基本能覆盖所有业务场景,可操作性强
  • 整体可用性高

2.3.2、缺陷

  • 成本高,如上图不难发现所有事务参与方都需要实现Try、Confirm、Cancel三个逻辑上的方法
  • 代码侵入高,原因同上

2.4、Saga

Saga是一种常见的处理长活事务(Long Lived Transaction)的方案,其核心思想是通过将一个跨服务跨数据库的业务流程(全局事务),分解为一些列可以保证事务性的业务动作(分支事务)集合(这种思想有点类似于分治思想),每个业务动作(Do)都是可以保持ACID特性的真实事务,且都需要提供相应的补偿操作(Compensate),按照一定顺序去执行这些动作,如果所有动作都执行成功则认为整体业务流程完成(事务提交),如果中间发生异常情况则需要通过补偿操作进行补偿。在Saga中,按照执行顺序分为两种流程:

  • 正向业务流程:按照一定顺序依次执行所有的业务动作(Do),每个业务动作都在执行完立即提交;
  • 逆向补偿流程:如果业务流程执行出现异常,则通过补偿操作(Compensate),按照业务流程的反向,依次撤销和补偿已经执行过的业务操作;

如下图是Saga方案的简单流程示意图,绿色即为正向流程,橙色是反向流程。

Sage方案流程示意图

2.4.1、优点

  • 适合长流程状态驱动的场景
  • 方案灵活

2.4.2、缺陷

  • 隔离性差,如图所示一旦事务A执行完即提交,此时整个全局事务未完成时,外部已经可以获取事务A提交的数据,容易造成脏读
  • 还原度差,因为隔离性差,所以一旦需要走逆向补偿流程时,完全还原到提交前的状态在高并发时基本不太可能

2.5、可靠消息

对于常见的微服务系统,系统间的调用大部分场景都是通过RPC或HTTP进行同步调用,此时通过上面提到的2PC、TCC、Saga等方案来实现分布式事务是较为合适的,但是如果系统间不直接依赖,而是通过MQ等方式进行异步交互时,应该如何实现分布式事务呢?这个时候,就要可靠消息方案来解决了。

可靠消息方案主要指的是在业务互动的主动方与被通知方之间,架设一个稳定可靠的消息系统来保证业务活动主动方执行本地业务动作与通知具备事务性,也就是说如果业务活动的主动方事务执行成功,被动方一定能收到相应的业务通知,反之如果主动方的事务回滚则业务通知不会发送给被动方。

整个方案分为两个步骤:

  1. 投递消息:

    1. 在业务活动的主动方提交事务前向可靠消息系统发送消息,可靠消息系统接收后暂存消息并不进行投递;
    2. 在业务活动的主动方提交事务后向可靠消息系统确认发送消息,可靠消息系统进行消息的投递,通过重试等手段保证消息投递MQ成功;
    3. 在业务活动的主动方事务回滚后向可靠消息系统删除消息,可靠消息系统对消息进行清理,不进行投递;
  2. 接收消息:

    1. 监听并处理对应MQ主题产生的消息,如果接收并处理成功则ACK给MQ,如果处理失败可以通过死信进行多次重试

如下图是可靠消息方案的简单流程示意图,其中上游系统对应的是活动通知方,而下游系统对应的是被通知方。

可靠消息方案示意图

1、如果是不支持ACK机制或者没有MQ而是通过可靠消息系统直接投递的场景,被通知方服务在处理成功后需要ACK给可靠消息系统,否则可靠消息系统会通过重试保障消息的必达

2、通过mq来进行系统交互,存在的一个比较大的问题就是需要保证消息的有序性,尤其是对kafka这种需要通过业务保证有序性时,需要做好消息的隔离

2.5.1、优点

  • 可用性强,可靠消息服务独立部署,可伸缩
  • 被通知方无改造成本

2.5.2、缺陷

  • 可靠消息服务实现成本高,有技术难度
  • 最终一致性的实时性无法保障
  • 一次事务,需要至少发送两次消息

2.6、最大努力通知

最大努力通知服务是一种较简单也是现实中常用的实现最终一致性的分布式事务解决方案,整个方案分为两个步骤:

  1. 投递消息:业务活动的主动方,在完成业务处理之后,向业务活动的被动方发送消息,允许消息丢失;
  2. 消息补偿:业务活动的被动方,通过一定的策略订单向业务活动主动发发起查询,恢复丢失的业务消息;

如下图是最大努力通知方案的简单流程示意图,注意其中下游系统对应的是活动通知方,而上游系统对应的是被通知方。

最大努力通知方案流程示意图

最大努力通知方案面向的场景和可靠消息方案类似,也是系统间通过异步进行交互,区别在于:

  • 可靠消息系统:关注业务活动的主动方在发消息过程的事务性,业务活动的主动方需要为可靠消息系统提供查询补偿操作
  • 最大努力通知:关注业务活动的被动方是否收到消息,业务活动的主动方需要为被动方提供查询补偿操作

2.6.1、优点

  • 可用性强
  • 支持异步场景
  • 方案简单

2.6.2、缺陷

  • 活动通知方需要提供查询能力,活动被通知放需要实现补偿
  • 下游依赖上游
  • 最终一致性的实时性差

2.7、需要注意的点

分布式事务下,虽然方案可能选择不同,但多少都会涉及到流程重试、业务补偿以及业务隔离,这就要求在设计开发相关业务操作(如TCC中的Try、Confirm、Cancel三个操作)时需要考虑到操作的可查询幂等

2.7.1、可查询

在分布式事务下,事务当前的业务状态和业务数据应该是可以查询的,因此这就要求一次业务操作应该具备一个全局唯一的标识,便于查询和补偿,如:

  • 业务的单据凭证号(订单号)
  • 系统分配的流水号
  • 对操作资源的组合(商户编号 + 用户编号)

2.7.2、幂等

幂等是一个老生常谈的问题,不是只有分布式事务场景下才需要考虑,主要指的是针对一个操作,任意多次执行产生的业务影响和一次执行产生的业务影响相同,一般来说实现上有两种思路:

  • 通过业务逻辑实现幂等性(如创建订单,如果对应订单之前已经创建成功则返回创建成功的订单信息,而不应该返回创建失败)
  • 存储所有请求和结果,检测到重复请求后,直接返回之前处理的结果

幂等不是简单地过滤掉重复请求,更不是简单地返回第一次操作的结果,实际上幂等的逻辑应该和业务逻辑紧密相关,在设计一个具备幂等能力的接口时我们需要多从实际业务场景来触发

2.8、对比

下面对上面几种方案进行简单对比,可以发现没有适用于所有场景的完美方案,都有各自的优缺点,通常情况下同步场景使用TCC的较多,而异步场景使用最大努力通知的较多。

方案一致性隔离性并发业务成本技术成本灵活度代码侵入开源项目适用场景
2PC(XA协议)强一致性需要设置数据隔离级别为SERIALIZABLE,将导致衰退严重无,数据库原生支持几乎无Seata AT不推荐
TCC最终一致性Try保障隔离高并发Seata TCCEasyTransactionByteTCCSwan各种同步场景
Saga最终一致性高并发,状态驱动中低,需要提供逆向方法Seata Saga流程节点多且长
可靠消息最终一致性高并发,但整体性能取决于可靠消息系统,一般会有限流保障低,实现事务状态反查高,可靠消息系统的研发RocketMQQmqMafka下游结果不影响上游执行的异步流程
最大努力通知最终一致性高并发,基本不影响整体性能低,通知方提供查询补偿能力基本无业务执行结果的通知