为啥出现一致性问题

在传统单体架构中,数据状态的处理都在同一个服务和数据库中,而具有**ACID特性**的数据库支持强一致性,就是说数据库本身是不会出现不一致的状态的,比如我们常用的关系型数据库MySQL就是通过多版本控制协议(MVCC)的实现来保证了强一致性。 但是随着互联网的发展,用户增多&服务也越来越多越来越复杂,数据量和请求的并发量都上来了。为了满足这一变化,越来越多的系统从单体架构投入到服务化或者微服务架构。然而,微服务是把双刃剑,虽然能通过对服务的有效拆分来实现敏捷开发和自动化部署,并提高系统的水兵伸缩能力;但是微服务模式下,服务之间通过网络来通信(网络并不是可靠的),那么当网络异常时,就很容易引起各个系统之间的不一致。

一致性相关思想理论

一致性类别

  • 强一致 当更新操作完成之后,任何多个后续进程或者线程的访问都会返回最新的更新过的值。这种是对用户最友好的,就是用户上一次写什么,下一次就保证能读到什么。根据 CAP 理论,这种实现需要牺牲可用性。

  • 弱一致性 系统并不保证续进程或者线程的访问都会返回最新的更新过的值。系统在数据写入成功之后,不承诺立即可以读到最新写入的值,也不会具体的承诺多久之后可以读到。

  • 最终一致性 弱一致性的特定形式。系统保证在没有后续更新的前提下,系统最终返回上一次更新操作的值。在没有故障发生的前提下,不一致窗口的时间主要受通信延迟,系统负载和复制副本的个数影响。DNS 是一个典型的最终一致性系统。

CAP原理

  • C:Consistency: 一致性;同一时刻,同一数据,各系统间的值相同。
  • A:Availability:可用性;服务能一直保证是可用的状态,能保证在有限的时间内处理完请求并响应。
  • P:Partition Tolerance:分区容忍性;出现网络分区时(网络断了、程序bug、死机等),系统仍可继续工作。

CAP原理证明,任何分布式系统只可以同时满足以上特性中的两种。

BASE思想

BASE思想是解决一般分布式一致性问题的基本思路,其满足CAP原理,通过牺牲强一致性获得可用性,通过达到最终一致性来满足业务的绝大多数需求。

  • BA: Basically Available;基本可用
  • S: Soft state;软状态,状态在一段时间内可不同步
  • E: Eventually consistent;最终一致性,在一定时间窗口内,数据最终达成一致。

如何保证最终一致性

当数据量和请求量都很大的时候,系统无法通过向上扩展(scale up)或者说向上扩展(scale up)的成本很高时并且水平切分后无法将相关数据分到数据库的同一个片上时,为了保证系统的可用性,我们可以通过牺牲强一致性获得可用性,通过达到最终一致性来满足业务的绝大多数需求。

一般来说,保证最终一致性的核心思想就是查询->补偿,通过查询操作的状态,或者查询数据或消息的差异来判断是否有不一致的情况发生,如果有,则执行对应的补偿行为。补偿行为有以下几种:

  • 自动恢复,程序根据发生不一致的环境,自动重试或回滚操作来达到一致状态。
  • 运营手工恢复,程序无法自动恢复时,通过通知运营人员手工进行补偿。
  • 技术运营,通过手工数据库或者代码变更来进行补偿以达到一致状态。这是最糟的情况,生产中应尽量避免。

查询

对于查询,需要确保有保存对应的操作状态唯一区分每个操作

保存操作状态

通常是通过将操作执行状态或消息发送状态保存到数据库中以确保后续可以通过查询接口查询到对应的状态来执行补偿操作。一般步骤如下: 在操作执行前(消息发送前),将对应的操作记录(消息)持久化到业务数据库,状态标记为待执行(待发送),然后执行操作(发送消息),如果成功。则改变对应的状态为执行成功(发送成功)。 通常保存记录的数据库或消息中间件是独立于业务系统的。

唯一区分每个操作

在分布式系统中,这需要一个全局唯一的ID来区分每一个服务操作,对于全局唯一ID有以下两种方式:

  • 持久型,使用数据库表自增或Sequence生成,通常为了提高效率,每个应用会缓存一个批次的ID。
  • 时间型 ,一般由机器号、业务号、时间、单节点内自增ID等组成,可以保证ID的粗略递增。 eg. Twitter的Snowflake算法

补偿

单次补偿

服务操作完成后,立即通过查询接口查询单次操作的执行状态,及时的根据操作状态判断是否需要进行对应的补偿操作。

定期校对

事后定期异步的批量校对操作的状态,根据操作状态判断是否需要进行对应的补偿操作。定期校对多用于金融系统中,如商户交易对账、现金对账等都属于定期校对。 根据系统的需求,以上两种补偿方式可单独使用,也可一起使用以尽可能的保证数据的一致性。


在上面的处理过程中,有一种情况需要特别考虑,就是各个服务之间的通讯超时问题,由于超时情况下,我们不知道到服务到底是否执行成功,所以需要特殊考虑。

超时处理

交互模式

讲超时处理前,我们先大致讲下系统间的几种交互模式:

  • 同步调用模式,适用于大规模、高并发的短小操作 。eg. JDBC
  • 异步调用模式
  • 异步接口模式,适用于非核心链路上高负载的处理环节,通常操作比较耗时,且对时效性要求不高。eg. 商品出售后,商户的入账收入
  • 消息队列异步处理模式,通过消息队列,服务A将事件传递给服务B而**不需要等待服务B的返回结果(即上游服务不关心下游的处理结果)**。eg. 订单成功后,对应的物流处理

超时补偿原则

超时情况下,我们都需要对服务间超时照成的后果进行处理,处理方式有快速失败和内部补偿两种,补偿模式又分调用方补偿和接收方补偿两种。那么我们该如何做决定呢,可参考如下原则:

  • 服务A调用服务B,如果服务B响应服务A并且告诉服务A消息已接收,那么服务A的任务就结束了;如果服务B处理失败,那么服务B应该负责重试或补偿。在这种情况下,服务B需要接收消息持久化后再告诉服务A接收成功随后才去执行对应的处理操作
  • 服务A调用服务B,服务B没有给出明确的接收响应,eg 超时,那么服务A应该重试,知道服务B表明接收到了消息(这需要保证消息的幂等性或者进行滤重,不然会产生重复处理的问题)。

参考: