分布式理论与分布式事务
基础理论
CAP
CAP理论又称为布鲁尔定理, 它指出对于一个分布式计算系统来说,不可能同时满足以下三点:
- 一致性(Consistency) (等同于所有节点访问同一份最新的数据副本)
- 可用性(Availability)(每次请求都能获取到非错的响应——但是不保证获取的数据为最新数据)
- 分区容错性(Partition tolerance)(以实际效果而言,分区相当于对通信的时限要求。系统如果不能在时限内达成数据一致性,就意味着发生了分区的情况,必须就当前操作在C和A之间做出选择)
Consistency 一致性
一致性指更新操作成功并返回客户端完成后,所有节点在同一时间的数据完全一致,所以说的就是数据一致性
对于一致性,可以分为从客户端和服务端两个不同的视角。从客户端来看,一致性主要指的是多并发访问时更新过的数据如何获取的问题。从服务端来看,则是更新如何复制分布到整个系统,以保证数据最终一致。
三种一致性策略:
- 对于关系型数据库,要求更新过的数据能被后续的访问都能看到,这是强一致性
- 如果能容忍后续的部分或者全部访问不到,则是弱一致性。
- 如果经过一段时间后要求能访问到更新后的数据,则是最终一致性
CAP中说,不可能同时满足的这个一致性指的是强一致性
Availability 可用性
可用性指服务一直可用,而且是正常响应时间
对于一个分布式系统,可用性一般都是通过停机时间来衡量的(常说的是系统的可用性是几个9)
Partition Tolerance 分区容错性
分区容错性指分布式系统在遇到某节点或网络分区故障的时候,仍然能够对外提供满足一致性和可用性的服务。
CAP权衡
在分布式系统中,CAP三者目前是无法同时满足的,所以我们要在不同的业务场景中做不同的权衡
- CA
这个情况基本上是不会选择的,因为分布式系统下,网络分区是一个必然的选项。如果要舍弃P,那就要舍弃分布式系统, 所以我们一般都是在CP和AP上做选择
- CP
如果系统选择不要可用性,即容许系统停机或者长时间无响应的话,就可以在CAP三者中保障CP而舍弃A。一个保证了CP而一个舍弃了A的分布式系统,一旦发生网络故障或者消息丢失等情况,就要牺牲用户的体验,等待所有数据全部一致了之后再让用户访问系统。
设计成CP的系统其实也不少,其中最典型的就是很多分布式数据库,他们都是设计成CP的。在发生极端情况时,优先保证数据的强一致性,代价就是舍弃系统的可用性。如Redis、HBase等,还有分布式系统中常用的Zookeeper也是在CAP三者之中选择优先保证CP的。
- AP
要高可用并允许分区,则需放弃一致性。一旦网络问题发生,节点之间可能会失去联系。为了保证高可用,需要在用户访问时可以马上得到返回,则每个节点只能用本地数据提供服务,而这样会导致全局数据的不一致性。
我们这里说的舍弃一致性,其实舍弃的是强一致性,退而求其次保证最终一致性
BASE
BASE是对CAP中一致性和可用性权衡的结果,其来源于对大规模互联网分布式系统实践的总结,是基于CAP定律逐步演化而来。其核心思想是即使无法做到强一致性,但每个应用都可以根据自身业务特点,才用适当的方式来使系统打到最终一致性。
BASE理论是Basically Available(基本可用),Soft State(软状态)和Eventually Consistent(最终一致性)三个短语的缩写。
基本可用 Basically Available
基本可用指的是,系统出现了不可预知的故障,但还是能用,但相对于正常的系统来说:
- 响应时间上的损失, 会比正常响应慢
- 功能上的损失,高峰期间,采取一些措施,部分用户会得不到正常完整功能(限流、降级)
软状态 Soft State
相对于原子性而言,要求多个节点的数据副本都是一致的,这是一种“硬状态”。
软状态指的是:允许系统中的数据存在中间状态,并认为该状态不影响系统的整体可用性,即允许系统在多个不同节点的数据副本存在数据延时。
最终一致性 Monotonic write consistency
上文的软状态只是一个中间状态,必须要有个时间期限,期限过后,应当保证所有副本保持数据一致性,从而达到最终一致性。这个期限取决于网络延时、系统负载、数据复制方案等
分布式事务-强一致性方案
2PC(2 Phase Commitment Protocol) 两段式提交协议
2PC协议,分为两个阶段提交一个事务,通过协调者和各个参与者的配合,实现分布式一致性。
两个阶段指的是:
- 第一阶段: 准备阶段
- 提交阶段
XA规范
XA规范是一个分布式事务处理模型,包括:
- 应用程序(AP)
- 事务管理器(TM): 交易中间件等
- 资源管理器(RM): 关系数据库等
- 通信资源管理器(CRM): 消息中间件等
XA规范定义了交易中间件和数据库之间的接口规范,交易中间件用它来通知数据库事务的开始、结束以及提交、回滚等。而XA接口函数由数据库厂商提供。
1.准备阶段
准备阶段分为三个步骤:
- 事务询问:协调者向所有的参与者询问,是否准备好了执行事务,并开始等待各参与者的响应
- 执行事务:各参与者节点执行事务操作。如果本地事务成功,将Undo和Redo信息记入事务日志中,但不提交;否则,直接返回失败,退出执行。
- 各个参与者向协调者反馈事务询问的响应:如果参与者成功执行了事务操作,那么就反馈给协调者 Yes响应,表示事务可以执行提交;如果参与者没有成功执行事务,就返回No给协调者,表示事务不可以执行提交
2.提交阶段
- 发送提交请求: 协调者向所有参与者发出commit请求。
- 事务提交: 参与者收到commit请求后,会正式执行事务提交操作,并在完成提交之后,释放整个事务执行期间占用的事务资源
- 反馈事务提交结果: 参与者在完成事务提交之后,向协调者发送Ack信息
- 事务提交确认: 协调者接收到所有参与者反馈的Ack信息后,完成事务
中断事务的流程如下:
- 发送回滚请求: 协调者向所有参与者发出Rollback请求
- 事务回滚: 参与者接收到Rollback请求后,会利用其在提交阶段种记录的Undo信息,来执行事务回滚操作。在完成回滚之后,释放在整个事务执行期间占用的资源
- 反馈事务回滚结果: 参与者在完成事务回滚之后,想协调者发送Ack信息
- 事务中断确认: 协调者接收到所有参与者反馈的Ack信息后,完成事务中断。
两阶段提交的优缺点
优点: 原理简单,实现方便
缺点:
- 同步阻塞:在第二阶段提交过程中,所有节点都在等其他节点响应,无法进行其他操作,这种同步阻塞限制了分布式系统的性能
- 单点问题:协调者如果出现了问题,整个流程就无法进行
- 数据不一致:协调者向所有的参与者发送commit请求之后,发生了局部网络异常,或者是协调者在尚未发送完所有 commit请求之前自身发生了崩溃,导致最终只有部分参与者收到了commit请求。这将导致严重的数据不一致问题。
- 容错性不好:如果在二阶段提交的提交询问阶段中,参与者出现故障,导致协调者始终无法获取到所有参与者的确认信息,这时协调者只能依靠其自身的超时机制,判断是否需要中断事务。显然,这种策略过于保守。换句话说,二阶段提交协议没有设计较为完善的容错机制,任意一个节点是失败都会导致整个事务的失败。
3PC
由于2PC存在的单点、阻塞等问题, 在2PC的基础上做了改进, 提出了三阶段提交, 与两阶段提交不同的是,三阶段提交有两个改动点。
- 引入超时机制 - 同时在协调者和参与者中都引入超时机制
- 把原本的2PC的准备阶段再细分为两个阶段: 将准备阶段一分为二的理由是,这个阶段是重负载的操作,一旦协调者发出开始准备的消息,每个参与者都将马上开始写重做日志,这时候涉及的数据资源都会被锁住。如果此时某一个参与者无法完成提交,相当于所有的参与者都做了一轮无用功。所以,增加一轮询问阶段,如果都得到了正面的响应,那事务能够成功提交的把握就比较大了,也意味着因某个参与者提交时发生崩溃而导致全部回滚的风险相对变小
- 第一阶段: CanCommit(询问阶段)
- 第二阶段: PreCommit(预提交,锁定资源)
- 第三阶段: Do Commit(提交)
1. Can Commit(询问)
- 事务询问: 协调者向参与者发送CanCommit请求。询问是否可以执行事务提交操作。然后开始等待参与者的响应。
- 响应反馈: 参与者接到CanCommit请求之后,正常情况下,如果其自身认为可以顺利执行事务,则返回Yes响应,并进入预备状态;否则反馈No。
2. Pre Commit(预提交)
- 发送预提交请求:协调者向所有参与者节点发出 preCommit 的请求,并进入 prepared 状态。
- 事务预提交: 参与者受到 preCommit 请求后,会执行事务操作,对应 2PC 准备阶段中的 “执行事务”,也会 Undo 和 Redo 信息记录到事务日志中
- 参与者反馈: 如果参与者成功执行了事务,就反馈 ACK 响应,同时等待指令:提交(commit) 或终止(abort)。
3. Do Commit
- 发送提交请求:协调者接收到各参与者发送的ACK响应,那么他将从预提交状态进入到提交状态。并向所有参与者发送 doCommit 请求。
- 事务提交: 参与者接收到 doCommit 请求之后,执行正式的事务提交。并在完成事务提交之后释放所有事务资源。
- 响应反馈: 事务提交完之后,向协调者发送 ACK 响应。
- 完成事务: 协调者接收到所有参与者的 ACK 响应之后,完成事务
协调者没有接收到参与者发送的 ACK 响应(可能是接受者发送的不是ACK响应,也可能响应超时),那么就会执行中断事务
- 发送中断请求: 协调者向所有参与者发送 abort 请求。
- 事务回滚: 参与者接收到 abort 请求之后,利用其在阶段二记录的 undo 信息来执行事务的回滚操作,并在完成回滚之后释放所有的事务资源
- 反馈结果: 参与者完成事务回滚之后,向协调者发送 ACK 消息。
- 中断事务: 协调者接收到参与者反馈的 ACK 消息之后,完成事务的中断。
分布式事务-最终一致性方案
本地事务状态表
本地事务状态表方案的大概处理流程是:
- 在调用方请求外部系统前将待执行的事务流程及其状态信息存储到数据库中,依赖数据库本地事务的原子特性保证本地事务和调用外部系统事务的一致性,这个存储事务执行状态信息的表称为本地事务状态表。
- 在将事务状态信息存储到DB后,调用方才会开始继续后面流程,同步调用外部系统,并且每次调用成功后会更新相应的子事务状态,某一步失败时则中止执行。
- 同时在后台运行一个定时任务,定期扫描事务状态表中未完成的子事务,并重新发起调用,或者执行回滚,或者在失败重试指定次数后触发告警让人工介入进行修复
TCC
TCC相较于XA(2PC或3PC)机制,解决了几个问题:
- 解决了协调者单点问题, 由由主业务方发起并完成这个业务活动。业务活动管理器也变成多点,引入集群
- 同步阻塞:引入超时,超时后进行补偿,并且不会锁定整个资源,将资源转换为业务逻辑形式,粒度变小
- 数据一致性,有了补偿机制之后,由业务活动管理器控制一致性
一个由两台服务器一起参与的事务,服务器A发起事务,服务器B参与事务。务器A的事务如果执行顺利,那么事务A就先行提交,如果事务B也执行顺利,则事务B也提交,整个事务就算完成。但是如果事务B执行失败,事务B本身回滚,这时事务A已经被提交,所以需要执行一个补偿操作,将已经提交的事务A执行的操作作反操作,恢复到未执行前事务A的状态。
TCC过程
- Try:
- 完成所有业务检查(一致性)
- 预留必须业务资源(准隔离性,冻结操作)
- Confirm
- 不作任何业务检查, 直接使用 Try 阶段准备好的资源来完成业务处理
- Confirm 可能会重复执行, 要满足幂等性
- Cancel:
- 释放Try阶段预留的业务资源
- Cancel 可能重复执行, 要满足幂等性
TCC总结
TCC 相较于可靠消息队列方案满足了隔离性, 但是TCC事务的使用严格依赖业务人员写的代码来回滚和补偿,很复杂, 使用的场景也不是很多。主要是在支付、交易相关的强一致性场景。
可靠消息队列
可靠消息队列方案是指当事务发起方执行完成本地事务后并发出一条消息,事务参与方(消息消费者)一定能够接收到消息并处理事务成功,此方案强调的是只要消息发给事务参与方,则最终事务要达到一致
可靠消息最终一致性方案要解决以下几个问题:
- 本地事务与消息发送的原子性问题:要求事务发起方在本地事务执行成功后消息必须发出去,否则就丢弃消息
- 事务参与方接收消息的可靠性:要求事务参与方必须能够从消息队列接收到消息,如果接收消息失败可以重复接收消息
- 消息重复消费的问题:要解决消息重复消费的问题就要实现事务参与方的方法幂等性。
- 目前主要的解决方案有2种,一种是本地消息表方案,一种是事务消息方案。
本地消息表
本地消息表是一种最终一致性的分布式事务处理方案, 适用于不需要强一致性的场景。
- A 系统操作自己本地事务的时候,同时插入一个消息到消息表
- 接着A系统把这个消息发送到MQ中
- B系统接收到MQ中的消息,先写入B系统的消息表,然后执行事务,事务执行成功后,更新B系统和A消息表的状态。
- A系统会定时扫描自己的消息表,如果有未处理的消息,将再次发送到mq让b系统处理
这种方案验证依赖于数据的消息表,在高并发场景下则不适用,数据库承受不了这么大的并发量
事务消息
这种方案不需要本地消息表了, 直接基于MQ来实现事务, 例如RocketMQ就支持消息事务
消息中间件如果收到 Comfirm 消息,则会将消息转为对消费者可见,并开始投递;如果收到 Rollback 消息,则会删除之前的事务消息;如果未收到确认消息,则会通过事务回查机制定时检查本地事务的状态,决定是否可以提交投递。
这种方案依赖于一种可靠的消息队列,确保消息被成功消息
最大努力通知
最大努力通知方案( Best-effort delivery)是最简单的一种柔性事务,适用于一些最终一致性时间敏感度低的业务,且被动方处理结果不影响主动方的处理结果。典型的使用场景:如银行通知、商户通知等。最大努力通知型的实现方案,一般符合以下特点:
不可靠消息:业务活动主动方,在完成业务处理之后,向业务活动的被动方发送消息,直到通知N次后不再通知,允许消息丢失(不可靠消息)。
定期校对:业务活动的被动方,根据定时策略,向业务活动主动方查询(主动方提供查询接口),恢复丢失的业务消息。
例如充值场景:
与可靠消息队列方案区别:
解决方案思想不同:
可靠消息一致性,发起通知方需要保证将消息发出去,并且将消息发到接收通知方,消息的可靠性关键由发起通知方来保证。最大努力通知,发起通知方尽最大的努力将业务处理结果通知为接收通知方,但是可能消息接收不到,此时需要接 收通知方主动调用发起通知方的接口查询业务处理结果,通知的可靠性关键在接收通知方。两者的业务应用场景不同:
可靠消息一致性关注的是交易过程的事务一致,以异步的方式完成交易。最大努力通知关注的是交易后的通知事务,即将交易结果可靠的通知出去。技术解决方向不同:
可靠消息一致性要解决消息从发出到接收的一致性,即消息发出并且被接收到。最大努力通知无法保证消息从发出到接收的一致性,只提供消息接收的可靠性机制。可靠机制是,最大努力的将消息通知给接收方,当消息无法被接收方接收时,由接收方主动查询消息(业务处理结果)。
SAGA 事务
大致思路是把一个大事务分解为可以交错运行的一系列子事务的集合。原本提出 SAGA 的目的,是为了避免大事务长时间锁定数据库的资源,后来才逐渐发展成将一个分布式环境中的大事务,分解为一系列本地事务的设计模式。
Saga 事务基本协议如下:
- 每个 Saga 事务由一系列幂等的有序子事务(sub-transaction) T1,T2,…,Ti,…,Tn组成。
- 每个 Ti 都有对应的幂等补偿动作C1,C2,…,Ci,…,Cn,补偿动作用于撤销 T1,T2,…,Ti,…,Tn造成的结果。
如果 T1 到 Tn 均成功提交,那么事务就可以顺利完成。否则,就要采取恢复策略,恢复策略分为向前恢复和向后恢复两种。
向前恢复
如果 Ti 事务提交失败,则一直对 Ti 进行重试,直至成功为止(最大努力交付)。这种恢复方式不需要补偿,适用于事务最终都要成功的场景,比如在别人的银行账号中扣了款,就一定要给别人发货。正向恢复的执行模式为:T1,T2,…,Ti(失败),Ti(重试)…,Ti+1,…,Tn,该情况下不需要Ci。
向后恢复
如果 Ti 事务提交失败,则一直执行 Ci 对 Ti 进行补偿,直至成功为止(最大努力交付)。这里要求 Ci 必须(在持续重试后)执行成功。向后恢复的执行模式为:T1,T2,…,Ti(失败),Ci(补偿),…,C2,C1。
命令协调模式
这种模式由中央协调器(Orchestrator,简称 OSO)集中处理事件的决策和业务逻辑排序,以命令/回复的方式与每项服务进行通信,全权负责告诉每个参与者该做什么以及什么时候该做什么。
以电商订单的例子为例:
- 事务发起方的主业务逻辑请求 OSO 服务开启订单事务
- OSO 向库存服务请求扣减库存,库存服务回复处理结果。
- OSO 向订单服务请求创建订单,订单服务回复创建结果。
- OSO 向支付服务请求支付,支付服务回复处理结果。
- 主业务逻辑接收并处理 OSO 事务处理结果回复。
中央协调器必须事先知道执行整个订单事务所需的流程(例如通过读取配置)。如果有任何失败,它还负责通过向每个参与者发送命令来撤销之前的操作来协调分布式的回滚。
基于中央协调器协调一切时,回滚要容易得多,因为协调器默认是执行正向流程,回滚时只要执行反向流程即可。
事件编排模型
这种模式没有中央协调器(没有单点风险),由每个服务产生并观察其他服务的事件,并决定是否应采取行动。
在事件编排方法中,第一个服务执行一个事务,然后发布一个事件。该事件被一个或多个服务进行监听,这些服务再执行本地事务并发布(或不发布)新的事件。
当最后一个服务执行本地事务并且不发布任何事件时,意味着分布式事务结束,或者它发布的事件没有被任何 Saga 参与者听到都意味着事务结束。
电商订单的例子为例:
- 事务发起方的主业务逻辑发布开始订单事件。
- 库存服务监听开始订单事件,扣减库存,并发布库存已扣减事件。
- 订单服务监听库存已扣减事件,创建订单,并发布订单已创建事件。
- 支付服务监听订单已创建事件,进行支付,并发布订单已支付事件。
- 主业务逻辑监听订单已支付事件并处理。
事件/编排是实现 Saga 模式的自然方式,它很简单,容易理解,不需要太多的代码来构建。如果事务涉及 2 至 4 个步骤,则可能是非常合适的。
SAGA 总结
- 适用场景
- 业务流程长、业务流程多
- 参与者包含第三方或遗留系统服务,无法提供TCC模式要求的三个接口
- 典型业务系统:如金融网络(与外部金融机构对接)、互联网微贷、渠道整合、分布式架构服务集成等业务系统
- 银行业金融机构使用广泛
- 主要优势
- 一阶段提交本地数据库事务,无锁,高性能;
- 参与者可以采用事务驱动异步执行,高吞吐;
- 补偿服务即正向服务的“反向”,易于理解,易于实现;
但是Saga 模式由于一阶段已经提交本地数据库事务,且没有进行“预留”动作,所以不能保证隔离性
总结
各个方案的特点总结如下:
- 2PC(XA)/3PC:依赖于数据库,能够很好的提供强一致性和强事务性,但相对来说延迟比较高,比较适合传统的单体应用,不适合高并发和高性能要求的场景。
- 本地事务状态表:方案轻量,容易实现,但与具体的业务场景耦合较高,不可公用。
- 可靠消息队列:适合执行周期长且实时性要求不高的场景。引入消息机制后,同步的事务操作变为基于消息执行的异步操作, 避免了分布式事务中的同步阻塞操作的影响,并实现了两个服务的解耦。典型的使用场景:注册送积分,登录送优惠券等。
- 最大努力通知:是分布式事务中要求最低的一种,适用于一些最终一致性时间敏感度低的业务;允许发起通知方处理业务失败,在接收通知方收到通知后积极进行失败处理,无论发起通知方如何处理结果都会不影响到接收通知方的后续处理;发起通知方需提供查询执行情况接口,用于接收通知方校对结果。典型的使用场景:银行通知、支付结果通知等。
- TCC:适用于执行时间确定且较短,实时性要求高,对数据一致性要求高,比如互联网金融企业最核心的三个服务:交易、支付、账务。但是对于业务的侵入性非常强,业务逻辑的每个分支都需要实现try、confirm、cancel三个操作。此外,其实现难度也比较大,需要按照网络状态、系统故障等不同的失败原因实现不同的回滚策略。
- SAGA:适合于“业务流程长、业务流程多”的场景。特别是针对参与事务的服务是遗留系统服务。但由于 Saga 事务不能保证隔离性,需要在业务层控制并发,适合于业务场景事务并发操作同一资源较少的情况。另外, Saga 相比缺少预提交动作,导致补偿动作的实现比较麻烦,例如业务是发送短信,补偿动作则得再发送一次短信说明撤销,用户体验比较差。Saga 事务较适用于补偿动作容易处理的场景。