高可用是指系统无中断的执行功能的能力,代表了系统的可用程度,是进行系统设计时必须要遵守的准则之一。 而高可用的实现方案,无外乎就是冗余,就存储的高可用而言,问题不在于如何进行数据备份,而在于如何规避数据不一致对业务造成的影响。 对于分布式系统而言,要保证分布式系统中的数据一致性就需要一种方案,可以保证数据在子系统中始终保持一致,避免业务出现问题。 这种实现方案就叫做分布式事务,要么一起成功,要么一起失败,必须是一个整体性的事务。
CAP,Consistency Availability Partition tolerance 的简写:
Consistency:一致性,对某个客户端来说,读操作能够返回最新的写操作结果。 Availability:可用性,非故障节点在合理的时间内返回合理的响应。 Partition tolerance:分区容错性,分布式系统中系统肯定部署在多台机器上,无法保证网络做到 100% 的可靠,所以网络分区一定存在,即 P 一定存在。
在出现网络分区后,就出现了可用性和一致性的问题,我们必须要在这两者之间进行取舍,因此就有了两种架构:
BASE 理论指的是基本可用 Basically Available,软状态 Soft State,最终一致性 Eventual Consistency,核心思想是即便无法做到强一致性,但应该采用适合的方式保证最终一致性。
BASE,Basically Available Soft State Eventual Consistency 的简写: BA:Basically Available 基本可用,分布式系统在出现故障的时候,允许损失部分可用性,即保证核心可用。 S:Soft State 软状态,允许系统存在中间状态,而该中间状态不会影响系统整体可用性。 E:Consistency 最终一致性,系统中的所有数据副本经过一定时间后,最终能够达到一致的状态。
BASE 理论本质上是对 CAP 理论的延伸,是对 CAP 中 AP 方案的一个补充。
二阶段提交(Two-phase Commit),是指,为了使基于分布式系统架构下的所有节点在进行事务提交时保持一致性而设计的一种算法(Algorithm)。通常,二阶段提交也被称为是一种协议(Protocol)。
在分布式系统中,每个节点虽然可以知晓自己的操作是成功或者失败,却无法知道其他节点的操作是成功或失败。
当一个事务跨越多个节点时,为了保持事务的 ACID 特性,需要引入一个作为协调者的组件来统一掌控所有节点(称作参与者)的操作结果并最终指示这些节点是否要把操作结果进行真正的提交(比如将更新后的数据写入磁盘等等)。
因此,二阶段提交的算法思路可以概括为:参与者将操作成败通知协调者,再由协调者根据所有参与者的反馈情报决定各参与者是否要提交操作还是中止操作。
image.png
投票阶段执行流程:
image.png
提交阶段执行流程:
三阶段提交(Three-phase commit),是为解决两阶段提交协议的缺点而设计的。与两阶段提交不同的是,三阶段提交是“非阻塞”协议。
三阶段提交在两阶段提交的第一阶段与第二阶段之间插入了一个准备阶段,使得原先在两阶段提交中,参与者在投票之后,由于协调者发生崩溃或错误,而导致参与者处于无法知晓是否提交或者中止的“不确定状态”所产生的可能相当长的延时的问题得以解决。
image.png
询问阶段:CanCommit 协调者向参与者发送 Commit 请求,参与者如果可以提交就返回 Yes 响应,否则返回 No 响应。
准备阶段:PreCommit 协调者根据参与者在询问阶段的响应判断是否执行事务还是终端事务: 如果所有参与者都返回 Yes,则执行事务。 如果参与者有一个或多个参与者返回 No 或者超时,则中断事务。 参与者执行完操作之后返回 ACK 响应,同时开始等待最终指令
提交阶段:DoCommit 协调者根据参与者在准备阶段的响应判断是否执行事务还是中断事务: 如果所有参与者都返回正确的 ACK 响应,则提交事务。 如果参与者有一个或多个参与者收到错误的 ACK 响应或者超时,则中断事务。 如果参与者无法及时接收到来自协调者的提交或者中断事务请求时,会在等待超时之后,会继续进行事务提交。 协调者收到所有参与者的 ACK 响应,完成事务。
三阶段提交解决了二阶段提交中存在的由于协调者和参与者同时挂掉可能导致的数据一致性问题和单点故障问题,并减少阻塞。 因为一旦参与者无法及时收到来自协调者的信息之后,他会默认执行提交事务(三阶段),而不会一直持有事务资源并处于阻塞状态(二阶段会)。
在提交阶段如果发送的是中断事务请求,但是由于网络问题,导致部分参与者没有接到请求。
那么参与者会在等待超时之后执行提交事务操作,这样这些由于网络问题导致提交事务的参与者的数据就与接受到中断事务请求的参与者存在数据不一致的问题。
所以无论是 2PC 还是 3PC 都不能保证分布式系统中的数据 100% 一致。
ZAB 协议全称是Zookeeper Atomic Broadcast,是为分布式协调服务 Zookeeper 专门设计的一种支持 崩溃恢复 和 原子广播 协议。 ZAB 协议满足CP,在选举过程中,是不能对外提供服务的。
ZAB 协议的消息广播过程(即数据同步过程)使用的是一个原子广播协议,类似一个 二阶段提交过程。对于客户端发送的写请求,全部由 Leader 接收,Leader 将请求封装成一个事务 Proposal,将其发送给所有 Follwer ,然后,根据所有 Follwer 的反馈,如果超过半数成功响应,则执行 commit 操作(先提交自己,再发送 commit 给所有 Follwer)。
广播流程:
image.png
注意: 1、 Leader 服务器与每一个 Follower 服务器之间都维护了一个单独的 FIFO 消息队列进行收发消息,使用队列消息可以做到异步解耦。 Leader 和 Follower 之间只需要往队列中发消息即可 2、Leader 在收到客户端请求之后,会将这个请求封装成一个事务,并给这个事务分配一个全局递增的唯一 ID,称为事务ID(ZXID),ZAB 协议需要保证事务的顺序,因此必须将每一个事务按照 ZXID 进行先后排序然后处理 3、可以认为这是一种简化版本的 2PC,2PC的单点问题(Leader崩溃)这里也会遇到,ZAB是通过崩溃恢复来解决的
所谓崩溃恢复,是指当leader崩溃(单点故障)后,重新选举leader并且数据保持一致性(数据同步)。
image.png
由上面规则可知,通常那台服务器上的数据越新(ZXID会越大),其成为Leader的可能性越大,也就越能够保证数据的恢复。如果ZXID相同,则SID越大机会越大。
注意:在zk选举中,通过投票已经确认leader服务器是最大的zxid的节点了,所以同步过程没有那么复杂。 同步阶段主要是利用 Leader 前一阶段获得的最新 Proposal 历史,同步集群中所有的副本。只有当超过半数的节点都同步完成,准 Leader 才会成为真正的 Leader。Follower 只会处理 zxid 比自己 lastZxid 大的 Proposal。
本地消息表的核心思想是将分布式事务拆分成本地事务进行处理。
例如,在订单系统新增一条消息表,将新增订单和新增消息放到一个事务里完成,然后通过轮询的方式去查询消息表,将消息推送到 MQ,库存系统去消费 MQ。
image.png
订单系统中的消息有可能由于业务问题会一直重复发送,所以为了避免这种情况可以记录一下发送次数,当达到次数限制之后报警,人工接入处理;库存系统需要保证幂等,避免同一条消息被多次消费造成数据一致。
本地消息表这种方案实现了最终一致性,需要在业务系统里增加消息表,业务逻辑中多一次插入的 DB 操作,所以性能会有损耗,而且最终一致性的间隔主要由定时任务的间隔时间决定。
消息事务的原理是将两个事务通过消息中间件进行异步解耦。
image.png
从上面可以看出,消息事务一定要保证业务操作与消息发送的一致性,如果业务操作成功,这条消息也一定投递成功。
消息事务依赖于消息中间件的事务消息,基于消息中间件第二阶段提交实现的,RocketMQ 就支持事务消息。RabbitMQ也支持事务消息
这种方案也是实现了最终一致性,对比本地消息表实现方案,不需要再建消息表,不再依赖本地数据库事务了,所以这种方案更适用于高并发的场景。
最大努力通知相比前两种方案实现简单,适用于一些最终一致性要求较低的业务,比如支付通知,短信通知这种业务。 以支付通知为例,业务系统调用支付平台进行支付,支付平台进行支付,进行操作支付之后支付平台会尽量去通知业务系统支付操作是否成功,但是会有一个最大通知次数。 如果超过这个次数后还是通知失败,就不再通知,业务系统自行调用支付平台提供一个查询接口,供业务系统进行查询支付操作是否成功。
image.png
这种方案也是实现了最终一致性。
TCC,Try-Confirm-Cancel 的简称,针对每个操作,都需要有一个其对应的确认和取消操作。 当操作成功时调用确认操作,当操作失败时调用取消操作,类似于二阶段提交,只不过是这里的提交和回滚是针对业务上的,所以基于 TCC 实现的分布式事务也可以看做是对业务的一种补偿机制。
在 Try 阶段,是对业务系统进行检查及资源预览,比如订单和存储操作,需要检查库存剩余数量是否够用,并进行预留,预留操作的话就是新建一个可用库存数量字段,Try 阶段操作是对这个可用库存数量进行操作。
步骤一(Try 阶段):订单系统将当前订单状态设置为支付中,库存系统校验当前剩余库存数量是否大于 2,然后将可用库存数量设置为库存剩余数量 -2,并且设置冻结库存为2
步骤二(Confirm 阶段):如果 Try 阶段执行成功,执行 Confirm 阶段,将订单状态修改为支付成功,库存剩余数量修改为可用库存数量。
步骤三(Cancel 阶段):如果 Try 阶段执行失败,执行 Cancel 阶段,将订单状态修改为支付失败,可用库存数量修改为库存剩余数量。
image.png
ByteTCC,github.com/liuyangming tcc-transaction:github.com/changmingxi