CAP,基础理论
CAP理论是分布式系统中最核心的理论基础
- Partition tolerance,分区容错性
the system continues to operate despite arbitrary message loss or failure of part of the system
系统能够在网络分区(网络故障或通信故障)的情况下还能继续提供服务
all nodes see the same data at the same time
所有节点在同一时间的数据是相同的,也就是更新操作完成后,所有节点保存的数据相同
Reads and writes always succeed
系统服务一直处于可用状态,
为什么CAP只能满足两个目标
C,A,P只能同时满足两个目标,而P是分布式的基本盘,所以需要在C与A之间进行取舍。
- 如果要保证服务可用性,就选择AP模型
选择AP意味着舍弃强一致性,以保证可用性与分区容错性。在AP架构下,系统能够快速响应客户端的请求,即时在网络分区情况下,也尽量保证服部分节点可用,从而为客户端提供不间断的服务,允许临时数据不一致(如异步复制)
- 如果要保证数据一致性,就选择CP模型
选择CP意味着舍弃可用性,以保证一致性与分区容错性。在CP架构下,系统会优先保证数据的一致性,当发生网络分区时,牺牲可用性(如暂停写操作直到分区修复)。
- 单机部署,就选择CA模型
场景:
你点外卖,选好商品后提交订单。但此时你刚好在电梯里,网络变差了(网络分区)。
你与服务器失去联系,无法确认订单真的提交成功,但你还是可以操作APP继续浏览菜单的(P,分区容错)。
这时候APP必须做出选择,A VS C.
选A(可用性),直接显示已提交(即使服务器没收到)。但如果网络一直没恢复,订单可能没提交成功,后续你需要重新下单(数据不一致)
选B(一致性),APP直接卡死或者显示加载中,必须等系统确认后才显示成功(数据一定正确,但期间无法操作)。
CAP的理论,只有网络分区发生了,才需要在CAP中进行权衡,但大部分时间,网络分区是不会发生的。因此CAP理论可以作为系统设计时需要衡量的因素,而非绝对的选择。
PACELC,拓展理论
CAP+Everything is Local and Connected = PACELC ,是CAP理论的拓展,上面说到CAP理论是在网络分区发生的情况下才需要考虑的。大多数情况下,系统都是平稳运行。在这种情况下,因为不需要考虑网络分区,所以要考虑就是数据一致与读写延迟的平衡
- P
分布式系统在面对网络分区时(网络故障或通信故障),仍然能够继续运行。
- A
分布式系统在面对故障时,依然能够提供服务并保证数据的可访问性。
- C
分布式系统在面对网络分区时,能够保持数据一致性。
- E
Eventual consistency,最终一致性,分布式系统在面对网络分区时,由于网络延迟或者异步复制等原因,可能会导致节点之间数据的不一致,但最终会达到一致状态
- L
Latency ,延迟,分布式系统的响应时间,在一些情况下,为提高系统响应速度,可能会牺牲一致性或者可用性
场景
承接上面的场景,当你出了电梯后,你的网络开始恢复。在此期间你点击了多次提交订单,导致服务器收到了多个重复的订单请求。
此时进入ELC矛盾阶段,在延迟(Latency)和一致性(Consistency)之间需要权衡。
选延迟(Latency),你很快看到订单状态更新,但可能不知道之前的重复提交被自动合并了(牺牲一致性,但用户体验流畅)。
选一致性(Consistency),弹出提示 “检测到多个订单请求,请确认是否需要保留一个”,等你手动选择后再处理。订单一定准确(一致性高),但你需要花时间确认(延迟高)
因此,PACELC也可以理解是对CAP的替代,因为它不仅讲述了网络分区的极端情况下如何取舍,还覆盖了系统正常时应该考虑的事项。
BASE,具体落地理论
Base 理论是 AP 系统的实践指导,通过牺牲强一致性换取高可用性和分区容错性,是 CAP 定理在工程中的具体落地方式。
- Basically Available,基本可用
不追求CAP中的,任何时候读写都可以成功,而是系统能够基本运行,一直提供服务。
允许损失部分可用,可能是响应时间延长,或者服务降级。
举个例子,如果并发太多超过了系统QPS峰值,可能会提示排队。这就是通过合理手段保护系统的稳定性。
- Soft State ,软状态
允许系统存在中间状态,比如异步复制的延迟。并认为该状态不影响整体运行,也就是允许系统在多个不同的节点的数据副本存在数据延时
比如分布式缓存中,数据副本允许短暂不一致。
- Eventually Consistency ,最终一致性
数据不可能一直是软状态,必须在一个时限之后保证所有副本数据一致。
Base理论的核心是最终一致性,即时无法做到强一致性(Strong Consistency),Application可以根据自身的业务特点,采用适当的方式来达到最终一致性
CAP在基础组件中的应用
如果一个分布式场景需要很强的数据一致性,或者该场景可以接收系统相应很慢的情况。使用CP架构就比较合适了,
保证CP的架构也很多,典型的有Redis,ZooKeeper。
以ZooKeeper为例:
zk这种设计保证了CP,需要超过一半节点同意才提交写操作,这中间的可用性是很低的。
如果一个分布式场景需要很强的可用性,且能接收数据暂时不一致,那么使用AP架构就比较合适。
保证AP的架构就很多了,比如数据库的读写分离,Eureka等。为了保证用户体验,牺牲了数据的一致性。
分布式事务
分布式事务是指涉及多个独立数据库或节点的事务操作,需要保证跨节点的数据一致性。由于分布式的特性导致了传统数据库事务的ACID特性难以直接应用。
由于CAP理论的桎梏,分布式事务也需要妥协部分特性,转而采用最终一致性(BASE理论)。
2PC,Two-Phase Commit
原理:事务分两阶段提交
- PreCommit
通知所有节点,开启事务并预提交
- DoCommit
根据参与者的反馈,决定提交还是回滚。
如果参与者全部同意,协调者通知所有参与者提交事务
如果任一节点失败,协调者通知所有参与者回滚
3PC,Three-Phase Commit
从2PC可以看到,如果Service2服务一开始就不可用,Service1与Service3依旧会开启事务。直到协调者通知回滚才关闭事务。这中间的粒度太大了,为了优化这个问题,有了3PC
- CanCommit
多衍生出一个询问阶段,仅询问是否提供服务,不开启事务
- PreCommit
若全部同意,开始事务预提交
- DoCommit
正式提交或者回滚
TCC,Try-Confirm-Cancel
尽管3PC缩小了2PC阻塞的粒度,但在PreCommit阶段之后,所有节点都会开启事务,这时候阻塞的粒度,依旧很大,这里还有没有优化空间呢?TCC方案就应运而生。
TCC原理与2/3PC类似,最大不同的点就是,2/3PC依赖数据库的事务机制,TCC更依赖让代码逻辑来变相实现事务。
- Try
协调者调用所有微服务API的try接口,将涉及到的资源提前创建或者锁住
- Confirm/Cancel
正式提交或者回滚
可以看到,TCC模式在流程上,除了颗粒度小了一点外,没有本质上的区别,那我如果对并发性没有要求的话,可以无脑使用2PC呢?
使用TCC还有一个核心的因素,2PC/3PC是基于数据库的XA协议,比较局限。只能在多个数据库之间实现分布式事务,而大多数情况下,都是微服务之间的API调用,并没有实现XA协议,因此TCC才登上历史舞台。
实现TCC要注意什么?
既然TCC是一种2PC的变种实现,那么它是如何解决Service出现故障后的数据一致性的呢?
答案是不断重试,因为try阶段已经提前创建或者锁定好了所有资源,所以无论是Confirm或者cancel都可以通过不断重试直到成功。
- try超时
比如try过程中很慢(进程还在处理),导致了try超时。会触发多次重试,而多次重试,就需要考虑幂等的操作。
可以利用幂等表来先查后写,来规避重复执行的问题。
要考虑第一次的try因为网络拥塞,所以在Confirm/Cancel后才到,代码逻辑一定要严谨
- confirm超时
检查业务状态,避免重复confirm,在极端情况下,两个重复请求同时到达,可以通过业务规则丢弃请求,比如已经提交了,再来个提交请求,就直接返回成功或者丢弃。
要考虑confirm因为网络拥塞,晚于cancel到达的情况
- cancel超时
同上,网络拥塞与幂等依旧是实现的难题。
要考虑cancel后,confirm到达的问题。
- 空回滚
Service Try还未执行,因为其它Service已经失败,所以发来了Cancel消息。
Cancel需要清理无效资源,而资源未创建,如果代码不够健壮,就会报错。
- 事务悬挂
Cancel 或 Confirm 已执行后,因为拥塞而迟到的 Try 请求到达。
Try阶段需要检查事务是否已经结束,否则拒绝执行或忽略
可以看到,自己实现TCC是一件很麻烦的事情。这对于研发来说简直就是一种灾难。
所以TCC一般都会搭配一张幂等表,来作为状态标记,辅助判断重试中过程中的各种情况。
分布式事务Id事务描述事务状态主键Id68402338-ec9d-489c-ba07-abb42546329d订单创建ACKxxxxb70240b8-17ce-4005-b322-e9b886d41e4b库存扣减Tryxxxx2bdd077c-d221-49ce-a7d4-6f441dec1928金额预扣款Cancelxxxx最终一致性事务
无论是基于2PC/3PC还是TCC的解决方案,核心都是基于XA协议的思路。事务参与者创建本地事务,由协调者协调最终的事务提交还是回滚。
上述的方案中,创建本地事务终究需要排他等待(强一致性),无论你如何优化都是无法避免的,因为这是CAP定理的桎梏。
这些全局事务方案由于操作麻烦,排他等待等因素,此类架构的架构是保证了强一致性,所以并发度不会高。
面对互联网主流的AP架构,演化出了与XA协议背道而驰的最终一致性解决方案。
- Order Service
开启一个本地事务,同时提交业务数据,扣减库存数据+预付款数据。保证强一致性。
- 独立轮询服务捞数据
由于网络原因,Order Serivce发送给MQ的消息可能会丢。因此需要一个服务一直扫描未发送的数据
- Inventory Service/Payment Service
收到MQ消息后,首先是幂等性校验,与TCC类似,本来会有一张幂等表来辅助实现接口幂等。
并在同一个事务中,更新业务逻辑与幂等状态。
如果处理失败,不会返回ACK,所以MQ会不断重试。
如果是业务失败,同Order Service流程,回滚自己的事务同时,向Msg表发送Cancel 扣减库存数据+预付款数据。实现分布式事务回滚。
FAQ
- 独立的轮询服务挂了
多部署几个实例,集群化。
要万一挂了,因为本地Msg表状态还是未发送,所以再重新发一次就好
- MQ挂了?
Msg是持久化在数据库中,所以消息并不会丢。如果长时间挂,再人工介入。
- 消费端挂了?
基于MQ的ACK机制,如果处理失败,MQ会不断重试,直到触发阈值,人工介入。
- 消费端幂等性?
每个服务都有自己的消息消费记录表,记录下处理过的MQ MessageId 先查后写。或者利用主键等本身能证明唯一性的数据,防止重复处理。
- 死循环消费
因为代码问题,导致的永远不会消费成功,可以在消息表中设计最大重试次数,或者挪到死信队列中,人工介入。
- 本地消息表过大,影响性能
随着写入量的增加,表的大小日积月累。这就是数据库单表优化的思路了,归档,分表等常规操作
回归正题,为什么我讨厌分布式事务
分布式事务的本质是 “妥协的艺术”,为了解决一个问题,从而引发出更多的问题。
使命召唤游戏里有一句经典台词,扑灭一场火最好的办法,是在旁边点燃一团更大的火,烧光它的可燃物,耗尽它的氧气,火自然就灭了。
不管是强一致性的2PC/3PC/TCC,还是最终一致性的本地消息表/消息队列。他们都会引入一些更麻烦的操作来实现,
比如2PC/3PC依赖DTC,TCC依赖程序员自身的素养,性能低的同时开发跟维护成本高。
最终一致性的方案也好不到哪里去,超时重试(避免因短暂延迟导致事务失败),幂等性(避免重复操作导致数据错误),补偿冲突(如多个补偿操作同时执行,需加锁或版本控制),都是分布式系统的必须处理的部分。
总结:分布式事务是 “最后的手段”,能通过业务规避就尽量规避,仅在 “数据绝对不能错”(如金融)或 “资源绝对不能冲突”(如航空订票)的场景中使用。
说人话就是,我只想早点下班,分布式事务太麻烦了。各位的项目没到那个体量就别给自己找麻烦。
来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作! |