1. 什么是一致性
1.1 互联网解决解决问题的方式 – 拆分
(1)水平拆分:由于单一节点没法满足性能需求,需要扩展为多个节点,多个节点具有一致的功能,组成一个服务池。一个节点服务一部分请求,所有节点共同处理大规模并发的请求。
(2)垂直拆分:按照功能进行拆分,把一个复杂的功能拆分成多个单一,简单的功能。由于每个功能单一,简单,使维护,变更,开发迭代,部署都敏捷和简单。
(3)在互联网中,一致性是指的是分布式服务化系统之间的一致性,包括应用系统的一致性和数据的一致性。
2. 一致性问题场景枚举
2.1 下单和扣库存
先下单,在扣库存,就会导致超卖。下单不成功,扣库存成功,导致少买。
2.2 同步调用超时
系统A同步调用系统B超时,系统A可以明确得到超时反馈,但是无法确定系统B是否已经完成了预设功能,这时系统A不知道该如何反馈给使用方。
2.3 异步回调超时(异步消息)
系统A发出请求调用系统B,系统B受理后则返回成功信息,然后系统B处理后异步通知系统A结果。在这个过程中,如果系统A由于某种原因没有收到回调结果,这两个系统的状态就不一致了。
2.4 掉单
两个系统协处理一个流程,分别为对方的上下游,如果一个系统中存在一个请求(订单),另外一个系统不存在,则会导致掉单。
2.5 缓存和数据库不一致
缓存与数据库之间如何保持一致性?
2.6 本地缓存节点间的不一致
一个服务池上多个节点之间的状态不一致。
2.7 缓存数据结构不一致
由于程序处理不当,将不完整的数据存入缓存中,缓存使用者很可能由于数据的不完整抛异常。
3. 解决一致性问题的模式和思路
3.1 酸(ACID)碱(BASE)平衡理论
(1)ACID
A: Atomic
C: Consistency
I: Isolation
D: Durability
每个事务都是原子的,或者成功,或者失败。事务是隔离的,不相互影响。最终状态是持久落盘的。
如果问题2.1, 订单表和库存表在同一个关系型数据库中,利用关系型数据库的强一致性解决。但不在同一数据库中,就要保证最终一致性。
(2)分布式系统的 CAP
C:Consistency: 所有节点,在同一时刻读取的数据都是最新的副本。
A:Availability: 可用性,就是好的响应性能。
P: Patition tolerence: 分区容忍性。尽管网络上有部分消息丢失,但消息任然可以继续工作。
任何分布式系统只可能同时满足上面两点,无法三者兼顾。风分布式系统的可用性和一致性不可兼得。
(3)BASE
通过牺牲强一致性获得可用性,通过达到最终一致性满足业务的需求。
BA : basically available 基本可用,
S:soft state : 软状态,状态可能在一段时间不同步。
E: eventually consisitent: 最终一致性,在一定的时间窗口,最终数据达到一致性。
3.2 分布式一致性协议
3.2.1 两阶段提交协议
(1)准备阶段:协调者向参阅者发起指令,参与者评估可以完成,则写redo/undo 日志,然后锁资源,执行操作,但不提交。
(2)提交阶段:如果每个参与者确认返回成功,则协调者向参与者发送提交的指令, 参阅者提交操作,释放资源。如果任意一个参与者在准备阶段失败,则协调者向其他参阅者发送终止指令。参与者取消变更的事务,执行undo,释放资源。
(3)存在的问题:
a. 阻塞:每一个指令都必须收到明确的指令才能继续下一步,否则处于阻塞,资源被占用,不被释放。
b. 单点故障:如果协调者宕机。
c. 脑裂:在高可用(HA)系统中,当联系2个节点的“心跳线”断开时,本来为一整体、动作协调的HA系统,就分裂成为2个独立的个体。由于相互失去了联系,都以为是对方出了故障。两个节点上的HA软件像“裂脑人”一样,争抢“共享资源”、争起“应用服务”,就会发生严重后果——或者共享资源被瓜分、2边“服务”都起不来了;或者2边“服务”都起来了,但同时读写“共享存储”,导致数据损坏(常见如数据库轮询着的联机日志出错)。解决方案:(1)双心跳线(2)磁盘锁(3)设置仲裁机制。
3.2.2 三阶段提交协议
三阶段提交是两阶段的改进版,用超时解决阻塞。
(1)询问阶段:协调者询问参与者是否可以完成指令,协调者只需要回答是或不是,而不需要真正的操作。这个阶段超时会导致终止。
(2)准备阶段:就是两阶段提交的第一步。
(3)提交阶段:就是两阶段提交的第二步。
三阶段和两阶段提交的不同点:
尽可能早的发现无法执行的操作,不能发现所有的,只会减少。
在准备阶段以后增加了超时,一旦超时,参与者和协调者则继续提交事务,默认成功。至少不会堵塞。
3.2.3 TCC(Try Confirm Cancel)
(1)正常流程是先执行Try,如果执行没有问题,再执行Confirm,如果执行过程中出现了问题,执行操作的逆行操作Cancel。任然是一个两阶段提交,但是,如果任何参与者出现了问题,协调者通过执行逆操作来Cancel之前的操作。
(2)如果在取消时,一些参与者收到指令,另外一些参与者没有收到指令,则整个系统任然不一致。解决方法:系统首先通过补偿尝试修复,如果无法修复,需要人工参与。
3.3 保证最终一致性的一些常用模式
3.3.1 查询模式
(1)任何服务操作都需要提供一个查询接口,供外部查询当前操作执行的状态,这样,根据不同的状态来做不同的处理操作。
(2)每个服务操作都要有唯一的流水号标识。可以用资源id标识,e.g. 请求流水号,订单号。
(3)2,3,4案例都可以通过查询模式理解被调用方的服务状态,来决定下一步做什么,或者补偿,或者回滚。
3.3.2 补偿模式
为了让系统达到最终一致状态的努力,都叫补偿。比如:重新执行未完成的子操作,或者取消已完成的操作。
补偿操作的几种形式:
(1)自动恢复:根据操作的状态,继续执行,或者回滚。
(2)通知运营:人工后台搬砖!
3.3.3 异步确保模式
(1)将要执行的异步操作封装后入库,通过定时任务拉取未完成的任务进行补偿操作来实现异步确保模式。
3.3.4 定期校对模式
3.3.5 可靠消息模式
(1)消息的可靠发送
(a)在发送消息之前将消息持久在数据库中,标记为待发送,然后发送消息。如果发送成功,将消息改为发送成功。再用定时任务在数据库捞取在一定时间未发送的消息并发送。
(b)第二种是使用独立的数据库,发消息前,发送一个消息给第三方的消息管理器,消息管理器再将其持久到数据库,标记位待发送。在发送成功后,标记消息为发送成功。定时从第三方系统的数据库中捞取一定时间内未发送的消息。
(2)消息处理的幂等性
a. 使用数据库表的唯一键进行滤重。
b. 使用数据库的行级锁来实现。
c. redis 设置唯一键为key。
4. 超时处理模式
4.1 微服务的交互模式
(1)同步调用
(2)异步接口调用
(3)消息队列异步接口调用
4.3 交互模式下超时问题的解决方案
4.3.1 同步调用模式下的解决方案
接口返回状态定义:(1)成功,失败 (2)成功,失败,处理中。
服务1调用服务2,服务2调用服务3。
如果服务1调用服务2超时,服务2调用服务3正常,使用查询模式补偿。
如果服务2调用服务3超时,则服务二应该对服务三查询,而且回滚。
4.3.2 异步调用模式下的解决方案
异步调用定义服务受理结果,通常返回状态:受理/未受理。返回结果状态:成功/失败
(1)异步调用接口超时
超时后查询补偿。
(2)异步调用内部超时
一旦受理,我们便要尽力将客户请求的操作处理成功。和同步模式下内部调用超时不一样。
(3)异步调用回调超时
服务1调用服务2,服务2调用服务3。服务2通知结果给服务1时超时。这个要让服务1查询服务2补偿。
4.3.3 消息队列异步处理模式的解决方案
(1)消息队列生产者超时
见3.5.5.
(2)消息队列消费者超时
a. 自增长消费。允许丢消息用这种方式。
b. 手工提交消费偏移量。一个消费者从消息服务器中取走消息,消息处理机先把消息持久到本地,告诉消息服务器在处理。这是消息服务期才移除消息。在没有告诉消息服务器在处理之前,发生各种问题,消息都存在于消息服务器中。消费者还可以继续处理。