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