分布式事务讲解之CAP,2PC,3PC,TCC

发布时间 2023-03-31 19:55:00作者: 上善若泪


学习此篇分布式事务前先学习Spring事务讲解

1 CAP

1.1 CAP原则

CAP原则又称CAP定理, 指的是在一个分布式系统中, Consistency(一致性) 、Availability(可用性) 、 Partition tolerance(分区容错性) , 三者不可兼得。

原则分类 详解
C
数据一致性(Consistency)
也叫做数据原子性系统在执行某项操作后仍然处于一致的状态。 在分布式系统中, 更新操作执行成功后所有的用户都应该读到最新的值,这样的系统被认为是具有强一致性的。 等同于所有节点访问同一份最新的数据副本
A
服务可用性(Availablity)
每一个操作总是能够在一定的时间内返回结果, 这里需要注意的是一定时间内返回结果。 一定时间内指的是,在可以容忍的范围内返回结果, 结果可以是成功或者是失败
P
分区容错性(Partition-torlerance)
在网络分区的情况下, 被分隔的节点仍能正常对外提供服务(分布式集群, 数据被分布存储在不同的服务器上, 无论什么情况, 服务器都能正常被访问)

分区容错性重点讲解:一个分布式系统里面,节点组成的网络本来应该是连通的。然而可能因为一些故障,使得有些节点之间不连通了,整个网络就分成了几块区域。数据就散布在了这些不连通的区域中。这就叫分区。当一个数据项只在一个节点中保存,那么分区出现后,和这个节点不连通的部分就访问不到这个数据了。这是分区就是无法容忍的。提高分区容忍性的办法就是一个数据项复制到多个节点上,那么出现分区之后,这一数据项就可能分布到各个区里。容忍性就提高了。然而,要把数据复制到多个节点,就会带来一致性的问题,就是多个节点上面的数据可能是不一致的。要保证一致,每次写操作就都要等待全部节点写成功,而这等待又会带来可用性的问题。
总的来说就是,数据存在的节点越多,分区容忍性越高,但要复制更新的数据就越多,一致性就越难保证。为了保证一致性,更新所有节点数据所需要的时间就越长,可用性就会降低

1.1.1 数据一致性

数据一致性的种类:

  • 强一致性(线性一致性):即复制是同步的
    任何一次读都能读到某个数据的最近一次写的数据
    系统中的所有进程,看到的操作顺序,都和全局时钟下的顺序一致
    简言之,在任意时刻,所有节点中的数据是一样的
  • 弱一致性:即复制是异步的
    数据更新后,如果能容忍后续的访问只能访问到部分或者全部访问不到,则是弱一致性

最终一致性就属于弱一致性

最终一致性
不保证在任意时刻任意节点上的同一份数据都是相同的,但是随着时间的迁移,不同节点上的同一份数据总是在向趋同的方向变化。
最终两个字用得很微妙,因为从写入主库到反映至从库之间的延迟,可能仅仅是几分之一秒,也可能是几个小时
简单说,就是在一段时间后,节点间的数据会最终达到一致状态

1.1.2 图示讲解

让我们来考虑一个非常简单的分布式系统,它由两台服务器G1和G2组成;这两台服务器都存储了同一个变量v,v的初始值为v0;G1和G2互相之间能够通信,并且也能与外部的客户端通信;我们的分布式系统的架构图如下图所示:
在这里插入图片描述
一个简单的分布式系统

客户端可以向任何服务器发出读写请求。服务器当接收到请求之后,将根据请求执行一些计算,然后把请求结果返回给客户端。譬如,下图是一个写请求的例子:

客户端发起写请求
客户端发起写请求

接着,下图是一个读请求的例子
客户端发起读请求
客户端发起读请求

现在我们的分布式系统建立起来了,下面我们就来回顾一下分布式系统的可用性、一致性以及分区容错性的含义。

1.1.2.1 一致性

在一个一致性的系统中,客户端向任何服务器发起一个写请求,将一个值写入服务器并得到响应,那么之后向任何服务器发起读请求,都必须读取到这个值(或者更加新的值)。

下图是一个不一致的分布式系统的例子:
不一致的分布式系统
客户端向G1发起写请求,将v的值更新为v1且得到G1的确认响应;当向G2发起读v的请求时,读取到的却是旧的值v0,与期待的v1不一致。

下图一致的分布式系统的例子:
一致的分布式系统
在这个系统中,G1在将确认响应返回给客户端之前,会先把v的新值复制给G2,这样,当客户端从G2读取v的值时就能读取到最新的值v1

1.1.2.2 可用性

在一个可用的分布式系统中,客户端向其中一个服务器发起一个请求且该服务器未崩溃,那么这个服务器最终必须响应客户端的请求。

1.1.2.3 分区容错性

服务器G1和G2之间互相发送的任意消息都可能丢失。如果所有的消息都丢失了,那么我们的系统就变成了下图这样:
网络分区
为了满足分区容错性,我们的系统在任意的网络分区情况下都必须正常的工作

1.2 CAP如何舍弃

定律: 任何分布式系统只可同时满足二点,没法三者兼顾,对于 分布式数据系统,分区容忍性是基本要求,否则就失去了价值

三者择其二 分析
CA, 放弃 P 如果想避免分区容错性问题的发生, 一种做法是将所有的数据(与事务相关的)都放在一台机器上。 虽然无法 100%保证系统不会出错, 单不会碰到由分区带来的负面效果。 当然这个选择会严重的影响系统的扩展性
CP, 放弃 A 相对于放弃"分区容错性"来说, 其反面就是放弃可用性。一旦遇到分区容错故障, 那么受到影响的服务需要等待一定时间, 因此在等待时间内系统无法对外提供服务
AP, 放弃 C 这里所说的放弃一致性, 并不是完全放弃数据一致性,而是放弃数据的强一致性, 而保留数据的最终一致性。 以网络购物为例, 对只剩下一件库存的商品, 如果同时接受了两个订单, 那么较晚的订单将被告知商品告罄

1.3 eureka与zookeeper区别

对比项 Zookeeper Eureka
CAP CP AP
Dubbo 集成 已支持 -
Spring Cloud 集成 已支持 已支持
kv 服务 支持 - ZK 支持数据存储,eureka不支持
使用接口(多语言能力) 提供客户端 http 多语言 ZK的跨语言支持比较弱
watch 支持 支持 支持 什么是Watch 支持?就是客户单监听服务端的变化情况。zk 通过订阅监听来实现eureka 通过轮询的方式来实现
集群监控 - metrics metrics,运维者可以收集并报警这些度量信息达到监控目的

1.4 CAP对应的模型和应用

1.4.1 CA without P

理论上放弃P(分区容错性),则C(强一致性)和A(可用性)是可以保证的。实际上分区是不可避免的,严格上CA指的是允许分区后各子系统依然保持CA。

CA模型的常见应用:

  • 集群数据库
  • xFS文件系统

1.4.2 CP without A

放弃A(可用),相当于每个请求都需要在Server之间强一致,而P(分区)会导致同步时间无限延长,如此CP也是可以保证的。很多传统的数据库分布式事务都属于这种模式。

CP模型的常见应用:

  • 分布式数据库
  • 分布式锁

1.4.3 AP wihtout C

要高可用并允许分区,则需放弃一致性。一旦分区发生,节点之间可能会失去联系,为了高可用,每个节点只能用本地数据提供服务,而这样会导致全局数据的不一致性。现在众多的NoSQL都属于此类。

AP模型常见应用:

  • Web缓存
  • DNS

1.4.4 常见注册中心

举个大家更熟悉的例子,像我们熟悉的注册中心ZooKeeperEurekaNacos中:

  • ZooKeeper 保证的是 CP
  • Eureka 保证的则是 AP
  • Nacos 不仅支持 CP 也支持 AP

1.5 BASE理论

BASE(Basically Available、Soft state、Eventual consistency)是基于CAP理论逐步演化而来的,核心思想是即便不能达到强一致性(Strong consistency),也可以根据应用特点采用适当的方式来达到最终一致性(Eventual consistency)的效果。
权衡了可用性一致性而提出的理论,相当于满足PA的情况(并没有完全舍弃C,只是舍弃强一致性,保留了最终一致性
在这里插入图片描述

BASE的主要含义:

  • Basically Available(基本可用)
    什么是基本可用呢?假设系统出现了不可预知的故障,但还是能用,只是相比较正常的系统而言,可能会有响应时间上的损失,或者功能上的降级。
    响应时间上:可能因为网络故障导致响应时间延长一点点
    功能上:由于某个服务突然被大量访问,那新来的访问被降级到其他服务,如返回网络繁忙等等
  • Soft State(软状态)
    什么是硬状态呢?要求多个节点的数据副本都是一致的,这是一种硬状态
    软状态也称为弱状态,相比较硬状态而言,允许系统中的数据存在中间状态,并认为该状态不影响系统的整体可用性,即允许系统在多个不同节点的数据副本存在数据延时。
    即:允许系统中的某些数据处于中间状态,且这些中间状态不影响整体的可用性,即允许各个节点之间的数据同步存在延迟
  • Eventually Consistent(最终一致性)
    上面说了软状态,但是不应该一直都是软状态。在一定时间后,应该到达一个最终的状态,保证所有副本保持数据一致性,从而达到数据的最终一致性。这个时间取决于网络延时、系统负载、数据复制方案设计等等因素。

2 分布式事务

现在实现分布式事务的设计方案应该有3种,分别就是二阶段提交、三阶段提交和TCC
他们都有2种重要的角色 事务协调者参与者,也就是各个服务】

2.1 二阶段提交(2PC)

两阶段提交的思路可以概括为:
参与者将操作成败通知协调者,再由协调者根据所有参与者的反馈情况决定各参与者是否要提交操作还是回滚操作。

sequenceDiagram 协调者 ->>参与者集群: 1.1询问各个参与者是否可以正常执行事务 activate 参与者集群 参与者集群 ->>参与者集群: 1.2 执行事务,不提交 参与者集群 -->>协调者: 1.3 反馈准备提交或回滚 deactivate 参与者集群 协调者 ->>参与者集群: 2.1 通知所有参与者提交或回滚 activate 参与者集群 参与者集群 ->>参与者集群:2.2 事务提交或回滚 参与者集群 -->>协调者: 2.3 反馈结果 deactivate 参与者集群

2.1.1 准备阶段

准备阶段:协调者(事务管理器)要求每个涉及到事务的数据库参与者 预提交(precommit)此操作,并反映是否可以提交

根据上面的UML图来看
1.3反馈准备提交或回滚 存在多种情况:

  • 所有参与者都反馈可以提交
  • 有参与者反馈回滚(不管多少)

2.1.2. 提交阶段

提交阶段:协调者(事务管理器)要求每个数据库参与者提交数据或者回滚数据
根据上面的UML图来看

  • 2.1根据1.3的反馈情况:
    • 所有参与者都反馈可以提交 –> 通知全部提交
    • 有参与者反馈回滚 –> 通知全部回滚
    • 等待反馈超时 –> 通知全部回滚
  • 2.2根据2.1
    • 通知全部提交 –> 提交
    • 通知全部回滚 –> 回滚
    • 一直没收到请求 –> 阻塞住

2.1.3 两阶段优缺点

优点:尽量保证了数据的强一致,实现成本较低,在各大主流数据库都有自己实现,对于MySQL是从5.5开始支持。

缺点:

  • 单点问题
    事务管理器在整个流程中扮演的角色很关键,如果其宕机,比如在第一阶段已经完成,在第二阶段正准备提交的时候事务管理器宕机,资源管理器就会一直阻塞,导致数据库无法使用。
  • 同步阻塞
    在准备就绪之后,资源管理器中的资源一直处于阻塞,直到提交完成,释放资源
    如上图所示,参与者反馈1.3后是处于阻塞状态等待2.1,如果网络问题或协调者宕机了,接受不到2.1,那么会一直阻塞
  • 数据不一致
    两阶段提交协议虽然为分布式数据强一致性所设计,但仍然存在数据不一致性的可能,比如在第二阶段中,假设协调者发出了事务commit的通知,但是因为网络问题该通知仅被一部分参与者所收到并执行了commit操作,其余的参与者则因为没有收到通知一直处于阻塞状态,这时候就产生了数据的不一致性
    如上图所示,部分参与者接收不到2.1,阻塞中,而接受到的就进行提交或回滚了,造成数据不一致

2.2 三阶段提交(3PC)

sequenceDiagram 协调者 ->>+参与者集群: 1.1 询问各个参与者是否可以正常执行事务 参与者集群 -->>-协调者: 1.2 反馈是否可执行 协调者 ->>+参与者集群: 2.1 准备提交 参与者集群 ->>参与者集群: 2.2 执行事务,不提交 参与者集群 -->>-协调者: 2.3 反馈结果 协调者 ->>+参与者集群:3.1 通知所有参与者提交或回滚 参与者集群 ->>参与者集群:3.2 事务提交或回滚 参与者集群 -->>-协调者:3.3 反馈结果

2.2.1 预判断阶段

预判断:协调者向参与者发送commit请求,参与者如果可以提交就返回Yes响应,否则返回No响应
如上图所示,1.2反馈情况

  • 所有参与者都反馈可以
  • 有参与者反馈不能成功执行(不管多少)

2.2.2 准备提交阶段

准备提交:协调者根据参与者在预判断阶段的响应判断是否执行事务还是中断事务,参与者执行完操作之后返回ACK响应,同时开始等待最终指令。
如上图所示

  • 2.1根据1.2的情况
    • 所有参与者都反馈可以 –> 通知全部准备提交
    • 有参与者反馈不能成功执行 –> 通知全部 abort 通知
    • 等待反馈超时 –> 通知全部 abort 通知
      协调者发起abort通知后就会进入结束状态了,不再进行后续
  • 2.2根据2.1
    • 通知全部准备提交 –> 执行事务,不提交
    • 通知全部 abort 通知 –> 会中断事务的操作
    • 等待超时 –> 会中断事务的操作

2.2.3 提交阶段

提交阶段:协调者根据参与者在预判断阶段的响应判断是否执行事务还是中断事务:

  • 如果所有参与者都返回正确的ACK响应,则提交事务
  • 如果参与者有一个或多个参与者收到错误的ACK响应或者超时,则中断事务
  • 如果参与者无法及时接收到来自协调者的提交或者中断事务请求时,在等待超时之后,会继续进行事务提交

如上图所示

  • 3.1根据2.3的情况
    • 所有参与者都反馈体可以提交 –> 通知全部提交
    • 有参与者反馈回滚或中断事务 –> 通知全部回滚
    • 等待超时 –> 通知全部回滚
  • 3.2根据3.1
    • 通知全部提交 –> 提交
    • 通知全部回滚 –> 回滚
    • 等待超时 –> 提交

为什么第三阶段等待超时就会自动提交呢?
因为经过了前面两阶段的判断,第三阶段可以提交的概率会大于回滚的概率

2.2.4 三阶段/二阶段差异

三阶段的参与者是有超时机制的,等待请求超时会进行事务中断,或事务提交
而二阶段不会超时,只会阻塞
可以看出,三阶段提交解决的只是两阶段提交中单体故障和同步阻塞的问题,因为加入了超时机制,这里的超时的机制作用于 准备提交阶段提交阶段。如果等待 准备提交请求 超时,参与者直接回到准备阶段之前。如果等到提交请求超时,那参与者就会提交事务了

注意:无论是2PC还是3PC都不能保证分布式系统中的数据100%一致

2.3 补偿提交(TCC)

2.3.1 定义

TCC(Try Confirm Cancel) ,是两阶段提交的一个变种,针对每个操作,都需要有一个其对应的确认取消操作,当操作成功时调用确认操作,当操作失败时调用取消操作,类似于二阶段提交,只不过是这里的提交回滚是针对业务上的,所以基于TCC实现的分布式事务也可以看做是对业务的一种补偿机制,其核心思想是:针对每个操作,都要注册一个与其对应的确认和补偿(撤销)操作。

TCC(Try-Confirm-Cancel)包括三段流程:

  • try阶段:尝试去执行,完成所有业务的一致性检查,预留必须的业务资源。
  • Confirm阶段:确认执行业务,如果Try阶段执行成功,接着执行Confirm 阶段。该阶段对业务进行确认提交,不做任何检查,因为try阶段已经检查过了,默认Confirm阶段是不会出错的。
  • Cancel 阶段:取消待执行的业务,如果Try阶段执行失败,执行Cancel 阶段。进入该阶段会释放try阶段占用的所有业务资源,并回滚Confirm阶段执行的所有操作。

TCC 是业务层面的分布式事务,保证最终一致性,不会一直持有资源的锁。

  • 优点: 把数据库层的二阶段提交交给应用层来实现,规避了数据库的 2PC 性能低下问题
  • 缺点TCCTryConfirmCancel 操作功能需业务提供,开发成本高。TCC 对业务的侵入较大和业务紧耦合,需要根据特定的场景和业务逻辑来设计相应的操作

2.3.2 操作

数据库表需要存多两个字段可更新数冻结数,将各个服务的事务执行分成3个步骤

  • T(try ,尝试更新阶段)
    • 原数据不变,只更新【可更新数】,同时保存变化差值在【冻结数】,方便回滚
    • 状态设置为【更新中】
  • C(confirm,确认阶段)
  • 更新原数据,【可更新数】不变(在try阶段更新好了),清除【冻结数】
  • 状态设置为【更新完】
    此时表示业务正常完成了
  • C(cancel,补偿还原阶段)
    如果try出错了,那各个服务就执行cancel还原数据,相当于回滚
    • 根据【可更新数】【冻结数】更新数据为事务前的样子
    • 状态设置为更新前的状态【未更新】

如果是 confirmcancel出错了,一般会去重复执行,因为过了try,可以认为confirm是一定可以执行成功的,除非重复执行次数达到阈值,就落地成日志,让人工处理
在这里插入图片描述

注意:这里事务回滚的方式不像我们认为的那样 — 数据库直接给我们处理好
而是通过cancel将数据补回去,所以TCC也叫补偿机制

2.4 分布式事务总结

二阶段提交三阶段提交TCC分别是三种实现分布式事务的方案
至于具体的实现框架:二阶段有mysql的XA事务,TCC有seata、tcc-transaction
我们看TCC会涉及到服务与服务之间的接口调用,因为网络问题,极有可能出现重复调用的情况,所以【confirm】【cancel】这些接口应该要实现幂等
当然服务间的通信除了通过【同步的rpc】,也可以通过【异步的MQ】来实现,所以引出了接下来的基于MQ最终一致性方案