一、产生背景
为了应对互联网环境带来了海量的数据容量、连接数与访问量,通过数据拆分实现数据库能力的线性扩展,通过微服务将复杂的单体应用拆分为若干个功能简单、松耦合的服务。系统微服务化后,一个看似简单的功能,内部可能需要调用多个服务并操作多个数据库实现,服务调用的分布式事务问题变的非常突出。分布式事务已经成为微服务落地最大的阻碍,也是最具挑战性的一个技术难题。 为此,本文将深入和大家探讨微服务架构下,分布式事务的各种解决方案。
二、理论基础
1、ACID
事务(Transaction)是由一系列对系统中数据进行访问与更新的操作所组成的一个程序执行逻辑单元(Unit),狭义上的事务特指数据库事务。一方面,当多个应用程序并发访问数据库时,事务可以在这些应用程序之间提供一个隔离方法,以防止彼此的操作互相干扰。另一方面,事务为数据库操作序列提供了一个从失败中恢复到正常状态的方法,同时提供了数据库即使在异常状态下仍能保持数据一致性的方法。事务具有四个特征,简称为事务的ACID特性。
• 原子性(Atomicity):事务作为一个整体被执行,包含在其中的对数据库的操作要么全部被执行,要么都不执行。
• 一致性(Consistency):事务应确保数据库的状态从一个一致状态转变为另一个一致状态。一致状态的含义是数据库中的数据应满足完整性约束。
• 隔离性(Isolation):多个事务并发执行时,一个事务的执行不应影响其他事务的执行。
• 持久性(Durability):已被提交的事务对数据库的修改应该永久保存在数据库中。
2、CAP定理
CAP定理是由加州大学伯克利分校Eric Brewer教授提出来的,他指出WEB服务无法同时满足一下3个属性:
• 一致性(Consistency) : 客户端知道一系列的操作都会同时发生(生效)
• 可用性(Availability) : 每个操作都必须以可预期的响应结束
• 分区容错性(Partition tolerance) : 即使出现单个组件无法可用,操作依然可以完成
具体地讲在分布式系统中,在任何数据库设计中,一个Web应用至多只能同时支持上面的两个属性。显然,任何横向扩展策略都要依赖于数据分区。因此,设计人员必须在一致性与可用性之间做出选择。这个定理在迄今为止的分布式系统中都是适用的!
3、BASE理论
在分布式系统中,我们往往追求的是可用性,它的重要程序比一致性要高,那么如何实现高可用性呢? 前人已经给我们提出来了另外一个理论,就是BASE理论,它是用来对CAP定理进行进一步扩充的。BASE理论指的是:
• Basically Available(基本可用)
• Soft state(软状态)
• Eventually consistent(最终一致性)
BASE理论是对CAP中的一致性和可用性进行一个权衡的结果,理论的核心思想就是:我们无法做到强一致,但每个应用都可以根据自身的业务特点,采用适当的方式来使系统达到最终一致性(Eventual consistency)。
4、2PC
两阶段提交,是实现分布式事务的成熟方案。第一阶段是表决阶段,是所有参与者都将本事务能否成功的反馈发给协调者;第二阶段是执行阶段,协调者根据所有参与者的反馈,通知所有参与者,步调一致地在所有分支上提交,或者在所有分支上回滚。
两阶段提交可以满足ACID,但代价是吞吐量。例如,数据库需要频繁地对资源上锁等等。而且更致命的是,资源被锁住的时间相对较长----在第一阶段即需要上锁,第二阶段才能解锁,依赖于所有分支的最慢者----这期间没有任何人可以对该资源进行修改。
两阶段提交理论的一个广泛工业应用是XA协议。目前几乎所有收费的商业数据库都支持XA协议。XA协议已在业界成熟运行数十年,但目前它在互联网海量流量的应用场景中,吞吐量这个瓶颈变得十分致命,因此很少被用到。
5、TCC
TCC(Try、Confirm、Cancel)是两阶段提交的一个变种。TCC提供了一个框架,需要应用程序按照该框架编程,将业务逻辑的每个分支都分为Try、Confirm、Cancel三个操作集。TCC让应用程序自己定义数据库操作的粒度,使得降低锁冲突、提高吞吐量成为可能。以一个典型的订单为例,按照TCC框架,应用需要在Try阶段将商品的库存减去,将买家账户中的相应金额扣掉,在临时表中记录下商品的数量,订单的金额等信息;另外再编写Confirm的逻辑,即在临时表中删除相关记录,生成订单,告知CRM、物流等系统,等等;以及Cancel逻辑,即恢复库存和买家账户金额,删除临时表相关记录。
最终一致性是指事务进行中,某些分支的中间状态可以被事务外观察到,即"读未提交",从而导致多个分支的状态可能不一致,但所有分支 最终 会达到要么全部提交,要么全部回滚的一致状态。很明显,最终一致性部分牺牲了ACID中的C和I,但它带来了可观的收益:资源不再需要长时间上锁,极大地提高了吞吐量。最终一致性在互联网应用场景中被广泛用做吞吐量和ACID的妥协点。
三、解决方案
1、两阶段提交(2PC)
XA 是指由 X/Open 组织提出的分布式事务处理的规范。XA规范主要定义了Transaction Manager(TM)和Resource Manager(RM)之间的接口,结构如下图所示。
XA协议的流程可大致分为三个步骤:
步骤1:AP向TM创建全局事务,TM向APP返回全局事务号。
步骤2:APP使用全局事务号,访问RM的资源(当RM为数据库时,资源访问就是SQL操作)。当RM第一次收到访问时,使用该全局事务号向TM注册,TM返回事务分支事务号。
步骤3:AP向TM发出全局事务提交请求,TM与参与事务的RM通信,进行提交处理,全部完成后,向AP返回结果。
TM与RM之间的提交处理,采用两阶段提交协议。TM在第一阶段对所有的参与事务的RM请求“预备”操作,达成关于分布式事务一致性的共识。事务参与者必须完成所有的约束检查,并且确保后续提交或放弃时所需要的数据已持久化。在第二阶段,根据之前达到的提交或放弃的共识,请求所有参事务的RM完成相应的操作。
提交事务的过程中需要在多个资源节点之间进行协调,而各节点对锁资源的释放必须等到事务最终提交时,所以两阶段提交在执行同样的事务时会比一阶段提交消耗更多的时间。当事务并发量达到一定数量时,就会出现大量事务积压甚至出现死锁,系统性能和处理吞吐量就会严重下滑。
2、补偿事务(TCC)
TCC模式为全局事务执行提供了一个框架,开发人员只需要实现每个事务分支的回滚,不需要记录整个事务流程的操作日志。TCC 其实就是采用的补偿机制,其核心思想是:针对每个操作,都要注册一个与其对应的确认和补偿(撤销)操作。它分为三个阶段:
• Try 阶段主要是对业务系统做检测及资源预留;
• Confirm 阶段主要是对业务系统做确认提交,Try阶段执行成功并开始执行 Confirm阶段时,默认 Confirm阶段是不会出错的。即:只要Try成功,Confirm一定成功。在这个阶段真正执行,不做业务检查。
• Cancel 阶段主要是在业务执行错误,需要回滚的状态下执行的业务取消,预留资源释放。
TCC模式结构如下图。
说明:
• 一个完整的业务活动由一个主业务服务与若干从业务服务组成。
• 主业务服务负责发起并完成整个业务活动。
• 从业务服务提供TCC型业务操作。
• 业务活动管理器控制业务活动的一致性,它登记业务活动中的操作,并在业务活动提交时确认所有的TCC型操作的confirm操作,在业务活动取消时调用所有TCC型操作的cancel操作。
TCC业务包括两个阶段完成:
• 第一阶段:主业务服务分别调用所有从业务的 try 操作,并在活动管理器中登记所有从业务服务。当所有从业务服务的 try 操作都调用成功或者某个从业务服务的 try 操作失败,进入第二阶段。
• 第二阶段:活动管理器根据第一阶段的执行结果来执行 confirm 或 cancel 操作。
如果第一阶段所有 try 操作都成功,则活动管理器调用所有从业务活动的 confirm操作。否则调用所有从业务服务的 cancel 操作。
使用TCC时要注意Try - Confirm - Cancel 3个操作的幂等控制,网络原因,或者重试操作都有可能导致这几个操作的重复执行。
3、本地消息表(异步确保)
本地消息表这种实现方式应该是业界使用最多的,其核心思想是将分布式事务拆分成本地事务进行处理这种思路是来源于ebay。我们可以从下面的流程图中看出其中的一些细节:
基本思路就是:
消息生产方,需要额外建一个消息表,并记录消息发送状态。消息表和业务数据要在一个事务里提交,也就是说他们要在一个数据库里面。然后消息会经过MQ发送到消息的消费方。如果消息发送失败,会进行重试发送。
消息消费方,需要处理这个消息,并完成自己的业务逻辑。此时如果本地事务处理成功,表明已经处理成功了,如果处理失败,那么就会重试执行。如果是业务上面的失败,可以给生产方发送一个业务补偿消息,通知生产方进行回滚等操作。
生产方和消费方定时扫描本地消息表,把还没处理完成的消息或者失败的消息再发送一遍。如果有靠谱的自动对账补账逻辑,这种方案还是非常实用的。
这种方案遵循BASE理论,采用的是最终一致性,笔者认为是这几种方案里面比较适合实际业务场景的,即不会出现像2PC那样复杂的实现(当调用链很长的时候,2PC的可用性是非常低的),也不会像TCC那样可能出现确认或者回滚不了的情况。
4、MQ事务消息
有一些第三方的MQ是支持事务消息的,比如RocketMQ,他们支持事务消息的方式也是类似于采用的二阶段提交,但是市面上一些主流的MQ都是不支持事务消息的,比如 RabbitMQ 和 Kafka 都不支持。
以阿里的 RocketMQ 中间件为例,其思路大致为:
第一阶段Prepared消息,会拿到消息的地址。第二阶段执行本地事务,第三阶段通过第一阶段拿到的地址去访问消息,并修改状态。
也就是说在业务方法内要想消息队列提交两次请求,一次发送消息和一次确认消息。如果确认消息发送失败了RocketMQ会定期扫描消息集群中的事务消息,这时候发现了Prepared消息,它会向消息发送者确认,所以生产方需要实现一个check接口,RocketMQ会根据发送端设置的策略来决定是回滚还是继续发送确认消息。这样就保证了消息发送与本地事务同时成功或同时失败。
MQ事务消息的一种可能实现的结构如下图。
说明:
1)业务处理服务在业务事务提交前,向实时消息服务请求发送消息,实时消息服务只记录消息数据,而不真正发送。
2)业务处理服务在业务事务提交后,向实时消息服务确认发送。只有在得到确认发送指令后,实时消息服务才真正发送消息。
3)业务处理服务在业务事务回滚后,向实时消息服务取消发送。
4)消息状态确认系统定期找到未确认发送或回滚发送的消息,向业务处理服务询问消息状态,业务处理服务根据消息ID或消息内容确定该消息是否有效。
通过消息进行事务异步的方式,可以保证业务数据操作和消息的发送同时执行成功或失败,保持了事务的最终一致性。
采用可靠消息的方式,在两个事务间实现分布式事务时,可以很好地满足事务最终一致性以及事务的回滚,但如果一个事务上下文中超过两个事务操作后,需要开发人员实现整个事务流程的操作日志的记录、每个事务分支的回滚以及整个流程的准确调度。
5、SAGA事务模型
Saga事务模型又叫做长时间运行的事务(Long-running-transaction), 它是由普林斯顿大学的H.Garcia-Molina等人提出,它描述的是另外一种在没有两阶段提交的的情况下解决分布式系统中复杂的业务事务问题。
我们这里说的是一种基于 Sagas 机制的工作流事务模型。该模型其核心思想就是拆分分布式系统中的长事务为多个短事务,或者叫多个本地事务,然后由 Sagas 工作流引擎负责协调,如果整个流程正常结束,那么就算是业务成功完成,如果在这过程中实现失败,那么Sagas工作流引擎就会以相反的顺序调用补偿操作,重新进行业务回滚。
四、开源软件Seata
1、简介
2007 开始,蚂蚁金服自主研发分布式事务分布式事务中间件 XTS(eXtended Transaction Service),在内部广泛应用并解决金融核心场景下的跨数据库、跨服务数据一致性问题,最终以 DTX(Distributed Transaction eXtended)的云产品化展现并对外开放。与此同时,阿里巴巴中间件团队发布 TXC(Taobao Transaction Constructor),为集团内应用提供分布式事务服务,经过多年的技术沉淀,于 2016 年产品化改造为 GTS(Global Transaction Service),通过阿里云解决方案在众多外部客户中落地实施。
2019 年 1 月,基于技术积累,阿里巴巴中间件团队发起了开源项目 Fescar(Fast & EaSy Commit And Rollback, Fescar),和社区一起共建分布式事务解决方案。Fescar 为解决微服务架构下的分布式事务问题交出了一份与众不同的答卷。而 Fescar 的愿景是让分布式事务的使用像本地事务的使用一样简单和高效。最终的目标是希望可以让 Fescar 适用于所有的分布式事务场景。
为了达到适用于更多的分布式事务业务场景的目标,蚂蚁金服加入 Fescar 社区共建,在 Fescar 0.4.0 版本中加入了 TCC 模式。蚂蚁金服的加入引发了社区核心成员的讨论,为了达到适用于所有的分布式事务业务场景的目标,也为了社区更中立、更开放、生态更加丰富,社区核心成员们决定进行品牌升级,改名 Seata。Seata 意为:Simple Extensible Autonomous Transaction Architecture,是一套一站式分布式事务解决方案。
2、架构
下图描述了GTS一种可能的实现架构。
与XA架构相同,GTS架构由应用、事务管理器、资源管理器三个部分组成。资源管理器由事务分支处理模块、镜像查询构造模块、并发控制模块、恢复控制模块,以及存储在数据库中的GTS事务信息(GTS锁表与GTS日志表)等组成。
•事务分支处理模块:是资源管理器的外部接口,并完成内部各模块的调用。
•镜像查询构造模块:从Insert、Update、Delete语句,生成该操作对应记录集的镜像查询语句。例如table_name表包含两个字段column1和column2,column1为主键,则镜像查询语句为select column1, column2 from table_name where column1=v1。
•并发控制模块:基于GTS事务锁表,维护读写并发控制。
•恢复控制模块:基于GTS日志表,进行故障恢复。
3、内部运行机制
AT(Automatic Transaction)模式
Seata AT模式是基于XA事务演进而来的一个分布式事务中间件,XA是一个基于数据库实现的分布式事务协议,本质上和两阶段提交一样,需要数据库支持,Mysql5.6以上版本支持XA协议,其他数据库如Oracle,DB2也实现了XA接口。核心思路是把一个分布式事务理解成一个包含了若干分支事务的全局事务。全局事务的职责是协调其下管辖的分支事务达成一致,要么一起成功提交,要么一起失败回滚。定义 3 个组件来协议分布式事务的处理过程。
Transaction Coordinator (TC): 事务协调器,维护全局事务的运行状态,负责协调并驱动全局事务的提交或回滚。
Transaction Manager (TM): 控制全局事务的边界,负责开启一个全局事务,并最终发起全局提交或全局回滚的决议。
Resource Manager (RM): 控制分支事务,负责分支注册、状态汇报,并接收事务协调器的指令,驱动分支(本地)事务的提交和回滚。
一个典型的分布式事务过程:
1)TM 向 TC 申请开启一个全局事务,全局事务创建成功并生成一个全局唯一的 XID。
2)XID 在微服务调用链路的上下文中传播。
3)RM 向 TC 注册分支事务,将其纳入 XID 对应全局事务的管辖。
4)TM 向 TC 发起针对 XID 的全局提交或回滚决议。
5)TC 调度 XID 下管辖的全部分支事务完成提交或回滚请求。
1)第一阶段
Seata 的 JDBC 数据源代理通过对业务 SQL 的解析,把业务数据在更新前后的数据镜像组织成回滚日志,利用 本地事务 的 ACID 特性,将业务数据的更新和回滚日志的写入在同一个 本地事务 中提交。这样,可以保证:任何提交的业务数据的更新一定有相应的回滚日志存在。
基于这样的机制,分支的本地事务便可以在全局事务的第一阶段提交,并马上释放本地事务锁定的资源。这也是Seata和XA事务的不同之处,两阶段提交往往对资源的锁定需要持续到第二阶段实际的提交或者回滚操作,而有了回滚日志之后,可以在第一阶段释放对资源的锁定,降低了锁范围,提高效率,即使第二阶段发生异常需要回滚,只需找对undolog中对应数据并反解析成sql来达到回滚目的。同时Seata通过代理数据源将业务sql的执行解析成undolog来与业务数据的更新同时入库,达到了对业务无侵入的效果。
2)第二阶段
如果决议是全局提交,此时分支事务此时已经完成提交,不需要同步协调处理(只需要异步清理回滚日志),Phase2 可以非常快速地完成。
如果决议是全局回滚,RM 收到协调器发来的回滚请求,通过 XID 和 Branch ID 找到相应的回滚日志记录,通过回滚记录生成反向的更新 SQL 并执行,以完成分支的回滚。
AT模型中有一个 重要前提:分支事务中涉及的资源,必须 是支持ACID事务的关系型数据库。分支的提交和回滚机制,都依赖于本地事务的保障。所以,如果应用使用的数据库是不支持事务的,或根本不是关系型数据库,就不适用。另外,目前 Seata 的实现还存在一些局限,比如:事务隔离级别最高支持到 读已提交 的水平,SQL 的解析还不能涵盖全部的语法等。为了覆盖 Fescar 原生机制暂时不能支持应用场景,我们定义了另外一种工作模式。
MT(Manual Transaction)模式
这种模式下,分支事务需要应用自己来定义业务本身及提交和回滚的逻辑。MT 模式一方面是 AT 模式的补充。另外,更重要的价值在于,通过 MT 模式可以把众多非事务性资源纳入全局事务的管理中。AT 和 MT 模式的分支从根本上行为模式是一致的,所以可以完全兼容,即,一个全局事务中,可以同时存在 AT 和 MT 的分支。这样就可以达到全面覆盖业务场景的目的:AT 模式可以支持的,使用 AT 模式;AT 模式暂时支持不了的,用 MT 模式来替代。另外,自然的,MT 模式管理的非事务性资源也可以和支持事务的关系型数据库资源一起,纳入同一个分布式事务的管理中。
4、具体处理流程
分别描述了insert/delete/update操作、读已提交操作、提交操作和回滚操作等四个操作的序列图(一种可能的实现方式)。
1)insert/delete/update操作流程序列图
2)读已提交操作流程序列图
3)提交操作流程序列图
4)回滚操作流程序列图
五、开源软件ServiceComb Pack
1、简介
ServiceComb是华为云于2017年6月开源的微服务框架,并于2017年12月正式进入Apache软件基金会孵化。其包括一站式的服务注册、服务治理、动态配置功能,具备服务化契约增强、多语言SDK支持、多通信协议支持等优势特性, 并提供SAGA数据最终一致性方案解决微服务架构数据一致性难题。ServiceComb 兼容Spring Cloud等业界流行微服务框架,互通业界生态。
ServiceComb Saga是针对微服务分布式事务最终一致性问题提供的解决方案。Saga分布式事务是由多个相关联的的本地事务操作所组成。Saga协调器负责保证Saga事务的最终一致性。当本地事务执行出错时,Saga协调器会自动执行相关的恢复操作保证分布式事务的最终一致性。相比其它的分布式事务一致性方案,Saga在简化事务配置以及提供多种事务恢复机制上有很明显的优势。目前开发的ServiceComb Saga 0.1.0支持用户通过Annoation的方式定义事务操作以及撤销事务操作的服务接口, 同时Saga协调器监控追踪事务的执行情况并负责协调事务执行者,保证事务的最终一致性。
2、架构
Pack中包含两个组件,即 alpha 和 omega。
alpha充当协调者的角色,主要负责对事务的事件进行持久化存储以及协调子事务的状态,使其得以最终与全局事务的状态保持一致。
omega是微服务中内嵌的一个agent,负责对网络请求进行拦截并向alpha上报事务事件,并在异常情况下根据alpha下发的指令执行相应的补偿操作。
3、Omega内部运行机制
omega是微服务中内嵌的一个agent。当服务收到请求时,omega会将其拦截并从中提取请求信息中的全局事务id作为其自身的全局事务id(即Saga事件id),并提取本地事务id作为其父事务id。在预处理阶段,alpha会记录事务开始的事件;在后处理阶段,alpha会记录事务结束的事件。因此,每个成功的子事务都有一一对应的开始及结束事件。
4、服务间通信流程
服务间通信的流程与Zipkin的类似。在服务生产方,omega会拦截请求中事务相关的id来提取事务的上下文。在服务消费方,omega会在请求中注入事务相关的id来传递事务的上下文。通过服务提供方和服务消费方的这种协作处理,子事务能连接起来形成一个完整的全局事务。
5、Saga具体处理流程
Saga处理场景是要求相关的子事务提供事务处理函数同时也提供补偿函数。Saga协调器alpha会根据事务的执行情况向omega发送相关的指令,确定是否向前重试或者向后恢复。
1)成功场景
成功场景下,每个事务都会有开始和有对应的结束事件。
2)异常场景
异常场景下,omega会向alpha上报中断事件,然后alpha会向该全局事务的其它已完成的子事务发送补偿指令,确保最终所有的子事务要么都成功,要么都回滚。
3)超时场景 (需要调整)
超时场景下,已超时的事件会被alpha的定期扫描器检测出来,与此同时,该超时事务对应的全局事务也会被中断。
6、TCC具体处理流程
TCC(try-confirm-cancel)与Saga事务处理方式相比多了一个Try方法。事务调用的发起方来根据事务的执行情况协调相关各方进行提交事务或者回滚事务。
1)成功场景
成功场景下, 每个事务都会有开始和对应的结束事件。
2)异常场景
异常场景下,事务发起方会向alpha上报异常事件,然后alpha会向该全局事务的其它已完成的子事务发送补偿指令,确保最终所有的子事务要么都成功,要么都回滚。
六、Seata和ServiceComb Pack对比
1、出身
Seata是阿里巴巴开源的分布式事务中间件,基于其内部的TXC和GTS的技术积累。虽然此框架非常活跃,但是19年刚刚开源,用于生产环境风险较大。
servicecomb-pack出自华为微服务框架servicecomb,servicecomb在Apache已经毕业了,但是一直比较“低调”。知名数据库中间Sharding-Sphere采用的就是servicecomb-pack提供的saga方案。
2、实现原理
Seata实际上本质就是将一个分布式事务转化为多个单库事务。采用Saga的思想,所有的正向操作,都保留逆向操作。一旦要回滚,只需要执行逆向操作就可以了。在业务数据库中额外增加一张事件表,这个事件表就是关键所在,在更新正常业务数据库的同时,在一个单库事务内(同一个数据库连接)同步更新事件表,这样来保证不丢数据。我们可以回顾一下一致性的要求,“要么同时成功,要么同时失败。”单库事务就可以保证。
servicecomb-pack和Seata一样,同样是saga的思想,所有的正向操作,都保留逆向操作。一旦要回滚,只需要执行逆向操作就可以了。但是,除此之外,servicecomb-pack也支持TCC。如图所示,Omega作为一个客户端,拦截所有的事务操作,事务开始向Alpha记录开始记录,事务结束向Alpha记录事务结束记录,一旦出现问题,直接在Alpha事件表中生成逆向操作,你应该已经看出来了,和fescar不同的是,事件表中的数据存储在全局协调者(alpha)这一侧。
两种做法各有优劣吧,存在业务侧实际上是有侵入的,不是绝对意义上的无侵入,虽然单库事务性能不错,但是事件表的所有操作都会影响正常业务,无法做到更好的隔离性。存在协调者一侧相对来说隔离性更好一些,但是这里会有概率产生不一致,例如,实际上业务操作已经完成了,数据库更新成功了,但是写事件日志可能会失败,这时候协调者会认为业务操作也失败了。
3、其它对比
稳定性:servicecomb-pack略胜一筹。更早的项目。
隔离性:Seata写隔离通过TC提供的分布式锁来实现,读隔离通过select for update实现,当然,servicecomb-pack同样可以通过select for update实现读隔离。
复杂度:servicecomb-pack略胜一筹。角色少,思路简单。业务侧,两个框架都可以通过简单的注解实现。
文档:fescar略胜一筹。
性能:没有实际测试,从原理上来讲,相差无几。
支持的数据库:fescar目前只支持mysql,servicecomb-pack的方案不区分数据库。
4、总结
虽然两个框架的目标都是让业务开发人员更简单,不用关心分布式事务的问题,但是在我看来,如果要使用,还是要搞清楚原理,除非对此问题非常敏感,否则,应该谨慎使用,能不用最好不用。 两个框架都在快速发展中,从实现思想上来讲非常相似,都是很好的解决方案,未来的情况主要看投入程度。