对于刚刚接触分布式系统的伙伴来说,分布式看起来非常高大上、深不可测。目前已有Dubbo、SpringCloud等较好的分布式框架,但分布式事务仍是分布式系统一大痛点,本文结合一些经典博客文章,简单解析一些常见的分布式事务解决方案。
事务可以看成是由多个小事件一起组成的一个大事件,这些小事件要么全部成功,则整个事件成功;如果有任意一个事件失败,则所有事件均宣告失败,并恢复成事件执行之前的样子。
在单应用开发场景中,较多的是通过关系型数据库来控制事务,利用数据库本身的事务特性来实现的,称为数据库事务,也可以称为本地事务。数据库事务在实现时会将一次事务的所有操作全部纳入到一个不可分割的执行单元,该执行单元的所有操作要么全部成功,要么全部失败,若其中任意操作执行失败,都将导致整个事务的回滚。
严格意义上的事务实现应该是具备原子性、一致性、隔离性和持久性,简称 ACID。
随着互联网技术的发展与数据体量的扩增,软件系统逐渐由单体应用演变为分布式系统/微服务应用。分布式系统把一个单体应用系统拆分成可独立部署的多个微服务,很多场景下需要服务之间远程协作才能完成事务操作,这种分布式系统环境下由不同的服务之间通过网络远程协作完成事务称之为分布式事务。分布式系统中实现事务,其实是由多个本地事务组合而成。对于分布式事务而言几乎满足不了 ACID。
跨JVM进程产生分布式事务
典型的场景就是微服务架构:微服务之间通过远程调用完成事务操作。比如:订单微服务和库存微服务,下单的同时订单微服务请求库存微服务减少库存。
跨数据库实例产生分布式事务
当单体应用需要访问多个数据库(实例)时就会产生分布式事务。比如:用户信息和订单信息分别在两个MySQL实例存储,用户管理系统删除用户信息,需要分别删除用户信息及用户的订单信息,由于用户信息和订单数据分布在不同的数据实例,需要通过不同的数据库链接去操作数据,就会产生分布式事务。
多服务访问同一个数据库实例
订单微服务和库存微服务即使访问同一个数据库也会产生分布式事务,原因就是跨JVM进程,两个微服务持有了不同的数据库链接进行数据库操作,此时产生分布式事务。
CAP 是 Consistency、Availability、Partition tolerance 三个单词的缩写,分别表示一致性、可用性和分区容忍性。一个分布式系统最多只能同时满足一致性、可用性和分区容错性这三项中的两项。
一致性是指写操作后的读操作可以读取到最新的数据状态,当数据分布在多个节点上,从任意结点读取到的数据都是最新的状态,即所有节点在同一时间的数据完全一致。
A. 从客户端和服务端来看一致性:
1.从客户端来看,一致性主要指的是多并发访问时更新过的数据如何获取的问题。
2.从服务端来看,则是更新如何分布到整个系统,以保证数据最终一致。
B. 从一致性的程度来看一致性:
1.强一致性:对于关系型数据库,要求更新过的数据能被后续的访问都能看到。
2.弱一致性:能容忍后续的部分或者全部访问不到。
3.最终一致性:经过一段时间后要求能访问到更新后的数据。
C. 分布式系统一致性的特点:
1.由于存在数据同步的过程,写操作的响应会有一定的延迟。
2.为了保证数据一致性会对资源暂时锁定,待数据同步完成释放锁定资源。
3.如果请求数据同步失败的结点则会返回错误信息,一定不会返回旧数据。
可用性指服务一直可用,任何事务操作都可以得到响应结果,且不会出现响应超时或响应错误。
分区容错性指在遇到某节点或网络分区故障的时候,仍然能够对外提供满足一致性和可用性的服务。通常分布式系统的各各结点部署在不同的子网,不可避免的会出现由于网络问题而导致结点之间通信失败,此时仍可对外提供服务,即为分区容错性。分区容错性分是布式系统具备的基本能力。
在所有分布式事务场景中不会同时具备 CAP 三个特性,因为在具备了P的前提下C和A是不能共存的。在生产中对分布式事务处理时要根据需求来确定满足 CAP 的哪两个方面。
CAP 是一个已经被证实的理论,一个分布式系统最多只能同时满足:一致性(Consistency)、可用性(Availability)和分区容忍性(Partition tolerance)这三项中的两项。它可以作为我们进行架构设计、技术选型的考量标准。对于多数大型互联网应用的场景,节点多、部署分散,而且现在的集群规模越来越大,所以节点故障、网络故障是常态,而且要保证服务可用性达到 N 个 9(99.99…%),并要达到良好的响应性能来提高用户体验,因此一般都会做出如下选择:保证 P 和 A ,舍弃 C 的强一致性,但保证最终一致性。
BASE 是 Basically Available(基本可用)、Soft state(软状态)和 Eventually consistent (最终一致性)三个短语的缩写。BASE 理论是对 CAP 中 AP 的一个扩展,通过牺牲强一致性来获得可用性,当出现故障允许部分不可用但要保证核心功能可用,允许数据在一段时间内是不一致的,但最终达到一致状态。满足BASE理论的事务,我们称之为“柔性事务”。
CAP 理论告诉我们一个分布式系统最多只能同时满足一致性(Consistency)、可用性(Availability)和分区容忍性(Partition tolerance)这三项中的两项,其中AP在实际应用中较多,AP 即舍弃一致性,保证可用性和分区容忍性,但是在实际生产中很多场景都要实现一致性,比如主数据库向从数据库同步数据,即使不要一致性,但是最终也要将数据同步成功来保证数据一致,这种一致性和 CAP 中的一致性不同,CAP 中的一致性要求在任何时间查询每个结点数据都必须一致,它强调的是强一致性,但是最终一致性是允许可以在一段时间内每个结点的数据不一致,但是经过一段时间每个结点的数据必须一致,它强调的是最终数据的一致性。
ACID (刚性事务) | BASE (柔性事务) |
---|---|
原子性(Atomicity) | 基本可用(Basically Available) |
一致性(Consistency) | 柔性状态(Soft state) |
隔离性(Isolation) | 最终一致性(Eventually Consistent) |
持久性(Durability) |
2PC(Two-phase commit protocol),中文叫二阶段提交。 2PC 是一种强一致性设计,有事务协调者和事务参与者两个主要角色,事务的发起者为事务协调者,事务的其他执行者为事务参与者。事务协调者协调管理各事务参与者的提交和回滚,。我们可以把事务协调者想象为带头大哥,而事务参与者则理解为跟班小弟,由带头大哥协调所有跟班小弟的任务执行。
事务协调者会等待收到所有事务参与者响应后才会进行下一步操作,且事务协调者在该阶段中有超时机制。如果事务协调者收到事务参与者响应信息为 yes,则向所有事务参与者发送提交(commit)信息;如果事务协调者收到事务参与者的失败信息或超时信息,则会给所有事务参与者发送回滚(rollback)信息进行事务回滚。
事务参与者根据来自事务协调者的指令执行提交事务或者回滚事务的操作,并释放所有事务处理过程中占用的锁资源(必须在最后阶段释放锁资源) 。以下是两种情况具体步骤解析:
2PC 方案实现起来相对简单,但实际项目中用的比较少,主要因为以下问题:
3PC 也是强一致性,该方案是 2PC 上的改进版本,主要是在事务协调者和事务参与者中都引入超时机制,并 将 2PC 方案的准备阶段拆分为2个阶段,插入了一个预提交阶段(PreCommit),以此来处理2PC的提交阶段中事务参与者发生崩溃或错误,或者网络波动,导致事务参与者无法知晓是否提交或回滚的不确定状态所引起的延时问题。
事务协调者向事务参与者发送 commit 请求,参与者如果可以提交就 yes 并进入预提交状态(与2PC中的准备阶段不同,参与者不执行事务操作),否则返回 no 响应。
3PC中的预提交阶段和2PC中的准备阶段一样,执行事务但不提交。事务协调者根据准备阶段中事务参与者的响应决定是否可以进行事务的预提交操作。
该阶段与2PC的提交阶段一样,进行真正的事务提交。进入提交阶段后,无论是事务协调者出现问题,或者事务协调者与事务参与者网络出现问题,都会导致事务参与者无法接收到事务协调者发出的 do Commit 请求或 abort 请求,事务参与者都会在等待超时之后,继续执行事务提交,因为预提交阶段的引入起到了一个统一状态的作用,进入到提交阶段则事务参与者默认认为事务应该被提交。
优点:3PC相比2PC,会先询问事务参与者是否有条件接事务,不会直接锁资源,降低了阻塞范围,在等待超时后事务协调者或事务参与者会中断事务。避免了事务协调者单点故障问题,3PC的提交阶段中即使事务协调者出现问题时,事务参与者会继续提交事务。
缺点:引入一个阶段,多一次交互,性能比2PC更低,且绝大部分情况事务参与者都由能执行事务的条件,仍要先询问一次。而且数据不一致问题依然存在,当在参与者收到 preCommit 请求后等待 do commite 指令时,此时如果协调者请求中断事务,而协调者无法与参与者正常通信,会导致参与者继续提交事务,造成数据不一致。
2PC 和3PC 都是数据库层面的,而 TCC 是基于业务层的分布式事务。TCC 的概念,最早是由 Pat Helland 于 2007 年发表的一篇名为《Life beyond Distributed Transactions:an Apostate’s Opinion》的论文提出的。其核心思想是:针对每个操作,都要注册一个与其对应的确认和补偿(撤销)操作。
TCC 是 Try、Confirm、Cancel 三个词语的缩写,TCC 要求每个分支事务实现三个操作:预处理 Try、确认 Confirm、撤销,Try、Confirm、Cancel 可以理解为 SQL 事务中的 Lock、Commit、Rollback。可以将TCC 简单理解为服务化的 2PC 编程模型,都是先试探性的执行,如果都可以那就真正的执行,如果不行就回滚。
TCC中还可以有一个事务管理者的角色 - TM 事务管理器:TM事务管理器可以实现为独立的服务,也可以让全局事务发起方充当 TM 的角色,TM 独立出来是为了成为公用组件,是为了考虑系统结构和软件复用。TM 在发起全局事务时生成全局事务记录,全局事务 ID 贯穿整个分布式事务调用链条,用来记录事务上下文, 追踪和记录状态,由于 Confirm 和 Cancel 失败需进行重试,因此需要实现为幂等,幂等性是指同一个操作无论请求多少次,其结果都相同。
TCC 的 Try、Confirm、Cancel 3 个方法均由业务编码实现:
为了方便理解,下面以电商下单为例进行方案解析,这里把整个过程简单分为扣减库存,订单创建 2 个步骤,库存服务和订单服务分别在不同的服务器节点上。
从执行阶段来看,与传统事务机制中业务逻辑相同。但从业务角度来看,却不一样。
TCC 机制中的 Try 仅是一个初步操作,它和后续的确认一起才能真正构成一个完整的业务逻辑,这个阶段主要完成:
TCC 事务机制是以初步操作(Try)为中心的,确认操作(Confirm)和取消操作(Cancel)都是围绕初步操作(Try)而展开。因此,Try 阶段中的操作,其保障性是最好的,即使失败,仍然有取消操作(Cancel)可以将其执行结果撤销。
假设商品库存为 100,购买数量为 2,这里检查和更新库存的同时,冻结用户购买数量的库存,同时创建订单,订单状态为待确认。
Confirm:当 Try 阶段服务全部正常执行, 执行确认业务逻辑操作。Try 阶段所有分支事务执行成功后开始执行 Confirm。通常情况下,采用 TCC 则认为 Confirm 阶段是不会出错的。即:只要 Try 成功,Confirm 一定成功,若 Confirm 阶段真的出错了,需引入重试机制或人工处理,所以 Confirm 操作需要满足幂等性。
Confirm 阶段也可以看成是对 Try 阶段的一个补充,Try + Confirm 一起组成了一个完整的业务逻辑,Confirm 阶段使用的资源一定是 Try 阶段预留的业务资源。
Cancel:当 Try 阶段存在服务执行失败需要回滚, 进入 Cancel 阶段,执行分支事务的业务取消,释放Try阶段中的预留业务员资源。通常情况下,采用 TCC 则认为 Cancel 阶段也是一定成功的。若 Cancel 阶段真的出错了,需引入重试机制或人工处理,所以 Cancel 操作需要满足幂等性。
TCC 事务机制相对于传统事务机制(X/Open XA),有以下优点:
缺点:
本地消息表的方案最初是由 eBay 提出,该方案基于可靠消息最终一致性方案。可靠消息最终一致性方案是指当事务发起方执行完成本地事务后并发出一条消息,事务参与方(消息消费者)一定能够接收消息并处理事务成功,此方案强调的是只要消息发给事务参与方最终事务要达到一致。
本地消息表核心思路是利用各系统本地的事务来实现分布式事务。通过在事务主动发起方额外新建事务消息表,事务发起方处理业务和记录事务消息在本地事务中完成,轮询事务消息表的数据发送事务消息,事务被动方基于消息中间件消费事务消息表中的事务。
事务消息表与业务数据表处于同一个数据库中,然后在执行业务的时候 将业务的执行和将消息放入消息表中的操作放在同一个事务中,保证消息放入本地表中业务肯定是执行成功的,这样就能利用本地事务来保证在对这两个表的操作满足事务特性,并且使用了消息队列来保证最终一致性。这样设计可以避免”业务处理成功 + 事务消息发送失败",或"业务处理失败 + 事务消息发送成功"的棘手情况出现,保证 2 个系统事务的数据一致性。
把分布式事务最先开始处理的事务方称为事务主动方,在事务主动方之后处理的业务内的其他事务称为事务被动方。为了方便理解,以电商下单为例进行方案解析,这里把整个过程简单分为扣减库存,订单创建 2 个步骤。
库存服务和订单服务分别在不同的服务器节点上,其中库存服务是事务主动方,订单服务是事务被动方。事务的主动方需要额外新建事务消息表,用于记录分布式事务的消息的发生、处理状态。
整个业务处理流程如下图:
事务主动方处理本地事务。
事务主动方在本地事务中处理业务更新操作和写消息表操作。上面例子中库存服务阶段在本地事务中完成扣减库存和写消息表(图中 1、2)。
事务主动方通过消息中间件,通知事务被动方处理事务通知事务待消息。
消息中间件可以基于 Kafka、RocketMQ 等消息队列,事务主动方主动写消息到消息队列,事务消费方消费并处理消息队列中的消息。
库存服务把事务待处理消息写到消息中间件,订单服务消费消息中间件的消息,完成新增订单(图中 3 - 5)。
事务被动方通过消息中间件,通知事务主动方事务已处理的消息。
订单服务把事务已处理消息写到消息中间件,库存服务消费中间件的消息,并将事务消息的状态更新为已完成(图中 6 - 8)。
为了数据的一致性,当处理错误需要重试,事务发送方和事务接收方相关业务处理需要支持幂等,而且一般重试会有最大次数,超过最大次数可以记录下报警让人工处理。可以看到本地消息表其实实现的是最终一致性,容忍了数据暂时不一致的情况。具体保存一致性的容错处理如下:
本地消息表的优点如下:
缺点如下:
MQ事务消息方案可以算是最大努力通知方案,最大努力通知方案的目标:发起通知方通过一定的机制最大努力将业务处理结果通知到接收方。具体包括:
所以最大努力通知其实只是表明了一种柔性事务的思想:我已经尽力我最大的努力想达成事务的最终一致了。
基于 MQ 的分布式事务方案其实是对本地消息表的封装,将本地消息表基于 MQ 内部,其他方面的协议基本与本地消息表一致。有一些第三方的MQ是支持事务消息的,比如RocketMQ,他们支持事务消息的方式也是类似于采用的二阶段提交,但是市面上一些主流的MQ都是不支持事务消息的,比如 RabbitMQ 和 Kafka 都不支持。
下面主要基于 RocketMQ 介绍 MQ 的分布式事务方案。
在本地消息表方案中,保证事务主动方发写业务表数据和写消息表数据的一致性是基于数据库事务,RocketMQ 的事务消息相对于普通 MQ,相当于提供了 2PC 的提交接口,第一阶段Prepared消息,会拿到消息的地址。 第二阶段执行本地事务,第三阶段通过第一阶段拿到的地址去访问消息,并修改状态。方案如下:
这种情况下,事务主动方服务正常,没有发生故障,发消息流程如下:
有断网或者应用重启等异常情况,流程如下:
介绍完 RocketMQ 的事务消息方案后,由于前面已经介绍过本地消息表方案,这里就简单介绍 RocketMQ 分布式事务:
最大努力通知与可靠消息一致性有什么不同?
解决方案思想不同
可靠消息一致性,发起通知方需要保证将消息发出去,并且将消息发到接收通知方,消息的可靠性关键由发起通知方来保证。最大努力通知,发起通知方尽最大的努力将业务处理结果通知为接收通知方,但是可能消息接收不到,此时需要接 收通知方主动调用发起通知方的接口查询业务处理结果,通知的可靠性关键在接收通知方。
两者的业务应用场景不同
可靠消息一致性关注的是交易过程的事务一致,以异步的方式完成交易。最大努力通知关注的是交易后的通知事务,即将交易结果可靠的通知出去。
技术解决方向不同
可靠消息一致性要解决消息从发出到接收的一致性,即消息发出并且被接收到。最大努力通知无法保证消息从发出到接收的一致性,只提供消息接收的可靠性机制。可靠机制是,最大努力的将消息通知给接收方,当消息无法被接收方接收时,由接收方主动查询消息(业务处理结果)
MQ事务消息方案相比本地消息表方案,其优点是:
缺点是:
Saga 事务源于 1987 年普林斯顿大学的 Hecto 和 Kenneth 发表的如何处理 long lived transaction(长活事务)论文。
Saga 事务核心思想是将长事务拆分为多个本地短事务组成,由 Saga 事务协调器协调,每个本地事务有相应的执行模块和补偿模块,如果正常结束那就正常完成,当 Saga 事务中的任意一个本地事务出错了, 可以根据相反顺序调用相关事务对应的补偿方法恢复,达到事务的最终一致性。在服务请求的过程中,可能会出现超时重试的情况,需要通过幂等来避免多次请求所带来的问题。
ACID (刚性事务) | Saga 只提供ACD保证 |
---|---|
原子性(Atomicity) | 原子性(通过Saga协调器实现) |
一致性(Consistency) | 一致性(本地事务 + Saga Log) |
隔离性(Isolation) | 隔离性(Saga 不保证) |
持久性(Durability) | 持久性(Saga Log) |
Saga 事务基本协议如下:
可以看到,和 TCC 相比,Saga 没有“预留”动作,它的 Ti 就是直接提交到库。
Saga 的执行顺序有两种,如下图。下面以下单流程为例,整个操作包括:创建订单、扣减库存、支付、增加积分。
Saga 定义了两种恢复策略:
Saga 事务常见的有两种不同的实现方式:
中央协调器(Orchestrator,简称 OSO)以命令/回复的方式与每项服务进行通信,全权负责告诉每个参与者该做什么以及什么时候该做什么。
以电商订单的例子为例:
中央协调器必须事先知道执行整个订单事务所需的流程(例如通过读取配置)。如果有任何失败,它还负责通过向每个参与者发送命令来撤销之前的操作来协调分布式的回滚。
基于中央协调器协调一切时,回滚要容易得多,因为协调器默认是执行正向流程,回滚时只要执行反向流程即可。
在事件编排方法中,第一个服务执行一个事务,然后发布一个事件。该事件被一个或多个服务进行监听,这些服务再执行本地事务并发布(或不发布)新的事件。
当最后一个服务执行本地事务并且不发布任何事件时,意味着分布式事务结束,或者它发布的事件没有被任何 Saga 参与者听到都意味着事务结束。
以电商订单的例子为例:
事件编排是实现 Saga 模式的自然方式,它很简单,容易理解,不需要太多的代码来构建。如果事务涉及 2 至 4 个步骤,则可能是非常合适的。
由于 Saga 模型中只支持ACD,没有 Prepare 阶段,因此事务间不能保证隔离性。
当多个 Saga 事务操作同一资源时,就会产生数据语义不一致、更新丢失、脏数据读取等问题。需要在业务层控制并发,解决方案如下:
命令协调设计的优点如下:
命令协调设计缺点如下:
事件编排设计优点如下:
事件/编排设计缺点如下:
Seata 是由阿里中间件团队发起的开源项目 Fescar,后更名为 Seata,它是一个是开源的分布式事务框架。
传统 2PC 的问题在 Seata 中得到了解决,它通过对本地关系数据库的分支事务的协调来驱动完成全局事务,是工作在应用层的中间件。主要优点是性能较好,且不长时间占用连接资源,它以高效并且对业务 0 侵入的方式解决微服务场景下面临的分布式事务问题,它目前提供 AT 模式(即 2PC)及 TCC 模式的分布式事务解决方案。
Seata 的设计目标其一是对业务无侵入,因此从业务无侵入的 2PC 方案着手,在传统 2PC的基础上演进,并解决 2PC 方案面临的问题。
Seata 把一个分布式事务理解成一个包含了若干分支事务的全局事务。全局事务的职责是协调其下管辖的分支事务达成一致,要么一起成功提交,要么一起失败回滚。此外,通常分支事务本身就是一个关系数据库的本地事务,下图是全局事务与分支事务的关系图:
与传统 2PC 的模型类似,Seata 定义了 3 个组件来协议分布式事务的处理过程:
拿新用户注册送积分举例,简单分析Seata的分布式事务过程:
Seata方案与传统 2PC 方案对比,有以下优点:
由于 Seata 的 0 侵入性并且解决了传统 2PC 长期锁资源的问题,推荐采用 Seata 实现 2PC。
介绍完分布式事务相关理论和常见解决方案后,最终的目的在实际项目中运用,因此,总结一下各个方案的常见的使用场景: