分布式事务解决方案
对于刚刚接触分布式系统的伙伴来说,分布式看起来非常高大上、深不可测。目前已有Dubbo、SpringCloud等较好的分布式框架,但分布式事务仍是分布式系统一大痛点,本文结合一些经典博客文章,简单解析一些常见的分布式事务解决方案。
基础介绍
事务
事务可以看成是由多个小事件一起组成的一个大事件,这些小事件要么全部成功,则整个事件成功;如果有任意一个事件失败,则所有事件均宣告失败,并恢复成事件执行之前的样子。
本地事务
在单应用开发场景中,较多的是通过关系型数据库来控制事务,利用数据库本身的事务特性来实现的,称为数据库事务,也可以称为本地事务。数据库事务在实现时会将一次事务的所有操作全部纳入到一个不可分割的执行单元,该执行单元的所有操作要么全部成功,要么全部失败,若其中任意操作执行失败,都将导致整个事务的回滚。
ACID
严格意义上的事务实现应该是具备原子性、一致性、隔离性和持久性,简称 ACID。
A(Atomic)原子性:一个事务中所有操作,要么都执行完成,要么全部不执行,不可能出现部分成功部分失败的情况。
C(Consistency)一致性:在事务执行前后,数据库的一致性约束没有被破坏。可以理解为数据是满足完整性约束的,也就是不会存在中间状态的数据。比如:张三给李四转 100 元,转账前和转账后的数据是正确的状态这叫一致性,如果出现张三转出 100 元,李四账户没有增加 100 元这就出现了中间状态的错误数据,没有达到一致性。
I(Isolation)隔离性:数据库中的事务一般都是并发的,隔离性是指并发的两个事务的执行互不干扰,一个事务不能看到其他事务的运行过程的中间状态。通过配置事务隔离级别可以比避免脏读、重复读问题。
D(Durability)持久性:一个事务完成了之后数据就被被持久化到数据库,之后的其他操作或故障都不会对事务的结果产生影响或者回滚操作。
分布式事务
随着互联网技术的发展与数据体量的扩增,软件系统逐渐由单体应用演变为分布式系统/微服务应用。分布式系统把一个单体应用系统拆分成可独立部署的多个微服务,很多场景下需要服务之间远程协作才能完成事务操作,这种分布式系统环境下由不同的服务之间通过网络远程协作完成事务称之为分布式事务。分布式系统中实现事务,其实是由多个本地事务组合而成。对于分布式事务而言几乎满足不了 ACID。
场景介绍
-
跨JVM进程产生分布式事务 典型的场景就是微服务架构:微服务之间通过远程调用完成事务操作。比如:订单微服务和库存微服务,下单的同时订单微服务请求库存微服务减少库存。
-
跨数据库实例产生分布式事务 当单体应用需要访问多个数据库(实例)时就会产生分布式事务。比如:用户信息和订单信息分别在两个MySQL实例存储,用户管理系统删除用户信息,需要分别删除用户信息及用户的订单信息,由于用户信息和订单数据分布在不同的数据实例,需要通过不同的数据库链接去操作数据,就会产生分布式事务。
-
多服务访问同一个数据库实例 订单微服务和库存微服务即使访问同一个数据库也会产生分布式事务,原因就是跨JVM进程,两个微服务持有了不同的数据库链接进行数据库操作,此时产生分布式事务。
CAP理论
CAP 是 <mark style="box-sizing: border-box; background: rgb(235, 255, 235); color: rgb(34, 34, 34); border-radius: 2px; padding: 2px 4px; margin: 0px 2px; font-weight: 500; text-indent: 0px;">Consistency</mark>、<mark style="box-sizing: border-box; background: rgb(235, 255, 235); color: rgb(34, 34, 34); border-radius: 2px; padding: 2px 4px; margin: 0px 2px; font-weight: 500; text-indent: 0px;">Availability</mark>、<mark style="box-sizing: border-box; background: rgb(235, 255, 235); color: rgb(34, 34, 34); border-radius: 2px; padding: 2px 4px; margin: 0px 2px; font-weight: 500; text-indent: 0px;">Partition tolerance</mark> 三个单词的缩写,分别表示一致性、可用性和分区容忍性。一个分布式系统最多只能同时满足一致性、可用性和分区容错性这三项中的两项。
CAP简介
- C(Consistency)一致性
一致性是指写操作后的读操作可以读取到最新的数据状态,当数据分布在多个节点上,从任意结点读取到的数据都是最新的状态,即<u style="box-sizing: border-box; text-indent: 0px;">所有节点在同一时间的数据完全一致</u>。
A. 从客户端和服务端来看一致性:
1.从客户端来看,一致性主要指的是多并发访问时更新过的数据如何获取的问题。
2.从服务端来看,则是更新如何分布到整个系统,以保证数据最终一致。
B. 从一致性的程度来看一致性:
1.<mark style="box-sizing: border-box; background: rgb(235, 255, 235); color: rgb(34, 34, 34); border-radius: 2px; padding: 2px 4px; margin: 0px 2px; font-weight: 500; text-indent: 0px;">强一致性</mark>:对于关系型数据库,要求更新过的数据能被后续的访问都能看到。
2.<mark style="box-sizing: border-box; background: rgb(235, 255, 235); color: rgb(34, 34, 34); border-radius: 2px; padding: 2px 4px; margin: 0px 2px; font-weight: 500; text-indent: 0px;">弱一致性</mark>:能容忍后续的部分或者全部访问不到。
3.<mark style="box-sizing: border-box; background: rgb(235, 255, 235); color: rgb(34, 34, 34); border-radius: 2px; padding: 2px 4px; margin: 0px 2px; font-weight: 500; text-indent: 0px;">最终一致性</mark>:经过一段时间后要求能访问到更新后的数据。
C. 分布式系统一致性的特点:
1.由于存在数据同步的过程,写操作的响应会有一定的延迟。
2.为了保证数据一致性会对资源暂时锁定,待数据同步完成释放锁定资源。
3.如果请求数据同步失败的结点则会返回错误信息,一定不会返回旧数据。
- A(Availability)可用性
可用性指服务一直可用,任何事务操作都可以得到响应结果,且不会出现响应超时或响应错误。
- P(Partition tolerance)分区容错性
分区容错性指在遇到某节点或网络分区故障的时候,仍然能够对外提供满足一致性和可用性的服务。通常分布式系统的各各结点部署在不同的子网,不可避免的会出现由于网络问题而导致结点之间通信失败,此时仍可对外提供服务,即为分区容错性。分区容错性分是布式系统具备的基本能力。
CAP组合
在所有分布式事务场景中不会同时具备 CAP 三个特性,因为在具备了P的前提下C和A是不能共存的。在生产中对分布式事务处理时要根据需求来确定满足 CAP 的哪两个方面。
AP:满足可用性和容错性,舍弃一致性。这也就是意味着你的系统在并发访问的时候可能会出现数据不一致的情况。这是很多分布式系统设计时的选择,大多数都是牺牲了一致性。但通常实现 AP 都会保证最终一致性,比如现在的12306抢票,本来你看到的是还有一张票,其实在这个时刻已经被买走了,你填好了信息准备提交订单的时候发现系统提示你已售罄。这就是牺牲了一致性,但过一段时间再重新查询,就会发现无票,这是实现了最终一致性,只要用户可以接受在一定的时间查到正确的数据即可。
CP:满足一致性和容错性,舍弃可用性。如果你的系统允许有段时间的访问失效等问题,这个是可以满足的。比如多个人并发买票,后台网络出现故障,你买的时候系统就崩溃了。zookeeper 其实就是追求的强一致,又比如跨行转账,一次转账请求要等待双方银行系统都完成整个事务才算完成。
CA:满足一致性和可用性,舍弃容错性。不考虑由于网络不通或结点挂掉的问题,那么系统将不是一个标准的分布式系统,涉及分布式的想法就是把功能分开,部署到不同的机器上。最常用的关系型数据就满足了 CA。
小结
CAP 是一个已经被证实的理论,一个分布式系统最多只能同时满足:一致性(Consistency)、可用性(Availability)和分区容忍性(Partition tolerance)这三项中的两项。它可以作为我们进行架构设计、技术选型的考量标准。对于多数大型互联网应用的场景,节点多、部署分散,而且现在的集群规模越来越大,所以节点故障、网络故障是常态,而且要保证服务可用性达到 N 个 9(99.99..%),并要达到良好的响应性能来提高用户体验,因此一般都会做出如下选择:保证 P 和 A ,舍弃 C 的强一致性,但保证最终一致性。
BASE理论
BASE理论简介
BASE 是 Basically Available(基本可用)、Soft state(软状态)和 Eventually consistent (最终一致性)三个短语的缩写。BASE 理论是对 CAP 中 AP 的一个扩展,通过牺牲强一致性来获得可用性,当出现故障允许部分不可用但要保证核心功能可用,允许数据在一段时间内是不一致的,但最终达到一致状态。满足BASE理论的事务,我们称之为“柔性事务”。
基本可用:分布式系统在出现故障时,允许损失部分可用功能,保证核心功能可用。如电商网站交易付款出现问题了,商品依然可以正常浏览。
软状态:由于不要求强一致性,所以BASE允许系统中存在中间状态(也叫软状态),这个状态不影响系统可用性,如订单的"支付中"、“数据同步中”等状态,待数据最终一致后状态改为“成功”状态。
最终一致:最终一致是指经过一段时间后,所有节点数据都将会达到一致。如订单的"支付中"状态,最终会变 为“支付成功”或者"支付失败",使订单状态与实际交易结果达成一致,但需要一定时间的延迟、等待。
强一致性和最终一致性
CAP 理论告诉我们一个分布式系统最多只能同时满足一致性(Consistency)、可用性(Availability)和分区容忍性(Partition tolerance)这三项中的两项,其中AP在实际应用中较多,AP 即舍弃一致性,保证可用性和分区容忍性,但是在实际生产中很多场景都要实现一致性,比如主数据库向从数据库同步数据,即使不要一致性,但是最终也要将数据同步成功来保证数据一致,这种一致性和 CAP 中的一致性不同,CAP 中的一致性要求<mark style="box-sizing: border-box; background: rgb(235, 255, 235); color: rgb(34, 34, 34); border-radius: 2px; padding: 2px 4px; margin: 0px 2px; font-weight: 500; text-indent: 0px;">在任何时间查询每个结点数据都必须一致</mark>,它强调的是强一致性,但是最终一致性是允许可以在一段时间内每个结点的数据不一致,但是经过一段时间每个结点的数据必须一致,它强调的是最终数据的一致性。
ACID与BASE
ACID (刚性事务) | BASE (柔性事务) |
---|---|
原子性(Atomicity) | 基本可用(Basically Available) |
一致性(Consistency) | 柔性状态(Soft state) |
隔离性(Isolation) | 最终一致性(Eventually Consistent) |
持久性(Durability) |
二阶段提交(2PC)
方案简介
2PC(Two-phase commit protocol),中文叫二阶段提交。 2PC 是一种强一致性设计,有事务协调者和事务参与者两个主要角色,事务的发起者为事务协调者,事务的其他执行者为事务参与者。事务协调者协调管理各事务参与者的提交和回滚,。我们可以把事务协调者想象为带头大哥,而事务参与者则理解为跟班小弟,由带头大哥协调所有跟班小弟的任务执行。
流程分析
准备阶段
事务协调者,给所有事务参与者发送事务内容,并询问能否提交事务,然后等待参与者回复。
事务参与者,收到事务内容,开始执行事务,同时将 <u style="box-sizing: border-box; text-indent: 0px;">undo</u> 和 <u style="box-sizing: border-box; text-indent: 0px;">redo</u> 信息记入事务日志中,但此时事务参与者并不提交事务。
如果参与者执行事务成功,则给协调者响应 yes ,回答可以进行事务提交;若果参与者事务执行失败,则给协调者回复 no ,表示不可进行事务提交。
提交阶段
事务协调者会等待收到所有事务参与者响应后才会进行下一步操作,且事务协调者在该阶段中有超时机制。如果事务协调者收到事务参与者响应信息为 yes,则向所有事务参与者发送提交(commit)信息;如果事务协调者收到事务参与者的失败信息或超时信息,则会给所有事务参与者发送回滚(rollback)信息进行事务回滚。
事务参与者根据来自事务协调者的指令执行提交事务或者回滚事务的操作,并释放所有事务处理过程中占用的锁资源(必须在最后阶段释放锁资源) 。以下是两种情况具体步骤解析:
- 当所有事务参与者均响应 yes,则提交事务,如下图。
事务协调者向所有事务参与者发出正式提交事务(commit)的请求。
事务参与者执行事务提交操作,若提交失败,则会不断重试,直到提交成功,然后释放整个事务期间占用的资源。
所有事务参与者向事务协调者响应提交事务完成的 ack 消息。
事务协调者收到所有事务参与者响应的 ack 消息后,完成提交事务操作。
- 当准备阶段任意一个事务参与者响应 no,则中断事务,如下图:
事务协调者向所有参与者发出回滚(rollback)请求。
事务参与者使用准备阶段中的 <u style="box-sizing: border-box; text-indent: 0px;">undo</u> 信息执行事务回滚操作,若回滚失败则会不断重试,直到所有事务参与者都回滚成功,然后释放整个事务期间占用的资源。
各事务参与者向事务协调者响应回滚事务完成的 ack 消息。
事务协调者收到所有参与者响应的 ack 消息后,完成事务中断操作。
方案小结
2PC 方案实现起来相对简单,但实际项目中用的比较少,主要因为以下问题:
性能较低:所有事务参与者在事务提交阶段处于同步阻塞状态,占用系统资源,效率低,有性能瓶颈。
可靠性问题:协调者存在单点故障问题,如果事务协调者出现故障,事务参与者将一直处于锁死状态。
数据一致性问题:在提交阶段中,如果发生网络波动,部分事务参与者收到了提交消息,另一些事务参与者没收到提交消息,导致了节点之间数据不一致的问题。
三阶段提交(3PC)
方案简介
3PC 也是强一致性,该方案是 2PC 上的改进版本,主要是在事务协调者和事务参与者中都引入超时机制,并 将 2PC 方案的准备阶段拆分为2个阶段,插入了一个预提交阶段(PreCommit),以此来处理2PC的提交阶段中事务参与者发生崩溃或错误,或者网络波动,导致事务参与者无法知晓是否提交或回滚的不确定状态所引起的延时问题。
流程分析
准备阶段(CanCommit)
事务协调者向事务参与者发送 commit 请求,参与者如果可以提交就 yes 并进入预提交状态(与2PC中的准备阶段不同,参与者不执行事务操作),否则返回 no 响应。
事务协调者向所有参与者发出包含事务内容的 canCommit 请求,询问是否可以提交事务,并等待所有事务参与者的响应。
事务参与者收到 canCommit 请求后,如果认为可以执行事务操作,则响应 yes 并进入预备状态,否则反馈 no (与2PC中的准备阶段不同,参与者不执行事务操作)。
预提交阶段(PreCommit)
3PC中的预提交阶段和2PC中的准备阶段一样,执行事务但不提交。事务协调者根据准备阶段中事务参与者的响应决定是否可以进行事务的预提交操作。
- 准备阶段中所有事务参与者均响应 yes,事务参与者进入预提交状态。
事务协调者向所有事务参与者发出 preCommit 请求,进入预提交阶段。
事务参与者收到 preCommit 请求后,执行事务操作,将 undo 和 redo 信息记入事务日志中(但不提交事务)。
所有事务参与者向事务协调者响应执行事务成功的 ack 消息或失败的 no消息 ,并等待事务协调者发出最终指令。
- 当准备阶段任意一个事务参与者响应 no,或者等待超时后事务协调者尚无法收到所有参与者的响应,则中断事务。
事务协调者向所有事务参与者发出 abort 请求。
事务参与者收到事务协调者发出的 abort 请求,或在等待事务协调者的指令过程中出现超时,均会中断事务。
提交阶段(DoCommit)
该阶段与2PC的提交阶段一样,进行真正的事务提交。进入提交阶段后,无论是事务协调者出现问题,或者事务协调者与事务参与者网络出现问题,都会导致事务参与者无法接收到事务协调者发出的 do Commit 请求或 abort 请求,事务参与者都会在等待超时之后,继续执行事务提交,因为预提交阶段的引入起到了一个统一状态的作用,进入到提交阶段则事务参与者默认认为事务应该被提交。
- 预提交阶段中所有事务参与者均响应 ack 消息,执行真正的事务提交。
如果事务协调者处于工作状态,则向所有事务参与者发出 do Commit 请求。
事务参与者收到 do Commit 请求后,会正式执行事务提交操作,并释放整个事务期间占用的资源。
所有事务参与者向协调者响应提交事务完成的 ack 消息。
事务协调者收到所有事务参与者响应的 ack 消息后,完成事务提交。
- 当预提交阶段中任意一个事务参与者响应 no,或者在预提交阶段中事务协调者无法收到所有参与者的响应,则中断事务。
如果事务协调者处于工作状态,向所有事务参与者发出 abort 请求。
事务参与者使用预提交阶段中的 undo 信息执行回滚操作,并释放整个事务期间占用的资源。
所有事务参与者向事务协调者响应回滚事务完成的 ack 消息。
事务协调者收到所有事务参与者响应的 ack 消息后,完成事务中断。
方案小结
优点:3PC相比2PC,会先询问事务参与者是否有条件接事务,不会直接锁资源,降低了阻塞范围,在等待超时后事务协调者或事务参与者会中断事务。避免了事务协调者单点故障问题,3PC的提交阶段中即使事务协调者出现问题时,事务参与者会继续提交事务。
缺点:引入一个阶段,多一次交互,性能比2PC更低,且绝大部分情况事务参与者都由能执行事务的条件,仍要先询问一次。而且数据不一致问题依然存在,当在参与者收到 preCommit 请求后等待 do commite 指令时,此时如果协调者请求中断事务,而协调者无法与参与者正常通信,会导致参与者继续提交事务,造成数据不一致。
TCC
方案简介
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 个方法均由业务编码实现:
Try 操作作为一阶段,负责资源的检查、预留和锁定。
Confirm 操作相当于2PC的提交阶段,执行真正的业务。
Cancel 是预留资源的取消,可以理解为撤销预留阶段的动作。
流程分析
为了方便理解,下面以电商下单为例进行方案解析,这里把整个过程简单分为扣减库存,订单创建 2 个步骤,库存服务和订单服务分别在不同的服务器节点上。
Try 阶段
从执行阶段来看,与传统事务机制中业务逻辑相同。但从业务角度来看,却不一样。
TCC 机制中的 Try 仅是一个初步操作,它和后续的确认一起才能真正构成一个完整的业务逻辑,这个阶段主要完成:
完成所有业务检查工作( 一致性 ) 。
预留锁定必须业务资源( 准隔离性 ) 。
Try 尝试执行业务。
TCC 事务机制是以初步操作(Try)为中心的,确认操作(Confirm)和取消操作(Cancel)都是围绕初步操作(Try)而展开。因此,Try 阶段中的操作,其保障性是最好的,即使失败,仍然有取消操作(Cancel)可以将其执行结果撤销。
假设商品库存为 100,购买数量为 2,这里检查和更新库存的同时,冻结用户购买数量的库存,同时创建订单,订单状态为待确认。
Confirm阶段
Confirm:当 Try 阶段服务全部正常执行, 执行确认业务逻辑操作。Try 阶段所有分支事务执行成功后开始执行 Confirm。通常情况下,采用 TCC 则认为 Confirm 阶段是不会出错的。即:只要 Try 成功,Confirm 一定成功,若 Confirm 阶段真的出错了,需引入重试机制或人工处理,所以 Confirm 操作需要满足幂等性。
Confirm 阶段也可以看成是对 Try 阶段的一个补充,Try + Confirm 一起组成了一个完整的业务逻辑,Confirm 阶段使用的资源一定是 Try 阶段预留的业务资源。
Cancel 阶段
Cancel:当 Try 阶段存在服务执行失败需要回滚, 进入 Cancel 阶段,执行分支事务的业务取消,释放Try阶段中的预留业务员资源。通常情况下,采用 TCC 则认为 Cancel 阶段也是一定成功的。若 Cancel 阶段真的出错了,需引入重试机制或人工处理,所以 Cancel 操作需要满足幂等性。
方案小结
TCC 事务机制相对于传统事务机制(X/Open XA),有以下优点:
性能提升:让应用自己定义数据操作的粒度,使得降低锁冲突、不会锁定整个资源,提高吞吐量。
适用范围广:基于业务实现,TCC 可以跨数据库,跨不同业务系统实现事务。
最终一致性:基于 Confirm 和 Cancel 的幂等性,保证事务最终完成确认或者取消,保证数据的最终一致性。
可靠性:解决了 XA 协议的协调者单点故障问题,由主业务方发起并控制整个业务活动,业务活动管理器也变成多点,引入集群。
缺点:
TCC 的 Try、Confirm 和 Cancel 操作功能要按具体业务来实现,业务耦合度较高,提高了开发成本。
这三个操作方法的实现有一定难度,需要按照网络状态、系统故障等不同的失败原因实现不同的回滚策略。
本地消息表
方案简介
本地消息表的方案最初是由 eBay 提出,该方案基于可靠消息最终一致性方案。可靠消息最终一致性方案是指当事务发起方执行完成本地事务后并发出一条消息,事务参与方(消息消费者)一定能够接收消息并处理事务成功,此方案强调的是只要消息发给事务参与方最终事务要达到一致。
本地消息表核心思路是利用各系统本地的事务来实现分布式事务。通过在事务主动发起方额外新建事务消息表,事务发起方处理业务和记录事务消息在本地事务中完成,轮询事务消息表的数据发送事务消息,事务被动方基于消息中间件消费事务消息表中的事务。
事务消息表与业务数据表处于同一个数据库中,然后在执行业务的时候 <mark style="box-sizing: border-box; background: rgb(235, 255, 235); color: rgb(34, 34, 34); border-radius: 2px; padding: 2px 4px; margin: 0px 2px; font-weight: 500; text-indent: 0px;"><u style="box-sizing: border-box; text-indent: 0px;">将业务的执行和将消息放入消息表中的操作放在同一个事务中</u></mark>,保证消息放入本地表中业务肯定是执行成功的,这样就能利用本地事务来保证在对这两个表的操作满足事务特性,并且使用了消息队列来保证最终一致性。这样设计可以避免”业务处理成功 + 事务消息发送失败",或"业务处理失败 + 事务消息发送成功"的棘手情况出现,保证 2 个系统事务的数据一致性。
流程分析
把分布式事务最先开始处理的事务方称为事务主动方,在事务主动方之后处理的业务内的其他事务称为事务被动方。为了方便理解,以电商下单为例进行方案解析,这里把整个过程简单分为扣减库存,订单创建 2 个步骤。
库存服务和订单服务分别在不同的服务器节点上,其中库存服务是事务主动方,订单服务是事务被动方。事务的主动方需要额外新建事务消息表,用于记录分布式事务的消息的发生、处理状态。
整个业务处理流程如下图:
-
事务主动方处理本地事务。
事务主动方在本地事务中处理业务更新操作和写消息表操作。上面例子中库存服务阶段在本地事务中完成扣减库存和写消息表(图中 1、2)。
-
事务主动方通过消息中间件,通知事务被动方处理事务通知事务待消息。
消息中间件可以基于 Kafka、RocketMQ 等消息队列,事务主动方主动写消息到消息队列,事务消费方消费并处理消息队列中的消息。
库存服务把事务待处理消息写到消息中间件,订单服务消费消息中间件的消息,完成新增订单(图中 3 - 5)。
-
事务被动方通过消息中间件,通知事务主动方事务已处理的消息。
订单服务把事务已处理消息写到消息中间件,库存服务消费中间件的消息,并将事务消息的状态更新为已完成(图中 6 - 8)。
为了数据的一致性,当处理错误需要重试,事务发送方和事务接收方相关业务处理需要支持幂等,而且一般重试会有最大次数,超过最大次数可以记录下报警让人工处理。可以看到本地消息表其实实现的是最终一致性,容忍了数据暂时不一致的情况。具体保存一致性的容错处理如下:
当步骤 1 处理出错,事务回滚,相当于什么都没发生。
当步骤 2、步骤 3 处理出错,由于未处理的事务消息还是保存在事务发送方,事务发送方可以定时轮询读取本地消息表中的超时消息数据,再次发送到消息中间件进行处理。事务被动方消费事务消息重试处理。
如果是业务上的失败,事务被动方可以发消息给事务主动方进行回滚。
如果多个事务被动方已经消费消息,事务主动方需要回滚事务时需要通知事务被动方回滚。
方案小结
本地消息表的优点如下:
从应用设计开发的角度实现了事务参与方接收消息数据的可靠性,消息数据的可靠性不依赖于消息中间件,弱化了对 MQ 中间件特性的依赖。
引入消息机制后,同步的事务操作变为基于消息执行的异步操作, 避免了分布式事务中的同步阻塞操作的影响,并实现了两个服务的解耦(非业务解耦)。
方案轻量,容易实现。
缺点如下:
与具体的业务场景绑定,耦合性强,不可公用。
消息数据与业务数据同库,占用业务系统资源。
业务系统在使用关系型数据库的情况下,消息服务性能会受到关系型数据库并发性能的局限。
MQ 事务消息
方案简介
MQ事务消息方案可以算是最大努力通知方案,最大努力通知方案的目标:发起通知方通过一定的机制最大努力将业务处理结果通知到接收方。具体包括:
有一定的消息重复通知机制。因为接收通知方可能没有接收到通知,此时要有一定的机制对消息重复通知。
消息校对机制。如果尽最大努力也没有通知到接收方,或者接收方消费消息后要再次消费,此时可由接收方主动向通知方查询消息信息来满足需求。
所以最大努力通知其实只是表明了一种柔性事务的思想:我已经尽力我最大的努力想达成事务的最终一致了。
基于 MQ 的分布式事务方案其实是对本地消息表的封装,将本地消息表基于 MQ 内部,其他方面的协议基本与本地消息表一致。有一些第三方的MQ是支持事务消息的,比如RocketMQ,他们支持事务消息的方式也是类似于采用的二阶段提交,但是市面上一些主流的MQ都是不支持事务消息的,比如 RabbitMQ 和 Kafka 都不支持。
流程分析
下面主要基于 RocketMQ 介绍 MQ 的分布式事务方案。
在本地消息表方案中,保证事务主动方发写业务表数据和写消息表数据的一致性是基于数据库事务,RocketMQ 的事务消息相对于普通 MQ,相当于提供了 2PC 的提交接口,第一阶段Prepared消息,会拿到消息的地址。 第二阶段执行本地事务,第三阶段通过第一阶段拿到的地址去访问消息,并修改状态。方案如下:
- 正常情况:事务主动方发消息
这种情况下,事务主动方服务正常,没有发生故障,发消息流程如下:
发送方向 MQ 服务端(MQ Server)发送事务消息即半消息(half),半消息不是说一半消息,而是这个消息对消费者来说不可见(图中 1)。
MQ Server 将消息持久化成功之后,向发送方 ack 确认消息已经发送成功(图中 2)。
发送方开始执行本地事务逻辑(图中 3)。
发送方根据本地事务执行结果向 MQ Server 提交二次确认(commit 或是 rollback)(图中 4)。
MQ Server 收到 commit 状态则将半消息标记为可投递,订阅方最终将收到该消息;MQ Server 收到 rollback 状态则删除半消息,订阅方将不会接受该消息(图中 5)。
- 异常情况:事务主动方消息恢复
有断网或者应用重启等异常情况,流程如下:
提交的二次确认超时未到达 MQ Server(图中 4)。
MQ Server 对该消息发起消息回查(图中 5)。
发送方收到消息回查后,需要检查对应消息的本地事务执行的最终结果(图中 6)。
发送方根据检查得到的本地事务的最终状态再次提交二次确认(图中 7)。
MQ Server基于 commit/rollback 对消息进行投递或者删除(图中 8)。
介绍完 RocketMQ 的事务消息方案后,由于前面已经介绍过本地消息表方案,这里就简单介绍 RocketMQ 分布式事务:
方案小结
最大努力通知与可靠消息一致性有什么不同?
-
解决方案思想不同
可靠消息一致性,发起通知方需要保证将消息发出去,并且将消息发到接收通知方,消息的可靠性关键由发起通知方来保证。最大努力通知,发起通知方尽最大的努力将业务处理结果通知为接收通知方,但是可能消息接收不到,此时需要接 收通知方主动调用发起通知方的接口查询业务处理结果,通知的可靠性关键在接收通知方。
-
两者的业务应用场景不同
可靠消息一致性关注的是交易过程的事务一致,以异步的方式完成交易。最大努力通知关注的是交易后的通知事务,即将交易结果可靠的通知出去。
-
技术解决方向不同
可靠消息一致性要解决消息从发出到接收的一致性,即消息发出并且被接收到。最大努力通知无法保证消息从发出到接收的一致性,只提供消息接收的可靠性机制。可靠机制是,最大努力的将消息通知给接收方,当消息无法被接收方接收时,由接收方主动查询消息(业务处理结果)
MQ事务消息方案相比本地消息表方案,其优点是:
消息数据独立存储 ,降低业务系统与消息系统之间的耦合。
系统吞吐量高于本地消息表方案。
缺点是:
一次消息发送需要两次网络请求(half 消息 + commit/rollback 消息)。
业务处理服务需要实现消息状态回查接口。
Saga 事务
方案简介
Saga 事务源于 1987 年普林斯顿大学的 Hecto 和 Kenneth 发表的如何处理 long lived transaction(长活事务)论文。
Saga 事务核心思想是将长事务拆分为多个本地短事务组成,由 Saga 事务协调器协调,每个本地事务有相应的执行模块和补偿模块,如果正常结束那就正常完成,当 Saga 事务中的任意一个本地事务出错了, 可以根据相反顺序调用相关事务对应的补偿方法恢复,达到事务的最终一致性。在服务请求的过程中,可能会出现超时重试的情况,需要通过幂等来避免多次请求所带来的问题。
ACID与Saga
ACID (刚性事务) | Saga 只提供ACD保证 |
---|---|
原子性(Atomicity) | 原子性(通过Saga协调器实现) |
一致性(Consistency) | 一致性(本地事务 + Saga Log) |
隔离性(Isolation) | 隔离性(Saga 不保证) |
持久性(Durability) | 持久性(Saga Log) |
流程分析
Saga 事务基本协议如下:
每个 Saga 事务由一系列幂等的有序子事务(sub-transaction) Ti 组成。
每个 Ti 都有对应的幂等补偿动作 Ci,补偿动作用于撤销 Ti 造成的结果。
可以看到,和 TCC 相比,Saga 没有“预留”动作,它的 Ti 就是直接提交到库。
Saga 的执行顺序有两种,如下图。下面以下单流程为例,整个操作包括:创建订单、扣减库存、支付、增加积分。
事务正常执行完成:T1, T2, T3, ..., Tn,例如:扣减库存(T1),创建订单(T2),支付(T3),依次有序完成整个事务。
事务回滚:T1, T2, ..., Tj, Cj,..., C2, C1,其中 0 < j < n,例如:扣减库存(T1),创建订单(T2),支付(T3,支付失败),支付回滚(C3),订单回滚(C2),恢复库存(C1)。
Saga 定义了两种恢复策略:
- 向前恢复(forward recovery):对应于上面第一种执行顺序,适用于必须要成功的场景,发生失败进行重试,执行顺序是类似于这样的:T1, T2, ..., Tj(失败), Tj(重试),..., Tn,其中j是发生错误的子事务(sub-transaction)。该情况下不需要Ci。如下图:
- 向后恢复(backward recovery):对应于上面提到的第二种执行顺序,其中 j 是发生错误的子事务(sub-transaction),这种做法的效果是撤销掉之前所有成功的子事务,使得整个 Saga 的执行结果撤销。
Saga 事务常见的有两种不同的实现方式:
- 集中式的实现方式 - 命令协调(Order Orchestrator):中央协调器负责集中处理事件的决策和业务逻辑排序。
中央协调器(Orchestrator,简称 OSO)以命令/回复的方式与每项服务进行通信,全权负责告诉每个参与者该做什么以及什么时候该做什么。
以电商订单的例子为例:
事务发起方的主业务逻辑请求 OSO 服务开启订单事务
OSO 向库存服务请求扣减库存,库存服务回复处理结果。
OSO 向订单服务请求创建订单,订单服务回复创建结果。
OSO 向支付服务请求支付,支付服务回复处理结果。
主业务逻辑接收并处理 OSO 事务处理结果回复。
中央协调器必须事先知道执行整个订单事务所需的流程(例如通过读取配置)。如果有任何失败,它还负责通过向每个参与者发送命令来撤销之前的操作来协调分布式的回滚。
基于中央协调器协调一切时,回滚要容易得多,因为协调器默认是执行正向流程,回滚时只要执行反向流程即可。
- 分布式的实现方式 - 事件编排(Event Choreography0):没有中央协调器(没有单点风险)时,每个服务产生并观察其他服务的事件,并决定是否应采取行动。
在事件编排方法中,第一个服务执行一个事务,然后发布一个事件。该事件被一个或多个服务进行监听,这些服务再执行本地事务并发布(或不发布)新的事件。
当最后一个服务执行本地事务并且不发布任何事件时,意味着分布式事务结束,或者它发布的事件没有被任何 Saga 参与者听到都意味着事务结束。
以电商订单的例子为例:
事务发起方的主业务逻辑发布开始订单事件。
库存服务监听开始订单事件,扣减库存,并发布库存已扣减事件。
订单服务监听库存已扣减事件,创建订单,并发布订单已创建事件。
支付服务监听订单已创建事件,进行支付,并发布订单已支付事件。
主业务逻辑监听订单已支付事件并处理。
事件编排是实现 Saga 模式的自然方式,它很简单,容易理解,不需要太多的代码来构建。如果事务涉及 2 至 4 个步骤,则可能是非常合适的。
隔离性
由于 Saga 模型中只支持ACD,没有 Prepare 阶段,因此事务间不能保证隔离性。
当多个 Saga 事务操作同一资源时,就会产生数据语义不一致、更新丢失、脏数据读取等问题。需要在业务层控制并发,解决方案如下:
在应用层面加入逻辑锁。
Session层面隔离保证串行化操作。
业务层面预先冻结资源数据。
业务操作过程中通过及时读取当前状态的方式获取更新。
方案小结
命令协调设计的优点如下:
服务之间关系简单,避免服务之间的循环依赖关系,因为 Saga 协调器会调用 Saga 参与者,但参与者不会调用协调器。
程序开发简单,只需要执行命令/回复(其实回复消息也是一种事件消息),降低参与者的复杂性。
易维护扩展,在添加新步骤时,事务复杂性保持线性,回滚更容易管理,更容易实施和测试,易于监控和协调。
命令协调设计缺点如下:
中央协调器容易处理逻辑容易过于复杂,导致难以维护。
存在协调器单点故障风险。
事件编排设计优点如下:
避免中央协调器单点故障风险。
当涉及的步骤较少服务开发简单,容易实现。
事件/编排设计缺点如下:
服务之间存在循环依赖的风险。
当涉及的步骤较多,服务间关系混乱,难以追踪调测。
Seata 方案
方案简介
Seata 是由阿里中间件团队发起的开源项目 Fescar,后更名为 Seata,它是一个是开源的分布式事务框架。
传统 2PC 的问题在 Seata 中得到了解决,它通过对本地关系数据库的分支事务的协调来驱动完成全局事务,是工作在应用层的中间件。主要优点是性能较好,且不长时间占用连接资源,它以高效并且对业务 0 侵入的方式解决微服务场景下面临的分布式事务问题,它目前提供 AT 模式(即 2PC)及 TCC 模式的分布式事务解决方案。
设计思想
Seata 的设计目标其一是对业务无侵入,因此从业务无侵入的 2PC 方案着手,在传统 2PC的基础上演进,并解决 2PC 方案面临的问题。
Seata 把一个分布式事务理解成一个包含了若干分支事务的全局事务。全局事务的职责是协调其下管辖的分支事务达成一致,要么一起成功提交,要么一起失败回滚。此外,通常分支事务本身就是一个关系数据库的本地事务,下图是全局事务与分支事务的关系图:
与传统 2PC 的模型类似,Seata 定义了 3 个组件来协议分布式事务的处理过程:
Transaction Coordinator(TC):事务协调器,它是独立的中间件,需要独立部署运行,它维护全局事务的运行状态,接收 TM 指令发起全局事务的提交与回滚,负责与 RM 通信协调各各分支事务的提交或回滚。
Transaction Manager(TM): 事务管理器,TM 需要嵌入应用程序中工作,它负责开启一个全局事务,并最终向 TC 发起全局提交或全局回滚的指令。
Resource Manager(RM):控制分支事务,负责分支注册、状态汇报,并接收事务协调器 TC 的指令,驱动分支(本地)事务的提交和回滚。
流程分析
拿新用户注册送积分举例,简单分析Seata的分布式事务过程:
用户服务的 TM 向 TC 申请开启一个全局事务,全局事务创建成功并生成一个全局唯一的 XID。
用户服务的 RM 向 TC 注册分支事务,该分支事务在用户服务执行新增用户逻辑,并将其纳入 XID 对应全局事务的管辖。
用户服务执行分支事务,向用户表插入一条记录。
逻辑执行到远程调用积分服务时(XID 在微服务调用链路的上下文中传播)。积分服务的 RM 向 TC 注册分支事务,该分支事务执行增加积分的逻辑,并将其纳入 XID 对应全局事务的管辖。
积分服务执行分支事务,向积分记录表插入一条记录,执行完毕后,返回用户服务。
用户服务分支事务执行完毕。
TM 向 TC 发起针对 XID 的全局提交或回滚决议。
TC 调度 XID 下管辖的全部分支事务完成提交或回滚请求。
方案小结
Seata方案与传统 2PC 方案对比,有以下优点:
架构层次方面:传统 2PC 方案的 RM 实际上是在数据库层,RM 本质上就是数据库自身,通过 XA 协议实现,而 Seata 的 RM 是以 jar 包的形式作为中间件层部署在应用程序这一侧的。
二阶段提交方面:传统 2PC无论提交阶段的决议是 commit 还是 rollback ,事务性资源的锁都要保持到 Phase2 完成才释放。而 Seata 的做法是在 Phase1 就将本地事务提交,这样就可以省去 Phase2 持锁的时间,整体提高效率。
由于 Seata 的 0 侵入性并且解决了传统 2PC 长期锁资源的问题,推荐采用 Seata 实现 2PC。
总结
方案对比
各方案使用场景
介绍完分布式事务相关理论和常见解决方案后,最终的目的在实际项目中运用,因此,总结一下各个方案的常见的使用场景:
2PC/3PC:依赖于数据库,能够很好的提供强一致性和强事务性,但相对来说延迟比较高,比较适合传统的单体应用,在同一个方法中存在跨库操作的情况,不适合高并发和高性能要求的场景。
TCC:适用于执行时间确定且较短,实时性要求高,对数据一致性要求高,比如互联网金融企业最核心的三个服务:交易、支付、账务。
本地消息表/MQ 事务:都适用于事务中参与方支持操作幂等,对一致性要求不高,业务上能容忍数据不一致到一个人工检查周期,事务涉及的参与方、参与环节较少,业务上有对账/校验系统兜底。
Saga 事务:由于 Saga 事务不能保证隔离性,需要在业务层控制并发,适合于业务场景事务并发操作同一资源较少的情况。 Saga 相比缺少预提交动作,导致补偿动作的实现比较麻烦,例如业务是发送短信,补偿动作则得再发送一次短信说明撤销,用户体验比较差。Saga 事务较适用于补偿动作容易处理的场景。