分布式系统的数据一致性


一 什么是事务?

事务一词是新兴词汇,从字面意思来看可能会感觉比较晦涩陌生,但是在软件行业这个词还是听的挺多的.事务无处不在,先来举个小栗子,比如我们要去逛超市购物那么需要先推一辆车再选购商品,再支付,而这整个流程就是事务.再例如支付这个节点也可以分为掏出手机,打开支付宝/微信,完成扫描,支付成功,这一整个过程又可以看作一个事务,其实我们看不到的还有支付宝内部的一连串处理节点,也是其自身的事务等等等……

通过上面的简单举例我们可以看出事务有如下特性:

  • 事务的最终目的是完成某件事或实现某目标
  • 事务可被拆分为更小的组成单元/步骤,即事务是一种特殊的集合
  • 事务集合中任何节点的失败都将造成整个事务的失败

二 事务的起源

提到事务不得不提到「XA规范」,详情可参考XA分布式事务原理


三 CAP理论

事务问题其实一直存在,只是在分布式系统中被放大了。并且随着系统拆分的粒度越细,问题的复杂度成指数上升。

「CAP」理论由Eric Brewer在2000年PODC会议上提出,所以还被称为Brewer定理。是Eric Brewer在Inktomi期间研发搜索引擎、分布式web缓存时得出的一个猜想,后来Seth Gilbert和Nancy Lynch对其进行了证明[3],成为我们熟知的「CAP」定理.

It is impossible for a web service to provide the three following guarantees : Consistency, Availability and Partition-tolerance.

详情可参考CAP理论


四 BASE理论

详情可参考BASE理论


五 分布式事务的常见解决方案-以CAP为基础

CAP理论以及BASE理论只是思想指导,基于这两种理论发展出一系列解决方案,每种方案的侧重点各有不同.

5.1 两阶段提交(2PC)

这是最简单的分布式事务解决方案,通过设立协调者来处理事务不同处理节点的相互关系.

两个阶段的执行

1.请求阶段
在请求阶段,协调者将通知事务参与者准备提交或取消事务,然后进入表决过程。
在表决过程中,参与者将告知协调者自己的决策:同意(事务参与者本地作业执行成功)或取消(本地作业执行故障)。

2.提交阶段
在该阶段,协调者将基于第一个阶段的投票结果进行决策:提交或取消。
当且仅当所有的参与者同意提交事务协调者才通知所有的参与者提交事务,否则协调者将通知所有的参与者取消事务。
参与者在接收到协调者发来的消息后将执行响应的操作。

两阶段提交的缺点

1.同步阻塞问题。执行过程中,所有参与节点都是事务阻塞型的。
当参与者占有公共资源时,其他第三方节点访问公共资源不得不处于阻塞状态。

2.单点故障。由于协调者的重要性,一旦协调者发生故障。参与者会一直阻塞下去。尤其在第二阶段,协调者发生故障,那么所有的参与者还都处于锁定事务资源的状态中,而无法继续完成事务操作。(如果是协调者挂掉,可以重新选举一个协调者,但是无法解决因为协调者宕机导致的参与者处于阻塞状态的问题)

3.数据不一致。在二阶段提交的阶段二中,当协调者向参与者发送commit请求之后,发生了局部网络异常或者在发送commit请求过程中协调者发生了故障,这讲导致只有一部分参与者接受到了commit请求。
而在这部分参与者接到commit请求之后就会执行commit操作。但是其他部分未接到commit请求的机器则无法执行事务提交。于是整个分布式系统便出现了数据部一致性的现象。

两阶段提交无法解决的问题

当协调者出错,同时参与者也出错时,两阶段无法保证事务执行的完整性。
考虑协调者再发出commit消息之后宕机,而唯一接收到这条消息的参与者同时也宕机了。
那么即使协调者通过选举协议产生了新的协调者,这条事务的状态也是不确定的,没人知道事务是否被已经提交。

5.2 三阶段提交协议

三阶段提交协议在协调者和参与者中都引入超时机制,并且把两阶段提交协议的第一个阶段拆分成了两步:询问,然后再锁资源,最后真正提交。

三个阶段的执行

1.CanCommit阶段
3PC的CanCommit阶段其实和2PC的准备阶段很像。
协调者向参与者发送commit请求,参与者如果可以提交就返回Yes响应,否则返回No响应。

2.PreCommit阶段
协调者根据参与者的反应情况来决定是否可以继续事务的PreCommit操作。
根据响应情况,有以下两种可能。
A.假如协调者从所有的参与者获得的反馈都是Yes响应,那么就会进行事务的预执行:
发送预提交请求。协调者向参与者发送PreCommit请求,并进入Prepared阶段。
事务预提交。参与者接收到PreCommit请求后,会执行事务操作,并将undo和redo信息记录到事务日志中。
响应反馈。如果参与者成功的执行了事务操作,则返回ACK响应,同时开始等待最终指令。

B.假如有任何一个参与者向协调者发送了No响应,或者等待超时之后,协调者都没有接到参与者的响应,那么就中断事务:
发送中断请求。协调者向所有参与者发送abort请求。
中断事务。参与者收到来自协调者的abort请求之后(或超时之后,仍未收到参与者的请求),执行事务的中断。

3.DoCommit阶段

该阶段进行真正的事务提交,也可以分为以下两种情况:

执行提交

A.发送提交请求。协调者接收到参与者发送的ACK响应,那么他将从预提交状态进入到提交状态。并向所有参与者发送doCommit请求。
B.事务提交。参与者接收到doCommit请求之后,执行正式的事务提交。并在完成事务提交之后释放所有事务资源。
C.响应反馈。事务提交完之后,向协调者发送ACK响应。
D.完成事务。协调者接收到所有参与者的ACK响应之后,完成事务。

中断事务

协调者没有接收到参与者发送的ACK响应(可能是接受者发送的不是ACK响应,也可能响应超时),那么就会执行中断事务。

三个阶段的特点

  • 3pc中协调者\参与者都进行了超时设置,2pc中只是协调者进行了超时设置,这样避免了协调者出现宕机后照成参与者产生上时间延时的问题.
  • PreCommit是一个缓冲,保证了在最后提交阶段之前各参与节点的状态是一致的
  • “非阻塞协议”:三阶段提交在两阶段提交的第一阶段与第二阶段之间插入了一个准备阶段,
    使得原先在两阶段提交中,参与者在投票之后,由于协调者发生崩溃或错误,
    而导致参与者处于无法知晓是否提交或者中止的“不确定状态”所产生的可能相当长的延时的问题得以解决。
  • 缺点:如果进入DoCommit阶段,由于网络原因,协调者发送的rollback响应没有及时被参与者接收到,那么部分参与者在等待超时之后执行了commit操作,而和其他接到rollback命令并执行回滚的参与者之间存在数据不一致的情况。

5.3 TCC模式

1、当所有try()方法均执行成功时,对全局事物进行提交,即由事物管理器调用每个微服务的confirm()方法
2、 当任意一个方法try()失败(预留资源不足,抑或网络异常,代码异常等任何异常),由事物管理器调用每个微服务的cancle()方法对全局事务进行回滚否则进行comfirm

基本原理

TCC 将事务提交分为 Try - Confirm - Cancel 3个操作。其和两阶段提交有点类似,Try为第一阶段,Confirm或Cancel为第二阶段,是一种应用层面侵入业务的两阶段提交。

操作方法 含义
Try 预留业务资源/数据效验
Confirm 确认执行业务操作,实际提交数据,不做任何业务检查,try成功,confirm必定成功,需保证幂等
Cancel 取消执行业务操作,实际回滚数据,需保证幂等

其核心在于将业务分为两个操作步骤完成。不依赖 RM 对分布式事务的支持,而是通过对业务逻辑的分解来实现分布式事务。

幂等控制

使用TCC时要注意Try - Confirm - Cancel 3个操作的幂等控制,网络原因,或者重试操作都有可能导致这几个操作的重复执行.

如何控制幂等,具体业务场景具体分析,例如在消费业务中由于网络等其它原因导致消费金额被提交了多次,这时如果不控制幂等将导致金额的持续扣除,我们可以接触业务逻辑进行避免,当第一次提交消费时标记以消费,那么第二次提交即可避免重复.

空回滚

如图所示,事务协调器在调用TCC服务的一阶段Try操作时,可能会出现因为丢包而导致的网络超时,此时事务协调器会触发二阶段回滚,调用TCC服务的Cancel操作;TCC服务在未收到Try请求的情况下收到Cancel请求,这种场景被称为空回滚;TCC服务在实现时应当允许空回滚的执行;

空回滚可以通过业务逻辑进行避免,当然也可以借助中间件实现,如在第一阶段try()执行完后,向一张事务表中插入一条数据(包含事务id,分支id),cancle()执行时,判断如果没有事务记录则直接返回

防悬挂

如下图所示,事务协调器在调用TCC服务的一阶段Try操作时,可能会出现因网络拥堵而导致的超时,此时事务协调器会触发二阶段回滚,调用TCC服务的Cancel操作;在此之后,拥堵在网络上的一阶段Try数据包被TCC服务收到,出现了二阶段Cancel请求比一阶段Try请求先执行的情况;

用户在实现TCC服务时,应当允许空回滚,但是要拒绝执行空回滚之后到来的一阶段Try请求;

具体怎么解决呢?可以在二阶段执行时插入一条事务控制记录,状态为已回滚,这样当一阶段执行时,先读取该记录,如果记录存在,就认为二阶段回滚操作已经执行,不再执行try方法;

TCC特点

  • 通过运用本地事务代替了全局事务使得可以不需要协调者存在,避免了协调者单点问题
  • 3PC协调者的作用时一旦发证宕机可以保证之后的数据回复,TCC中可以通过事务日志解决

以CAP理论为基础的解决方案通常会出现一个类似”协调者”的服务对象,以上这三种就是主流的DTS(Distributed Transaction Service)框架。


六 分布式事务的常见解决方案-以BASE为基础

6.1异步消息-本地消息表

  • 服务A在本地事务操作业务的同时插入一条数据到消息表
  • 数据表操作完成后进行消息处理,进行RPC远程调用,并自主实现去重\顺序\重试等
  • 服务B接手到消息后同服务A处理逻辑一致,也要向本地消息表插入记录,如果该消息被处理过则进行事务回滚,以保证不重复处理消息
  • 服务B执行成后将更新自己本地消息表状态以及服务A消息表状态
  • 如果服务处理失败则不更新本地消息表,此时服务A会定时扫描消息表对未处理消息进行重复消息处理,知道服务B成功执行服务.

一般情况下为保证最终一致性被调用服务必须执行成功(可多次调用),但上图和说明仅仅是思想原理,具体的实现方案可以根据实际情况定,例如在每一个服务中或单独抽出一个”消息回滚处理应用服务”进行失败消息的回滚处理.

本地消息表方案最初由ebay提出,其完整方案可看参考内容.此方案的核心是将需要分布式处理的任务划分为本地事务并通过消息日志的方式来异步执行。消息日志可以存储到本地文本、数据库或消息队列,再通过业务规则自动或人工发起重试。人工重试更多的是应用于支付场景,通过对账系统对事后问题的处理。

6.2异步消息-不支持事务的MQ

与本地消息表唯一的区别是将远程调用控制改为由MQ控制的消息消费机制,借助MQ实现消息的顺序执行\去重\重试等机制.

其实大部分的MQ都是不支持事务的,所以我们需要自己想办法解决可能出现的MQ消息未能成功投递出去的问题。有个便宜可以捡的是,如果需投递的MQ消息条数仅有1的话,可以将本地事务的commit放于消息投递之后即可避免此问题。伪代码如下:

1
2
3
4
5
6
7
8
9
try{    
beginTrans();
modifyLocalData1();
modifyLocalData2();
deliverMessageToMQ();
commitTrans();
}catch(Exception ex){
rollbackTrans();
}

6.3异步消息-支持事务的MQ

如上图1,2,3,4步骤,即整个消息生产方与消息消费方之间的交互类似于TCC模式,但又有一些不同和补充,图解流程如下:

  • MQ发送方(消息生产方)进行第一阶段提交,向MQ server进行确认,如图1,2
  • MQ发送方第一阶段完成后进行本地事务处理并根据事务处理结果进行commit(告知server可以投递消息)或rollback(告诉server不投递消息),如图3,4
  • 若出现特殊情况如网络问题导致MQ server没有收到MQ发送方的第二阶段指令,那么将进行事务状态回查处理,如图5,6,7
  • MQ server接收到提交指令后将运行消息被消费并保证消息被消费,如果出现消息消费失败,则MQ server将重复发送,直至成功.

本方案的思路是基于BASE理论实现最终一致性

6.4Saga理论

详情可参考分布式事务方案Saga理论

6.5Gossip算法

Gossip算法因为Cassandra而名声大噪,Gossip看似简单,但要真正弄清楚其本质远没看起来那么容易。Gossip算法如其名,灵感来自办公室八卦,只要一个人八卦一下,在有限的时间内所有的人都会知道该八卦的信息,这种方式也与病毒传播类似,因此Gossip有众多的别名“闲话算法”、“疫情传播算法”、“病毒感染算法”、“谣言传播算法”。

Gossip算法又被称为反熵(Anti-Entropy),熵是物理学上的一个概念,代表杂乱无章,而反熵就是在杂乱无章中寻求一致,这充分说明了Gossip的特点:在一个有界网络中,每个节点都随机地与其他节点通信,经过一番杂乱无章的通信,最终所有节点的状态都会达成一致。每个节点可能知道所有其他节点,也可能仅知道几个邻居节点,只要这些节可以通过网络连通,最终他们的状态都是一致的,当然这也是疫情传播的特点。

Gossip是一个带冗余的容错算法,更进一步,Gossip是一个最终一致性算法。虽然无法保证在某个时刻所有节点状态一致,但可以保证在”最终“所有节点一致,”最终“是一个现实中存在,但理论上无法证明的时间点。

因为Gossip不要求节点知道所有其他节点,因此又具有去中心化的特点,节点之间完全对等,不需要任何的中心节点。实际上Gossip可以用于众多能接受“最终一致性”的领域:失败检测、路由同步、Pub/Sub、动态负载均衡。

但Gossip的缺点也很明显,冗余通信会对网路带宽、CUP资源造成很大的负载,而这些负载又受限于通信频率,该频率又影响着算法收敛的速度,后面我们会讲在各种场合下的优化方法。


七 总结

通过以上内容我们可以发现基于「CAP」的解决方案都是在线的,而「Base」是允许离线的

不管怎样,如果每个解决方案中增加「重试」和「回滚」会大大提升程序的自我修正能力,以降低需要人为介入的比例。

这些基于「BASE」的解决方案都是可以作为「CAP」解决方案出现问题时的PlanB来用的,起到补充作用。当然,如非必要,可以优先考虑基于「BASE」的方案,毕竟这才是天然易伸缩的,自然也能带来更好的性能。

解决方案如此多,所以不管我们是架构师、还是在成为架构师的路上,甚至在日常生活中,都需要养成Balance的习惯,找到那个最适合的方案

「事物都具有两面性」,所以,在选择走向分布式之前,慎重考虑下是否有必要,以免给自己徒增麻烦


本文部分内容参考:

https://www.imooc.com/article/75228

https://www.cnblogs.com/AndyAo/p/8228099.html

https://blog.csdn.net/hosaos/article/details/89136666#TCC_100

https://queue.acm.org/detail.cfm?id=1394128

https://www.cs.cornell.edu/andru/cs711/2002fa/reading/sagas.pdf

版权声明:本文为博主原创文章,欢迎转载,转载请注明作者、原文超链接,感谢各位看官!!!

本文出自:monkeyGeek

座右铭:生于忧患,死于安乐

欢迎志同道合的朋友一起交流、探讨!

monkeyGeek

评论

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×