分布式服务化系统一致性的“最佳实干”

1 背景

一致性是一个抽象的、具备多重含义的计算机术语,在不一样应用场景下,有不一样的定义和含义。在传统的IT时代,一致性一般指强一致性,强一致性一般体如今你中有我、我中有你、浑然一体;而在互联网时代,一致性的含义远远超出了它原有的含义,在咱们讨论互联网时代的一致性以前,咱们先了解一下互联网时代的特色,互联网时代信息量巨大、须要计算能力巨大,不但对用户响应速度要求快,并且吞吐量指标也要向外扩展(既:水平伸缩),因而单节点的服务器没法知足需求,服务节点开始池化,想一想那个经典的故事,一只筷子一折就断,一把筷子怎么都折不断,可见人多力量大的思想是多么的重要,可是人多也不必定能解决全部事情,还得进行有序、合理的分配任务,进行有效的管理,因而互联网时代谈论最多的话题就是拆分,拆分通常分为“水平拆分”和“垂直拆分”(你们不要对应到数据库或者缓存拆分,这里主要表达一种逻辑)。这里,“水平拆分”指的是同一个功能因为单机节点没法知足性能需求,须要扩展成为多节点,多个节点具备一致的功能,组成一个服务池,一个节点服务一部分的请求量,团结起来共同处理大规模高并发的请求量。“垂直拆分”指的是按照功能拆分,秉着“专业的人干专业的事儿”的原则,把一个复杂的功能拆分到多个单一的简单的元功能,不一样的元功能组合在一块儿,和未拆分前完成的功能是一致的,因为每一个元功能职责单1、功能简单,让维护和变动都变得更简单、安全,更易于产品版本的迭代,在这样的一个互联网的时代和环境,一致性指分布式服务化系统之间的弱一致性,包括应用系统一致性和数据一致性。html

不管是水平拆分仍是垂直拆分,都解决了特定场景下的特定问题,凡事有好的一面,都会有坏的一面,拆分后的系统或者服务化的系统最大的问题就是一致性问题,这么多个具备元功能的模块,或者同一个功能池中的多个节点之间,如何保证他们的信息是一致的、工做步伐是一致的、状态是一致的、互相协调有序的工做呢?git

本文根据做者在互联网企业的实际项目经验,对服务化系统中最难解决的一致性问题进行研究和探讨,试图从实践经验中找到规律,抽象出模式,分享给你们,但愿对你们的项目实施有所帮助,在对实践的总结中也会对相关的一致性术语作最朴实的解释,但愿能帮助你们完全理解一致性的本质,并能将其应用到实践,解决读者现实中遇到的服务化系统的一致性问题,本文使用理论与实践相结合的方法,突出在实践中解决问题的模式,所以叫作《分布式服务化系统一致性的“最佳实干”》。github

2 问题

本节列举不一致会致使的种种问题,这也包括一例生活中的问题。算法

案例1:买房

假如你想要享受生活的随意,只想买个两居,不想让房贷有太大压力,而你媳妇却想要买个三居,还得带花园的,那么大家就不一致了,不一致致使生活不愉快、不协调,严重状况下还会吵架,可见生活中的不一致问题影响很大。sql

案例2:转帐

转帐是经典的不一致案例,设想一下银行为你处理一笔转帐,扣减你帐户上的余额,而后增长别人帐户的余额;若是扣减你的帐户余额成功,增长别人帐户余额失败,那么你就会损失这笔资金。反过来,若是扣减你的帐户余额失败,增长别人帐户余额成功,那么银行就会损失这笔资金,银行须要赔付。对于资金处理系统来讲,上面任何一种场景都是不容许发生的,一旦发生就会有资金损失,后果是不堪设想的,严重状况会让一个公司瞬间倒闭,可参考案例数据库

案例3:下订单和扣库存

电商系统中也有一个经典的案例,下订单和扣库存如何保持一致,若是先下订单,扣库存失败,那么将会致使超卖;若是下订单没有成功,扣库存成功,那么会致使少卖。两种状况都会致使运营成本的增长,严重状况下须要赔付。缓存

案例4:同步超时

服务化的系统间调用经常由于网络问题致使系统间调用超时,即便是网络很好的机房,在亿次流量的基数下,同步调用超时也是屡见不鲜。系统A同步调用系统B超时,系统A能够明确获得超时反馈,可是没法肯定系统B是否已经完成了预约的功能或者没有完成预约的功能。因而,系统A就迷茫了,不知道应该继续作什么,如何反馈给使用方。(曾经的一个B2B产品的客户要求接口超时从新通知他们,这个在技术上是难以实现的,由于服务器自己可能并不知道本身超时,可能会继续正常的返回数据,只是客户端并无接受到结果罢了,所以这不是一个合理的解决方案)。安全

案例5:异步回调超时

此案例和上一个同步超时案例相似,不过这个场景使用了异步回调,系统A同步调用系统B发起指令,系统B采用受理模式,受理后则返回受理成功,而后系统B异步通知系统A。在这个过程当中,若是系统A因为某种缘由迟迟没有收到回调结果,那么两个系统间的状态就不一致,互相认知不一样会致使系统间发生错误,严重状况下会影响核心事务,甚至会致使资金损失。服务器

案例6:掉单

分布式系统中,两个系统协做处理一个流程,分别为对方的上下游,若是一个系统中存在一个请求,一般指订单,另一个系统不存在,则致使掉单,掉单的后果很严重,有时候也会致使资金损失。网络

案例7:系统间状态不一致

这个案例与上面掉单案例相似,不一样的是两个系统间都存在请求,可是请求的状态不一致。

案例8:缓存和数据库不一致

交易相关系统基本离不开关系型数据库,依赖关系型数据库提供的ACID特性(后面介绍),可是在大规模高并发的互联网系统里,一些特殊的场景对读的性能要求极高,服务于交易的数据库难以抗住大规模的读流量,一般须要在数据库前垫缓存,那么缓存和数据库之间的数据如何保持一致性?是要保持强一致呢仍是弱一致性呢?

案例9:本地缓存节点间不一致

一个服务池上的多个节点为了知足较高的性能需求,须要使用本地缓存,使用了本地缓存,每一个节点都会有一份缓存数据的拷贝,若是这些数据是静态的、不变的,那永远都不会有问题,可是若是这些数据是半静态的或者常被更新的,当被更新的时候,各个节点更新是有前后顺序的,在更新的瞬间,各个节点的数据是不一致的,若是这些数据是为某一个开关服务的,想象一下重复的请求走进了不一样的节点(在failover或者补偿致使的场景下,重复请求是必定会发生的,也是服务化系统必须处理的),一个请求走了开关打开的逻辑,同时另一个请求走了开关关闭的逻辑,这致使请求被处理两次,最坏的状况下会致使灾难性的后果,就是资金损失。

案例10:缓存数据结构不一致

这个案例会时有发生,某系统须要种某一数据结构的缓存,这一数据结构有多个数据元素组成,其中,某个数据元素都须要从数据库中或者服务中获取,若是一部分数据元素获取失败,因为程序处理不正确,仍然将不彻底的数据结构存入缓存,那么缓存的消费者消费的时候颇有可能由于没有合理处理异常状况而出错。

3 模式

3.1 生活中不一致问题的解决

你们回顾一下上一节列举的生活中的案例1-买房,若是置身事外来看,解决这种不一致的办法有两个,一个是避免不一致的发生,若是已是媳妇了就很差办了:),还有一种方法就是慢慢的补偿,先买个两居,而后慢慢的等资金充裕了再换三居,买比特币赚了再换带花园的房子,因而问题最终被解决了,最终你们处于一致的状态,都开心了。这样能够解决案例1的问题,很天然因为有了过渡的方法,问题在不经意间就消失了,可见“过渡”也是解决一致性问题的一个模式。

从案例1的解决方案来看,咱们要解决一致性问题,一个最直接最简单的方法就是保持强一致性,对于案例1的状况,尽可能避免在结婚前两我的可以互相了解达成一致,避免不一致问题的发生;不过有些事情事已至此,发生了就是发生了,出现了不一致的问题,咱们应该考虑去补偿,尽最大的努力从不一致状态修复到一致状态,避免损失所有或者一部分,也不失为一个好方法。

所以,避免不一致是上策,出现了不一致及时发现及时修复是中策,有问题不积极解决留给他人解决是下策。

3.2 酸碱平衡理论

ACID在英文中的意思是“酸”,BASE的意识是“碱”,这一段讲的是“酸碱平衡”的故事。

1. ACID(酸)

如何保证强一致性呢?计算机专业的童鞋在学习关系型数据库的时候都学习了ACID原理,这里对ACID作个简单的介绍。若是想全面的学习ACID原理,请参考ACID

关系型数据库天生就是解决具备复琐事务场景的问题,关系型数据库彻底知足ACID的特性。

ACID指的是:

  • A: Atomicity,原子性
  • C: Consistency,一致性
  • I: Isolation,隔离性
  • D: Durability,持久性

具备ACID的特性的数据库支持强一致性,强一致性表明数据库自己不会出现不一致,每一个事务是原子的,或者成功或者失败,事物间是隔离的,互相彻底不影响,并且最终状态是持久落盘的,所以,数据库会从一个明确的状态到另一个明确的状态,中间的临时状态是不会出现的,若是出现也会及时的自动的修复,所以是强一致的。

3个典型的关系型数据库Oracle、Mysql、Db2都能保证强一致性,Oracle和Mysql使用多版本控制协议实现,而DB2使用改进的两阶段提交协议来实现。

若是你在为交易相关系统作技术选型,交易的存储应该只考虑关系型数据库,对于核心系统,若是须要较好的性能,能够考虑使用更强悍的硬件,这种向上扩展(升级硬件)虽然成本较高,可是是最简单粗暴有效的方式,另外,Nosql彻底不适合交易场景,Nosql主要用来作数据分析、ETL、报表、数据挖掘、推荐、日志处理等非交易场景。

前面提到的案例2-转帐案例3-下订单和扣库存均可以利用关系型数据库的强一致性解决。

然而,前面提到,互联网项目多数具备大规模高并发的特性,必须应用拆分的理念,对高并发的压力采起“大而化小、小而化了”的方法,不然难以知足动辄亿级流量的需求,即便使用关系型数据库,单机也难以知足存储和TPS上的需求。为了保证案例2-转帐能够利用关系型数据库的强一致性,在拆分的时候尽可能的把转帐相关的帐户放入一个数据库分片,对于案例3,尽可能的保证把订单和库存放入同一个数据库分片,这样经过关系型数据库天然就解决了不一致的问题。

然而,有些时候事与愿违,因为业务规则的限制,没法将相关的数据分到同一个数据库分片,这个时候咱们就须要实现最终一致性。

对于案例2-转帐场景,假设帐户数量巨大,对帐户存储进行了拆分,关系型数据库一共分了8个实例,每一个实例8个库,每一个库8个表,共512张表,假如要转帐的两个帐户正好落在了一个库里,那么能够依赖关系型数据库的事务保持强一致性。

若是要转帐的两个帐户正好落在了不一样的库里,转帐操做是没法封装在同一个数据库事务中的,这个时候会发生一个库的帐户扣减余额成功,另一个库的帐户增长余额失败的状况。

对于这种状况,咱们须要继续探讨解决之道,CAP原理和BASE原理,BASE原理经过记录事务的中间的临时状态,实现最终一致性。

2. CAP(帽子理论)

若是想深刻的学习CAP理论,请参考CAP

因为对系统或者数据进行了拆分,咱们的系统再也不是单机系统,而是分布式系统,针对分布式系的帽子理论包含三个元素:

  • C:Consistency,一致性, 数据一致更新,全部数据变更都是同步的
  • A:Availability,可用性, 好的响应性能,彻底的可用性指的是在任何故障模型下,服务都会在有限的时间处理响应
  • P:Partition tolerance,分区容错性,可靠性

帽子理论证实,任何分布式系统只可同时知足二点,无法三者兼顾。关系型数据库因为关系型数据库是单节点的,所以,不具备分区容错性,可是具备一致性和可用性,而分布式的服务化系统都须要知足分区容错性,那么咱们必须在一致性和可用性中进行权衡,具体表如今服务化系统处理的异常请求在某一个时间段内多是不彻底的,可是通过自动的或者手工的补偿后,达到了最终的一致性。

3. BASE(碱)

BASE理论解决CAP理论提出了分布式系统的一致性和可用性不能兼得的问题,若是想全面的学习BASE原理,请参考Eventual consistency

BASE在英文中有“碱”的意思,对应本节开头的ACID在英文中“酸”的意思,基于这两个名词提出了酸碱平衡的结论,简单来讲是在不一样的场景下,能够分别利用ACID和BASE来解决分布式服务化系统的一致性问题。

BASE模型与ACID模型大相径庭,知足CAP理论,经过牺牲强一致性,得到可用性,通常应用在服务化系统的应用层或者大数据处理系统,经过达到最终一致性来尽可能知足业务的绝大部分需求。

BASE模型包含个三个元素:

  • BA:Basically Available,基本可用
  • S:Soft State,软状态,状态能够有一段时间不一样步
  • E:Eventually Consistent,最终一致,最终数据是一致的就能够了,而不是时时保持强一致

BASE模型的软状态是实现BASE理论的方法,基本可用和最终一致是目标。按照BASE模型实现的系统,因为不保证强一致性,系统在处理请求的过程当中,能够存在短暂的不一致,在短暂的不一致窗口请求处理处在临时状态中,系统在作每步操做的时候,经过记录每个临时状态,在系统出现故障的时候,能够从这些中间状态继续未完成的请求处理或者退回到原始状态,最后达到一致的状态。

案例1-转帐为例,咱们把用户A给用户B转帐分红四个阶段,第一个阶段用户A准备转帐,第二个阶段从用户A帐户扣减余额,第三个阶段对用户B增长余额,第四个阶段完成转帐。系统须要记录操做过程当中每一步骤的状态,一旦系统出现故障,系统可以自动发现没有完成的任务,而后,根据任务所处的状态,继续执行任务,最终完成任务,达到一致的最终状态。

在实际应用中,上面这个过程一般是经过持久化执行任务的状态和环境信息,一旦出现问题,定时任务会捞取未执行完的任务,继续未执行完的任务,直到执行完成为止,或者取消已经完成的部分操做回到原始状态。这种方法在任务完成每一个阶段的时候,都要更新数据库中任务的状态,这在大规模高并发系统中不会有太好的性能,一个更好的办法是用Write-Ahead Log(写前日志),这和数据库的Bin Log(操做日志)类似,在作每个操做步骤,都先写入日志,若是操做遇到问题而中止的时候,能够读取日志按照步骤进行恢复,而且继续执行未完成的工做,最后达到一致。写前日志能够利用机械硬盘的追加写而达到较好性能,所以,这是一种专业化的实现方式,多数业务系系统仍是使用数据库记录的字段来记录任务的执行状态,也就是记录中间的“软状态”,一个任务的状态流转通常能够经过数据库的行级锁来实现,这比使用Write-Ahead Log实现更简单、更快速。

有了BASE理论做为基础,咱们对复杂的分布式事务进行拆解,对其中的每一步骤都记录其状态,有问题的时候能够根据记录的状态来继续执行任务,达到最终的一致,经过这个方法咱们能够解决案例2-转帐案例3-下订单和扣库存中遇到的问题。

4. 酸碱平衡的总结

  1. 使用向上扩展(强悍的硬件)运行专业的关系型数据库(例如:Oracle或者DB2)可以保证强一致性,钱能解决的问题就不是问题
  2. 若是钱是问题,能够对廉价硬件运行的开源关系型数据库(例如:Mysql)进行分片,将相关的数据分到数据库的同一个片,仍然可以使用关系型数据库保证事务
  3. 若是业务规则限制,没法将相关的数据分到同一个片,就须要实现最终一致性,经过记录事务的软状态(中间状态、临时状态),一旦处于不一致,能够经过系统自动化或者人工干预来修复不一致的状况

3.3 分布式一致性协议

国际开放标准组织Open Group定义了DTS(分布式事务处理模型),模型中包含4个角色:应用程序、事务管理器、资源管理器、通讯资源管理器四部分。事务处理器是统管全局的管理者,资源处理器和通讯资源处理器是事务的参与者。

J2EE规范也包含此分布式事务处理模型的规范,并在全部的AppServer中进行实现,J2EE规范中定义了TX协议和XA协议,TX协议定义应用程序与事务管理器之间的接口,而XA协议定义了事务管理器与资源处理器之间的接口,在过去,你们使用AppServer,例如:Websphere、Weblogic、Jboss等配置数据源的时候会看见相似XADatasource的数据源,这就是实现了DTS的关系型数据库的数据源。企业级开发JEE中,关系型数据库、JMS服务扮演资源管理器的角色,而EJB容器则扮演事务管理器的角色。

下面咱们就介绍两阶段提交协议三阶段提交协议以及阿里巴巴提出的TCC,它们都是根据DTS这一思想演变出来的。

1. 两阶段提交协议

上面描述的JEE的XA协议就是根据两阶段提交来保证事务的完整性,并实现分布式服务化的强一致性。

两阶段提交协议把分布式事务分红两个过程,一个是准备阶段,一个是提交阶段,准备阶段和提交阶段都是由事务管理器发起的,为了接下来说解方便,咱们把事务管理器称为协调者,把资管管理器称为参与者。

两阶段以下:

  1. 准备阶段:协调者向参与者发起指令,参与者评估本身的状态,若是参与者评估指令能够完成,参与者会写redo或者undo日志(这也是前面提起的Write-Ahead Log的一种),而后锁定资源,执行操做,可是并不提交
  2. 提交阶段:若是每一个参与者明确返回准备成功,也就是预留资源和执行操做成功,协调者向参与者发起提交指令,参与者提交资源变动的事务,释放锁定的资源;若是任何一个参与者明确返回准备失败,也就是预留资源或者执行操做失败,协调者向参与者发起停止指令,参与者取消已经变动的事务,执行undo日志,释放锁定的资源

两阶段提交协议成功场景示意图以下:

两阶段提交协议

咱们看到两阶段提交协议在准备阶段锁定资源,是一个重量级的操做,并能保证强一致性,可是实现起来复杂、成本较高,不够灵活,更重要的是它有以下致命的问题:

  1. 阻塞:从上面的描述来看,对于任何一次指令必须收到明确的响应,才会继续作下一步,不然处于阻塞状态,占用的资源被一直锁定,不会被释放
  2. 单点故障:若是协调者宕机,参与者没有了协调者指挥,会一直阻塞,尽管能够经过选举新的协调者替代原有协调者,可是若是以前协调者在发送一个提交指令后宕机,而提交指令仅仅被一个参与者接受,而且参与者接收后也宕机,新上任的协调者没法处理这种状况
  3. 脑裂:协调者发送提交指令,有的参与者接收到执行了事务,有的参与者没有接收到事务,就没有执行事务,多个参与者之间是不一致的

上面全部的这些问题,都是须要人工干预处理,没有自动化的解决方案,所以两阶段提交协议在正常状况下能保证系统的强一致性,可是在出现异常状况下,当前处理的操做处于错误状态,须要管理员人工干预解决,所以可用性不够好,这也符合CAP协议的一致性和可用性不能兼得的原理。

2. 三阶段提交协议

三阶段提交协议是两阶段提交协议的改进版本。它经过超时机制解决了阻塞的问题,而且把两个阶段增长为三个阶段:

  1. 询问阶段:协调者询问参与者是否能够完成指令,协调者只须要回答是仍是不是,而不须要作真正的操做,这个阶段超时致使停止
  2. 准备阶段:若是在询问阶段全部的参与者都返回能够执行操做,协调者向参与者发送预执行请求,而后参与者写redo和undo日志,执行操做,可是不提交操做;若是在询问阶段任何参与者返回不能执行操做的结果,则协调者向参与者发送停止请求,这里的逻辑与两阶段提交协议的的准备阶段是类似的,这个阶段超时致使成功
  3. 提交阶段:若是每一个参与者在准备阶段返回准备成功,也就是预留资源和执行操做成功,协调者向参与者发起提交指令,参与者提交资源变动的事务,释放锁定的资源;若是任何一个参与者返回准备失败,也就是预留资源或者执行操做失败,协调者向参与者发起停止指令,参与者取消已经变动的事务,执行undo日志,释放锁定的资源,这里的逻辑与两阶段提交协议的提交阶段一致

三阶段提交协议成功场景示意图以下:

三阶段提交协议

然而,这里与两阶段提交协议有两个主要的不一样:

  1. 增长了一个询问阶段,询问阶段能够确保尽量早的发现没法执行操做而须要停止的行为,可是它并不能发现全部的这种行为,只会减小这种状况的发生
  2. 在准备阶段之后,协调者和参与者执行的任务中都增长了超时,一旦超时,协调者和参与者都继续提交事务,默认为成功,这也是根据几率统计上超时后默认成功的正确性最大

三阶段提交协议与两阶段提交协议相比,具备如上的优势,可是一旦发生超时,系统仍然会发生不一致,只不过这种状况不多见罢了,好处就是至少不会阻塞和永远锁定资源。

3. TCC

上面两节讲解了两阶段提交协议和三阶段提交协议,实际上他们能解决案例2-转帐案例3-下订单和扣库存中的分布式事务的问题,可是遇到极端状况,系统会发生阻塞或者不一致的问题,须要运营或者技术人工解决。不管两阶段仍是三阶段方案中都包含多个参与者、多个阶段实现一个事务,实现复杂,性能也是一个很大的问题,所以,在互联网高并发系统中,鲜有使用两阶段提交和三阶段提交协议的场景。

阿里巴巴提出了新的TCC协议,TCC协议将一个任务拆分红Try、Confirm、Cancel,正常的流程会先执行Try,若是执行没有问题,再执行Confirm,若是执行过程当中出了问题,则执行操做的逆操Cancel,从正常的流程上讲,这仍然是一个两阶段的提交协议,可是,在执行出现问题的时候,有必定的自我修复能力,若是任何一个参与者出现了问题,协调者经过执行操做的逆操做来取消以前的操做,达到最终的一致状态。

能够看出,从时序上,若是遇到极端状况下TCC会有不少问题的,例如,若是在Cancel的时候一些参与者收到指令,而一些参与者没有收到指令,整个系统仍然是不一致的,这种复杂的状况,系统首先会经过补偿的方式,尝试自动修复的,若是系统没法修复,必须由人工参与解决。

从TCC的逻辑上看,能够说TCC是简化版的三阶段提交协议,解决了两阶段提交协议的阻塞问题,可是没有解决极端状况下会出现不一致和脑裂的问题。然而,TCC经过自动化补偿手段,会把须要人工处理的不一致状况降到到最少,也是一种很是有用的解决方案,根据线人,阿里在内部的一些中间件上实现了TCC模式。

咱们给出一个使用TCC的实际案例,在秒杀的场景,用户发起下单请求,应用层先查询库存,确认商品库存还有余量,则锁定库存,此时订单状态为待支付,而后指引用户去支付,因为某种缘由用户支付失败,或者支付超时,系统会自动将锁定的库存解锁供其余用户秒杀。

TCC协议使用场景示意图以下:

TCC

总结一下,两阶段提交协议、三阶段提交协议、TCC协议都能保证分布式事务的一致性,他们保证的分布式系统的一致性从强到弱,TCC达到的目标是最终一致性,其中任何一种方法均可以不一样程度的解决案例2:转帐、案例3:下订单和扣库存的问题,只是实现的一致性的级别不同而已,对于案例4:同步超时能够经过TCC的理念解决,若是同步调用超时,调用方可使用fastfail策略,返回调用方的使用方失败的结果,同时调用服务的逆向cancel操做,保证服务的最终一致性。

3.4 保证最终一致性的模式

在大规模高并发服务化系统中,一个功能被拆分红多个具备单一功能的元功能,一个流程会有多个系统的多个元功能组合实现,若是使用两阶段提交协议和三阶段提交协议,确实能解决系统间一致性问题,除了这两个协议带来的自身的问题,这些协议的实现比较复杂、成本比较高,最重要的是性能并很差,相比来看,TCC协议更简单、容易实现,可是TCC协议因为每一个事务都须要执行Try,再执行Confirm,略微显得臃肿,所以,在现实的系统中,底线要求仅仅须要能达到最终一致性,而不须要实现专业的、复杂的一致性协议,实现最终一致性有一些很是有效的、简单粗暴的模式,下面就介绍这些模式及其应用场景。

1. 查询模式

任何一个服务操做都须要提供一个查询接口,用来向外部输出操做执行的状态。服务操做的使用方能够经过查询接口,得知服务操做执行的状态,而后根据不一样状态来作不一样的处理操做。

为了可以实现查询,每一个服务操做都须要有惟一的流水号标识,也可以使用这次服务操做对应的资源ID来标志,例如:请求流水号、订单号等。

首先,单笔查询操做是必须提供的,咱们也鼓励使用单笔订单查询,这是由于每次调用须要占用的负载是可控的,批量查询则根据须要来提供,若是使用了批量查询,须要有合理的分页机制,而且必须限制分页的大小,以及对批量查询的QPS须要有容量评估和流控等。

查询模式的示意图以下:

查询模式

对于案例4:同步超时、案例5:异步回调超时、案例6:掉单、案例7:系统间状态不一致,咱们都须要使用查询模式来了解被调用服务的处理状况,来决定下一步作什么:补偿未完成的操做仍是回滚已经完成的操做。

2. 补偿模式

有了上面的查询模式,在任何状况下,咱们都能得知具体的操做所处的状态,若是整个操做处于不正常的状态,咱们须要修正操做中有问题的子操做,这可能须要从新执行未完成的子操做,后者取消已经完成的子操做,经过修复使整个分布式系统达到一致,为了让系统最终一致而作的努力都叫作补偿。

对于服务化系统中同步调用的操做,业务操做发起的主动方在尚未获得业务操做执行方的明确返回或者调用超时,场景可参考案例4:同步超时,这个时候业务发起的主动方须要及时的调用业务执行方得到操做执行的状态,这里使用查询模式,得到业务操做的执行方的状态后,若是业务执行方已经完预设的工做,则业务发起方给业务的使用方返回成功,若是业务操做的执行方的状态为失败或者未知,则会当即告诉业务的使用方失败,而后调用业务操做的逆向操做,保证操做不被执行或者回滚已经执行的操做,让业务的使用方、业务发起的主动方、业务的操做方最终达成一致的状态。

补偿模式的示意图以下:

补偿模式

补偿操做根据发起形式分为:

  1. 自动恢复:程序根据发生不一致的环境,经过继续未完成的操做,或者回滚已经完成的操做,自动来达到一致
  2. 通知运营:若是程序没法自动恢复,而且设计时考虑到了不一致的场景,能够提供运营功能,经过运营手工进行补偿
  3. 通知技术:若是很不巧,系统没法自动回复,又没有运营功能,那必须经过技术手段来解决,技术手段包括走数据库变动或者代码变动来解决,这是最糟的一种场景

3. 异步确保模式

异步确保模式是补偿模式的一个典型案例,常常应用到使用方对响应时间要求并不过高,咱们一般把这类操做从主流程中摘除,经过异步的方式进行处理,处理后把结果经过通知系统通知给使用方,这个方案最大的好处可以对高并发流量进行消峰,例如:电商系统中的物流、配送,以及支付系统中的计费、入帐等。

实践中,将要执行的异步操做封装后持久入库,而后经过定时捞取未完成的任务进行补偿操做来实现异步确保模式,只要定时系统足够健壮,任何一个任务最终会被成功执行。

异步确保模式的示意图以下:

异步确保模式

对于案例5:异步回调超时,使用的就是异步确保模式,这种状况下对于某个操做,若是迟迟没有收到响应,咱们经过查询模式和补偿模式来继续未完成的操做。

4. 按期校对模式

既然咱们在系统中实现最终一致性,系统在没有达到一致以前,系统间的状态是不一致的,甚至是混乱的,须要补偿操做来达到一致的目的,可是咱们如何来发现须要补偿的操做呢?

在操做的主流程中的系统间执行校对操做,咱们能够过后异步的批量校对操做的状态,若是发现不一致的操做,则进行补偿,补偿操做与补偿模式中的补偿操做是一致的。

另外,实现按期校对的一个关键就是分布式系统中须要有一个自始至终惟一的ID,ID的生成请参考SnowFlake

在分布式系统中,全局惟一ID的示意图以下:

惟一ID

通常状况下,生成全局惟一ID有两种方法:

  1. 持久型:使用数据库表自增字段或者Sequence生成,为了提升效率,每一个应用节点能够缓存一批次的ID,若是机器重启可能会损失一部分ID,可是这并不会产生任何问题
  2. 时间型:通常由机器号、业务号、时间、单节点内自增ID组成,因为时间通常精确到秒或者毫秒,所以不须要持久就能保证在分布式系统中全局惟1、粗略递增能特色

实践中,为了能在分布式系统中迅速的定位问题,通常的分布式系统都有技术支持系统,它可以跟踪一个请求的调用链,调用链是在二维的维度跟踪一个调用请求,最后造成一个调用树,原理可参考谷歌的论文Dapper, a Large-Scale Distributed Systems Tracing Infrastructure,一个开源的参考实现为pinpoint

在分布式系统中,调用链的示意图以下:

调用链

全局的惟一流水ID能够把一个请求在分布式系统中的流转的路径聚合,而调用链中的spanid能够把聚合的请求路径经过树形结构进行展现,让技术支持人员轻松的发现系统出现的问题,可以快速定位出现问题的服务节点,提升应急效率。

关于订单跟踪、调用链跟踪、业务链跟踪,咱们会在后续文章中详细介绍。

在分布式系统中构建了惟一ID,调用链等基础设施,咱们很容易对系统间的不一致进行核对,一般咱们须要构建第三方的按期核对系统,以第三方的角度来监控服务执行的健康程度。

按期核对系统示意图以下:

按期核对模式

对于案例6:掉单、案例7:系统间状态不一致一般经过按期校对模式发现问题,并经过补偿模式来修复,最后完成系统间的最终一致性。

按期校对模式多应用在金融系统,金融系统因为涉及到资金安全,须要保证百分之百的准确性,因此,须要多重的一致性保证机制,包括:系统间的一致性对帐、现金对帐、帐务对帐、手续费对帐等等,这些都属于按期校对模式,顺便说一下,金融系统与社交应用在技术上本质的区别在于社交应用在于量大,而金融系统在于数据的准确性。

到如今为止,咱们看到经过查询模式、补偿模式、按期核对模式能够解决案例4到案例7的全部问题,对于案例4:同步超时,若是同步超时,咱们须要查询状态进行补偿,对于案例5:异步回调超时,若是迟迟没有收到回调响应,咱们也会经过查询状态进行补偿,对于案例6:掉单、案例7:系统间状态不一致,咱们经过按期核对模式能够保证系统间操做的一致性,避免掉单和状态不一致致使问题。

5. 可靠消息模式

在分布式系统中,对于主流程中优先级比较低的操做,大多采用异步的方式执行,也就是前面提到的异步确保型,为了让异步操做的调用方和被调用方充分的解耦,也因为专业的消息队列自己具备可伸缩、可分片、可持久等功能,咱们一般经过消息队列实现异步化,对于消息队列,咱们须要创建特殊的设施保证可靠的消息发送以及处理机的幂等等。

消息的可靠发送

消息的可靠发送能够认为是尽最大努力发送消息通知,有两种实现方法:

第一种,发送消息以前,把消息持久到数据库,状态标记为待发送,而后发送消息,若是发送成功,将消息改成发送成功。定时任务定时从数据库捞取必定时间内未发送的消息,将消息发送。

消息发送模式1

第二种,实现方式与第一种相似,不一样的是持久消息的数据库是独立的,并不耦合在业务系统中。发送消息以前,先发送一个预消息给某一个第三方的消息管理器,消息管理器将其持久到数据库,并标记状态为待发送,发送成功后,标记消息为发送成功。定时任务定时从数据库捞取必定时间内未发送的消息,回查业务系统是否要继续发送,根据查询结果来肯定消息的状态。

消息发送模式2

一些公司把消息的可靠发送实如今了中间件里,经过Spring的注入,在消息发送的时候自动持久消息记录,若是有消息记录没有发送成功,定时会补偿发送。

消息处理器的幂等性

若是咱们要保证消息可靠的发送,简单来讲,要保证消息必定要发送出去,那么就须要有重试机制,有了重试机制,消息必定会重复,那么咱们须要对重复作处理。

处理重复的最佳方式为保证操做的幂等性,幂等性的数学公式为:

f(f(x)) = f(x)

保证操做的幂等性经常使用的几个方法:

  1. 使用数据库表的惟一键进行滤重,拒绝重复的请求
  2. 使用分布式表对请求进行滤重
  3. 使用状态流转的方向性来滤重,一般使用行级锁来实现(后续在锁相关的文章中详细说明)
  4. 根据业务的特色,操做自己就是幂等的,例如:删除一个资源、增长一个资源、得到一个资源等

6. 缓存一致性模型

大规模高并发系统中一个常见的核心需求就是亿级的读需求,显然,关系型数据库并非解决高并发读需求的最佳方案,互联网的经典作法就是使用缓存抗读需求,下面有一些使用缓存的保证一致性的最佳实践:

  1. 若是性能要求不是很是的高,尽可能使用分布式缓存,而不要使用本地缓存
  2. 种缓存的时候必定种彻底,若是缓存数据的一部分有效,一部分无效,宁肯放弃种缓存,也不要把部分数据种入缓存
  3. 数据库与缓存只须要保持弱一致性,而不须要强一致性,读的顺序要先缓存,后数据库,写的顺序要先数据库,后缓存

这里的最佳实践可以解决案例8:缓存和数据库不一致、案例9:本地缓存节点间不一致、案例10:缓存数据结构不一致的问题,对于数据存储层、缓存与数据库、Nosql等的一致性是更深刻的存储一致性技术,将会在后续文章单独介绍,这里的数据一致性主要是处理应用层与缓存、应用层与数据库、一部分的缓存与数据库的一致性。

3.5 专题模式

这一节介绍特殊场景下的一致性问题和解决方案。

迁移开关的设计

在大多数企业里,新项目和老项目通常会共存,你们都在努力的下掉老项目,可是因为种种缘由老是下不掉,若是要完全的下掉老项目,就必需要有很是完善的迁移方案,迁移是一项很是复杂而艰巨的任务,我会在未来的文章中详细探讨迁移方案、流程和技术,这里咱们只对迁移中使用的开关进行描述。

迁移过程必须使用开关,开关通常都会基于多个维度来设计,例如:全局的、用户的、角色的、商户的、产品的等等,若是迁移过程当中遇到问题,咱们须要关闭开关,迁移回老的系统,这须要咱们的新系统兼容老的数据,老的系统也兼容新的数据,从某种意义上来说,迁移比实现新系统更加困难。

曾经看过不少简单的开关设计,有的开关设计在应用层次,经过一个curl语句调用,没有权限控制,这样的开关在服务池的每一个节点都是不一样步的、不一致的;还有的系统把开关配置放在中心化的配置系统、数据库或者缓存等,处理的每一个请求都经过统一的开关来判断是否迁移等等,这样的开关有一个致命的缺点,服务请求在处理过程当中,开关可能会变化,各个节点之间开关可能不一样步、不一致,致使重复的请求可能走到新的逻辑又走了老的逻辑,若是新的逻辑和老的逻辑没有保证幂等性,这个请求就被重复处理了,若是是金融行业的应用,可能会致使资金损失,电商系统可能会致使发货并退款等问题。

这里面咱们推荐使用订单开关,无论咱们在什么维度上设计了开关,接收到服务请求后,咱们在请求建立的关联实体(例如:订单)上标记开关,之后的任何处理流程,包括同步的和异步的处理流程,都经过订单上的开关来判断,而不是经过全局的或者基于配置的开关,这样在订单建立的时候,开关已经肯定,再也不变动,一旦一份数据再也不发生变化,那么它永远是线程安全的,而且不会有不一致的问题。

这个模式在生产中使用比较频繁,建议每一个企业都把这个模式做为设计评审的一项,若是不检查这一项,不少开发童鞋都会偷懒,直接在配置中或者数据库中作个开关就上线了。

4 总结

本文从一致性问题的实践出发,从大规模高并发服务化系统的实践经验中进行总结,列举致使不一致的具体问题,围绕着具体问题,总结出解决不一致的方法,而且抽象成模式,供你们在开发服务化系统的过程当中参考。

另外,因为篇幅有限,还有一些关于分布式一致性的技术没法在一篇文章中与你们分享,包括:paxos算法、raft算法、zab算法、nwr算法、一致性哈希等,我会在后续文章中详细介绍。

相关文章
相关标签/搜索