2.分布式服务架构:原理、设计与实战 --- 彻底解决分布式系统一致性的问题

	微服务在服务化的基础上,对服务化的细节和方案进行了细化,重点突出无中心化管理的微服务架构,通过对服务进行有效的拆分来实现敏捷开发和自动化部署,
并在海量的用户请求下,提供了微服务架构下细粒度的水平伸缩能力。然而,微服务架构是一把双刃剑,我们在享受微服务对单体系统拆分后的红利的同时,也会遇到
数据模型和服务之间不一致的问题。

2.1 什么是一致性 
	拆分分为水平拆分和垂直拆分:
		1.水平拆分指的是单一节点无法满足性能要求,需要扩展多个节点,多个节点具有同一个功能,组成一个服务池,一个节点服务一部分请求量,所有节点共同
		处理大规模并发的请求量。
		2.垂直拆分指按照功能进行拆分,"专业人干专业的事情",把一个复杂的功能拆分为多个单一,简单的功能,不同的单一功能组合在一起,和未拆封前完成的
		功能是一样的。由于每个功能职责单一,简单,使得维护和变更都变得简单,容易,安全,所以更容易于产品版本的迭代,还能快速的敏捷发布和上线。

	在互联网时代,一致性指的是分布式系统之间的弱一致性,包括应用系统的一致性和数据的一致性。

2.2 一致性问题 
	案例1:下订单和扣库存
		如果先下订单,扣库存失败,那么将导致超卖;如果下订单不成功,扣库存成功,那么会导致少卖。

	案例2:同步调用超时
		服务化的系统间调用常常因为网络问题导致系统间调用超时。系统A同步调用系统B超时,系统A可以明确得到超时反馈,但是无法确定系统B是否已经完成了
	预设的功能。于是,系统A不知道应该继续做什么,如何反馈给使用方。

	案例3:异步回调超时
		系统A同步调用系统B发起指令,系统B采用受理模式,受理后返回成功信息,然后系统B处理后异步通知系统A处理结果。在这个过程中,如果系统A由于某种
	原因迟迟没有收到回调结果,那么这2个系统之间的状态就不一致。

	案例4:掉单
		在分布式系统中,两个系统协作处理一个流程,分别为对方的上下游,如果一个系统中存在一个请求(通常指订单),另外一个系统不存在,则会导致掉单。

	案例5:系统间状态不一致
		不同的是两个系统间都存在请求,但是请求的状态不一致。

	案例6:缓存和数据库不一致
		缓存和数据库之间的数据如何保持一致?要保持强一致还是弱一致?

	案例7:本地缓存节点不一致
		一个服务池上的多个节点为了满足高性能的需求,需要使用本地缓存,这样每个节点都会有一份缓存数据的复制。如果是静态的,不变的,就永远不会
	有问题,但是如果这些数据是半静态或者经常更新的,则被更新时各个节点的更新是有先后顺序的,在更新的瞬间,在某个时间窗口内各个节点的数据是不
	一致的。

	案例8:缓存数据结构不一致
		某系统需要在缓存中缓存某种类型的数据,该数据由多个数据元素组成,其中,某个数据需要从数据库或者服务中获取,如果一个部分获取失败,则由于
	程序处理不正确,仍然将不完全的数据存入缓存中,就会有问题。

2.3 解决一致性问题的模式和思路 
	2.3.1 酸碱平衡理论 
		1.ACID(酸)
			1.A:Atomicity,原子性
			2.C:Consistency,一致性
			3.I:Isolation,隔离性
			4.D:Durability,持久性

		如果在为交易的相关系统做技术选型,则交易的存储应该只考虑关系型数据库。另外,NoSQL 完全不适合交易场景,主要用来做数据分析,ETL,
	报表,数据挖掘,推荐,日志处理,调用链路追踪等非核心交易场景。

		2.CAP(帽子原理)
			C:Consistency,一致性
			A:Availability,可用性
			P:Partition tolerance,分区容忍性

		CPU 原理证明,任何分布式系统只能满足以上2点,无法三者兼顾。由于关系型数据库是单点无法复制的,因此不具备分区容忍性,但具有一致性和
	可用性,而分布式的服务化系统都需要满足分区容忍性,那么我们就必须在一致性和可用性之间进行权衡。如果在网络上有消息丢失,也就是出现了网络分区,
	则复制操作可能会被延后,如果这时我们的使用方等待复制完成再返回,则可能导致在有限时间内无法返回,就失去了可用性;而如果使用方不等待复制完成,
	而在主分片写完后直接返回,则具有了可用性,但是失去了一致性。

		3.BASE(碱)
			BASE 思想解决了CAP提出的分布式系统的一致性和可用性不可兼得的问题。BASE 思想与 ACID 原理不同,它满足CAP原理,通过系统强一致性
		来获得可用性,一般应用于服务化系统的应用层或者大数据处理系统中,通过达到最终一致性来满足业务绝大多数的需求。BASE 模型包含如下三个元素:
			BA : Basically Available,基本可用
			S : Soft State,软状态,状态可以在一段时间内不同步
			E : Eventually Consistent,最终一致,在一定时间窗口内,最终数据达成一致即可。

			软状态是实现BASE思想的方法,基本可用和最终一致是目标。以BASE思想实现的系统由于不保证强一致性,所以系统在处理请求的过程中可以短暂
		存在不一致,在短暂的不一致的时间窗口内,请求处理处于临时状态中,系统在进行每步操作时,通过记录每个临时状态,在系统出现故障时可以从这些
		中间状态继续处理未完成的请求或者退回到原始状态,最终达到一致性。
			以转账为例,我们将用户A向用户B转账分为4个部分:第一阶段,用户A装备转账’第二个节点,从用户A账户扣减余额;第三个阶段,用户B增加余额;
		第四个阶段,完成转账。系统需要记录操作过程中的每个步骤的状态,一旦系统出现故障,系统便能够自动发现没有完成的任务,然后根据任务所处的状态
		继续执行任务。
			在实际应用中,上面过程通常是通过持久化执行任务的状态和环节信息,一旦出现问题,则定时任务会捞取未执行完的任务,继续执行未执行完的任务,
		直到执行完成,或者取消已经完成的这部分操作并回到原始状态。这种方法在任务完成的每个阶段时,都需要更新数据库中的任务状态,这在大规模,高并发
		系统中不会有太好的性能,一种更好的办法是用 Write-Ahead Log(写前日志),这和数据库的binlog(操作日志)相似,在进行每个步骤时,都先写入
		日志,如果操作遇到问题而停止,则可以继续读取日志并按照步骤进行恢复,继续执行完未完成的工作,最后达到一致性的状态。写前日志可以利用机械
		硬盘的追加写达到较好的性能,然而这是一种专业化的实现方式,多数业务系统还是使用数据库记录的字段来记录任务的执行状态,也就是记录中间的
		"软状态"。一个任务的状态流转一般可以通过数据库的行级锁来实现,这比使用写前日志实现更简单,快速。

		4.对酸碱平衡的总结
			1.使用向上扩展(强悍的硬件)并运行专业的关系型数据库能够保证强一致性,能用向上扩展解决的问题都不是问题
			2.如果向上扩展的成本很高,则可以用相对廉价硬件运行的开源关系型数据库(如mysql),进行水平伸缩和分片,将相关数据分到数据库的同一个
			分片上,仍然能够使用关系型数据库保证事务
			3.如果业务规则限制,无法将相关数据分到同一个分片上,就需要实现最终一致性,在记录事务的软状态(中间状态,临时状态)时若出现不一致,
			则可以通过系统自动化或者人工干预来修复不一致性的问题。

	2.3.2 分布式一致性协议 
		国际开放标准组织Open Group 定义了 DTS(分布式事务处理模型),模型中包含了4个角色:应用程序,事务管理器,资源管理器和通信资源管理器。事务
	管理器是统管全局的管理者,资源管理器和通信资源管理器是事务的参与者。
		JEE(Java企业版)规范也包含了此分布式事务处理模型的规范,并在所有 AppServer 中进行实现。在JEE规范中定义了 TX 协议和 XA协议,TX协议定义
	了应用程序与事务管理器之间的接口,XA协议则定义了事务管理器与资源管理器之间的接口。在企业级开发JEE中,关系型数据库,JMS服务扮演了资源管理器的
	角色,而EJB容器扮演了事务管理器的角色。
		下面介绍了两阶段协议,三阶段协议以及阿里巴巴提出的TCC,它们都是根据DTS这一思想演变而来的。

		1.两阶段提交协议
			JEE 的 XA 协议就是根据两阶段提交来保证事务的完整性,并实现分布式服务化的强一致性。两阶段协议把分布式事务分为两个阶段,一个是
		准备阶段,另外一个是提交阶段。准备阶段和提交阶段都是由事务管理器发起的,下面将事务管理器为协调者,将资源管理器称为参与者。
			
			两阶段协议的流程如下:
			1.准备阶段:协调者向参与者发起指令,参与者评估自己的状态,如果参与者评估指令可以完成,则会写 redo 或者 undo 日志
			(Write-Ahead Log的一种),然后锁定资源,执行操作,但是并不提交。
			2.提交阶段:如果每个参与者明确返回准备成功,也就是预留资源和执行操作都成功,则协调者向参与者发起提交指令,参与者提交资源
			变更的事务,释放锁定的资源;如果任何一个参与者明确返回准备失败,也就是预留的资源或执行操作失败,则协调者向参与者发起终止
			指令,参与者取消已经变更的事务,执行undo日志,缩放锁定的资源。

			我们看到两阶段提交协议在准备阶段锁定资源,这是一个重量级的操作,能够保证强一致性,但是实现起来复杂,成本比较高,不够灵活,更
		重要的是它有如下致命的问题:
			1.阻塞
				从上面描述看,对于任何一次质量都必须收到明确的响应,才会继续进行下一步,否则处于阻塞状态,占用的资源被被一直锁定,不会
			被释放。
			2.单点故障
				如果协调者宕机,参与者没有协调者指挥,则会一直阻塞,尽管可以通过选举新的协调者代替原有的协调者,但是如果协调者在发送一个
			指令后宕机,而提交指令仅仅被一个参与者接收,并且参与者接收后也宕机,则新上任的协调者无法处理这种情况。
			3.脑裂
				协调者发送提交指令,有的参与者接收到了并执行事务,有的参与者没有收到事务就没有执行事务,多个参与者之间是不一致的。

			两阶段协议能够保证系统的强一致性,但是在出现异常的情况,需要人为干预解决。

		2.三阶段提交协议
			三阶段提交协议是两阶段提交协议的改进版本。它通过超时机制解决了阻塞的问题,并且把两个阶段增加为三个阶段。
			1.问询阶段
				协调者询问参与者是否可以完成指令,协调者只需要回答是或者不是,而不需要真正的操作,这个阶段超时会导致终止。
			2.准备阶段
				如果在询问阶段所有参与者都返回可以执行操作,则协调者向参与者发送预执行请求,然后参与者写redo或者undo日志,执行操作但是不提交;
			如果在询问阶段任意参与者返回不能执行操作的结果,则协调者向参与者发送终止请求,这里的逻辑与两阶段提交协议的准备阶段是相似的。
			3.提交阶段
				如果每个参与者在准备阶段返回准备成功,也就是说预留资源和执行操作成功,则协调者向参与者发起提交指令,参与者提交资源变更的事务,
			锁定释放的资源;如果任何参与者返回准备失败,也就是说预留资源或者执行操作失败,则协调者向参与者发起终止指令,参与者取消已经变更的
			事务,执行undo日志,释放锁定的资源,这里的逻辑与两阶段提交协议的提交阶段是一致的。

			三阶段提交协议与两阶段提交协议主要有以下两个不同点:
			1.增加了一个询问阶段,询问阶段可以确保尽可能早的发现无法执行操作而需要终止的行为,但是它并不能发现所有这种行为,只会减少这种情况的
			发送。
			2.在准备节点以后,协调者与参与者执行的任务都增加了超时,一旦超时,则协调者和参与者都会继续提交事务,默认为成功,这也是根据概率统计
			超时后默认为成功的正确性最大。

			三阶段提交协议与两阶段提交协议相比,具有如上优点,但是一旦发生超时,系统仍然会发生不一致,只不过这种情况很少见,好处是至少不会阻塞
		和永远锁定资源。

		3.TCC
			TCC 协议将一个任务拆分成 Try,Confirm,Cancel 三个步骤,正常的流程会先走 Try,如果没有问题,则再执行 Confirm,如果执行过程中
		出了问题,则会执行操作的逆操作Cancel。从正常流程来说,这仍然是一个两阶段提交协议,但是在执行出现问题时有一定的自我修复能力,如果任何
		参与者出了问题,则协调者通过执行操作的逆操作Cancel之前的操作,达到最终一致性状态。
			可以看出,从时序上来说,如果遇到极端情况,则TCC会有很多问题,例如,如果在取消时一些参与者收到指令,而另外一些参与者没有收到指令,
		则整个系统仍然是不一致性的。对于这种复杂情况,系统会首先通过补偿的方式尝试自动修复,如果系统无法修复,则必须由人工参与解决。
			从TCC的逻辑上看,可以说TCC是简化版的三阶段提交协议,解决了两阶段提交协议的阻塞问题,但是没有解决阶段情况下出现不一致和脑裂问题。
		然而,TCC通过自动化补偿手段,将需要人工处理的不一致性情况降到最少,也是一种非常有用的解决方案。
			我们给出一个TCC的实例案例,在秒杀场景中,用户发起下单请求,应用层先查询库存,确认商品库还有余量,则锁定库存,此时订单状态为
		待支付,然后指引用户去支付,由于某种原因用户支付失败或者支付超时,则系统会自动将锁定的库存解锁以供其他用户秒杀。

	2.3.3 保证最终一致性的模式 
		在大规模,高并发服务化系统中,一个功能被拆分成多个具有单一功能的子功能,一个流程会有多个系统的多个单一功能的服务组合实现,如果使用
	两阶段提交协议和三阶段提交协议,则确实能解决系统间的一致性问题。除了这两个协议的自身问题,其实现也比较复杂,成本比较高,最重要的是性能不好。
	相比来看,TCC 协议更简单且容易实现,但是TCC协议由于每个事务都需要 Try, 再执行 Confirm,略显臃肿,因此,现实系统的底线是仅仅需要达到最终
	一致性,而不需要实现专业的,复杂的一致性协议。实现最终一致性有一些非常有效,简单的模式。
		1.查询模式
			任何服务操作都需要提供一个查询接口,用来向外部输出操作执行的状态。服务操作的使用方可以通过查询接口得知服务操作执行的状态,然后根据
		不同的状态做不同的处理操作。为了能够实现查询,每个服务操作都需要有唯一的流水号标识,也可使用此次服务操作对应的资源ID来标识,例如:请求
		流水号,订单号等。
			首先,单笔查询是必须提供的,也是鼓励使用单笔查询的,这是因为每次调用需要占用的负载是可控的。批量查询则根据需要来提供,如果使用了
		批量查询,则需要有合理的分页机制,必须限制分页的大小,以及对批量查询的吞吐量有容量评估,熔断,隔离和限流等措施。

		2.补偿模式
			有了查询模式,在任何状态下,我们都能得知具体的操作所处的状态。为了让系统最终达到一致状态而做的努力都叫做补偿。

			补偿操作根据发起形式分为如下几种:
			1.自动恢复
				程序根据不一致的环境,通过继续进行未完成的操作,或者回滚已经完成的操作,来自动达到一致状态。
			2.通知运营
				如果程序无法自动恢复,并且设计时考虑到了不一致的场景,则可以提供运营功能,通过运营手工进行补偿。
			3.技术运营
				如果没有运营,那么必须通过技术手段来解决,技术手段包括进行数据库变更后者代码变更,这种比较糟糕,我们应该避免。

		3.异步确保模式
			异步确保模式是补充模式的一个典型案例,经常应用到使用方对响应时间要去不高的场景中,通常把这类操作从主流程中摘除,通过异步的方式进行
		处理,然后把结果通过通知系统通知给使用方。这个方案最大的好处是能够对高并发的流量进行消峰。在实践中将要执行的异步操作封装后持久入库,然后
		通过定时捞取未完成的任务进行补偿操作来实现异步确保模式,只要定时系统足够健壮,则任何任务最重都会被执行。

		4.定期校对模式
			在操作主流程中的系统执行校对操作,可以在事后异步的批量校对操作的状态,如果发现不一致操作,则进行补偿,补偿操作与补偿模式中的补偿操作
		是一致的。另外,实现定期校对的一个关键是分布式系统中需要有一个自始至终唯一的ID,生成全局ID有以下两种方法:
			1.持久型
				使用数据库表自增字段或者 Sequence 生成,为了提高效率,每个应用节点可以缓存一批的ID,如果机器重启则可能损失一部分ID,但这并不
			会产生任何问题。
			2.时间型
				一般由机器号,业务号,时间,单节点自增ID组成,由于时间一般精确到秒或者毫秒,因此,不需要持久就能够保证在分布式系统中全局唯一,
			粗略递增等。
				在实践中想在分布式系统中迅速定位问题,可通过分布式系统的调用链追踪系统进行,它能够追踪一个请求的调用链。调用链是从二维的维度跟踪
			一个调用请求,最后形成一个调用树,其原理可以参考google的 Dapper 论文以及一个开源的实现 Pinpoint。
				全局唯一的流水ID可以将一个请求在分布式系统中的流转路径聚合,而调用链中的SpanID可以将聚合的请求路径通过树形结构进行展示,让技术
			支持工作人员轻松的发现系统出现的问题,能够快速定位问题的服务节点,提高应急效率。
				在分布式系统中构建了唯一ID,调用链等基础设施后,我们很容易对系统间的不一致进行核对。通过我们需要构建第三方的定期核对系统,从
			第三方的角度来监控服务执行的健康程度。对案例4和案例5,通常通过定期校对模式发现问题,并通过补偿模式来修复,最后达到系统间的最终
			一致性。定期校对模式多应用于金融系统中。金融系统由于涉及资金安全,需要保证准确性,所以需要多种的一致性保证机制,包括商户交易对账,
			系统间的一致性对账,现金对账,账务对账,手续费对账等,这些都属于定期校对模式。金融系统与社交应用在技术上的本质区别在于:社交应用在于
			量大,而金融系统在于数据的准确性。	

				到现在为止,我们通过查询模式,补偿模式和定期校对模式可以解决案例2~案例5的所有问题:案例2,如果同步超时发生,则需要查询状态进行
			补偿;案例3,如果迟迟没有收到回调响应,则会通过查询状态进行补偿;案例4和案例5,通过定期校对模式可以保证系统间操作的一致性,避免因为
			掉单和状态不一致而导致出现问题。

		5.可靠消息模式
			在分布式系统中,对于主流程中优先级比较低的操作,大多数采用异步的方式执行,也就是前面提到的异步确保模型,为了让异步操作的调用方和被调用
		方充分解耦,也由于专业的消息队列本身具有可伸缩,可分片,可持久化等功能,我们通常通过消息队列来异步化。对于消息队列,我们需要建立特殊的设施
		来保证可靠的消息发送及处理机的幂等性。

			1)消息的可靠发送
				消息的可靠发送可以认为是尽最大努力发送消息通知,有以下2种方法实现:
				1.在消息发送之前将消息持久化到数据库,状态标记为待发送,然后发送消息,如果发送成功,则将消息改为发送成功。定时任务定时从数据库
			捞取一定时间内未发送的消息将消息发送。
				2.该实现与第一种类似,不同的是持久消息的数据库是独立的,并不耦合在业务系统中。消息发送之前,先发送一个预消息给某个第三方的消息
			管理器,消息管理器将其持久到数据库,并标记为待发送,在发送成功后,标记消息为发送成功。定时任务定时从数据库中捞取一定时间内未发送的
			消息,查询业务系统是否要继续发送,根据查询结果来确定消息的状态。

				一些公司把消息的可靠发送实现在了中间件里面,通过Spring的注入,在消息发送时自动持久化消息记录,如果有消息记录没有发送成功,则
			定时补偿发送。

			2)消息处理器的幂等性
				如果我们要保证可靠的发送消息,简单来说就是要保证消息一定发送出去,那么需要有重试机制。有了重试机制后,消息一定会重复发送,那么
			我们需要对重复的问题进行处理。
				保证操作的幂等性的常用方法如下:
					1.使用数据库表的唯一键进行滤重,拒绝重复的请求
					2.使用分布式表对请求进行滤重
					3.使用状态流转的方向性来滤重,通常使用数据库的行级锁来实现
					4.根据业务的特点,操作本身就是幂等的,例如:删除一个资源,增加一个资源,获得一个资源等。

		6.缓存一致性模式
			下面是使用缓存来保证一致性的最佳实践:
				1.如果性能要求不高,则尽量使用分布式缓存,而不要使用本地缓存
				2.写缓存时数据一定要完整,如果缓存数据的一部分有效,另外一部分无效,则宁可在需要时回源数据库,也不要把部分数据放入缓存中
				3.使用缓存牺牲了一致性,为了提高性能,数据库和缓存只需要保持弱一致性,而不需要保持强一致性,否则违背了使用缓存的初衷
				4.读的顺序是先读缓存,后读数据库,写的顺序是先写数据库,后写缓存。

2.4 超时处理模式 
	2.4.1 微服务的交互模式 
		服务之间的交互模式分为如下3种:
			1.同步调用模式
				同步调用适合大规模,高并发的短小操作,而不适用于后端负载较高的场景。
			2.接口异步调用模式
				接口异步调用模式适用于非核心链路上负载较高的处理环节,这个环节经常耗时比较长,并且对实时性要求不高。
			3.消息队列异步处理模式
				消息队列异步处理模式与接口异步调用模式类似,多应用于非核心链路上负载较高的处理环节中,并且服务的上下游不关心下游的处理
			结果,下游也不需要向上游返回处理结果。

	2.4.2 同步与异步的抉择 
		原则:
			1.尽量使用异步来替代同步操作
			2.能用同步解决的问题,不要引入异步
			
			第一条原则是从业务功能的角度出发的,也就是从与用户或者使用方的交互模式出发的,如果业务逻辑允许,用户对产品的交互形态没有异议,
		则我们可以将一些耗时比较长的,用户对响应没有特别要求的操作异步化,以此来减少核心链路的层级,释放系统的压力。
			第二个原则是从技术和架构的角度出发的,这条原则应用的前提是同步能够解决问题,这隐含了一个含义:如果性能不是问题,或者所处理的
		操作是短小的轻量级处理逻辑,那么同步调用方式是最理想不过的,因为这样不需要引入异步化的复杂处理流程。

	2.4.3 交互模式下超时问题的解决方案 
		1.同步调用模式下的解决方案
			在同步调用的模式下,对外的接口会提供服务契约,契约定义了服务的处理结果会通过返回值给使用方,对返回的状态定义分为下面两种:
			1.成功和失败(两状态的同步接口)
				只规定了成功和超时,服务处理结果必须是成功或者失败的,在这种情况下可能发生两种同步调用超时。
				第一种同步调用超时发生在使用方调用此同步接口的过程中。针对这个问题,我们要服务的使用方使用上面提到的查询模式,异步查询
			处理结果,在获得明确的处理结果后,得知处理结果是成功还是失败,然后做相应的处理。这里有个问题,如果查询模式返回的是未知的请求,
			那么在这种情况下使用方超时,服务1实际上没有接收到一开始的处理请求,服务使用方需要使用同一个请求ID进行重试,服务1也必须是请求
			处理的幂等性。

				第二种同步调用超时发生在内部服务1调用服务2的过程中。
				在使用方调用服务1,且服务1接收到请求后,同步调用服务2,由于通信出了问题,所以服务1得到超时的结果,这时服务1该怎么做呢?
			是重试,取消还是快速失败?因为契约只有成功或者失败,也就是对于使用方来说,不允许有中间状态,对于这种服务内部超时的场景,必须使用
			快速失败的策略:针对这个超时错误,服务快速返回失败,同时在内部调用服务2的冲正接口,服务2的冲正接口可以判断之前是否接收到请求,
			如果接收到了请求并做了处理,则应该做反向的回滚操作。如果服务2之前没有接收到请求,则忽略冲正请求,依次来实现服务的幂等性。

			2.成功,失败和处理中(三状态的同步接口)
				对于上面定义的第二种定义,服务契约中规定了三种处理结果,为:成功,失败和处理中,对于超时等系统错误的请求,其实可以认为是处理中
			状态的一个特例,在这种场景的应用里,超时被视为内部暂时的问题,随后可能被修复,因此,可能在一定的时间窗口内告知使用方在处理中,随后
			修复问题并尝试执行,达到最大化请求处理成功的目标,不至于让使用方重试,以提升用户体验。

				服务处理结果可能是成功失败,也可能是处理中,在这种情况下可能发生两种同步调用超时。
				第一种同步调用超时发生在使用方调用此同步接口的过程中。这种场景和两状态同步调用的超时接口场景类似,使用方调用服务1的接口,由于
			网络等原因获得超时的结果,这时使用方应该将超时看做一个处理中的特例,使用服务1的查询接口后续补齐上一个请求的处理状态,可参照两状态
			同步调用的接口超时场景的方案。

				第二种同步超时发生在内服务1调用服务2的过程中。在使用方调用服务1,且服务1收到请求后,同步调用服务2,由于通信出了问题,所以服务1
			得到超时的结果,这时服务1该怎么做?
				这和两状态同步调用的内部超时场景不一样,两状态设计由于与使用方约定了契约,不是成功就是失败,所以必须在同步调用时给与一个明确的
			结果,然后,在三状态同步调用的内部超时场景下,可能返回给使用方一个中间状态,也就是处理中的结果,变相的把同步接口变成异步接口,达到
			最终一致性。
				在这种场景下,我们倾向于给用户更好的体验,尽最大努力成功处理用户发来的请求,因此,针对在服务1调用服务2超时时,我们会返回给用户
			处理中的状态,随后系统尽最大努力补偿执行出错的部分,服务1需要通过服务2的查询接口得到最新的请求处理状态,如果服务2没有明确回复,则可以
			尝试重新发送请求,然后,这需要服务2也实现了操作的幂等性。

		2.异步调用模式下的解决方案
			在异步调用下,对外的接口也会提供服务契约,契约定义了服务的受理结果会通过返回值返回使用方,返回的状态通常有2个:受理和未受理。和三
		状态同步调用接口不同的是,异步调用模式还有异步处理返回结果的通知,状态包括处理成功和处理失败。

			不同阶段的网络通信产生的超时和处理方案如下:
			1.异步调用接口超时
				异步调用接口超时发生在使用方调用服务1的受理接口时,同两状态同步接口调用超时及三状态同步调用接口超时的场景是一样的,需要通过查询
			来补齐状态,并根据状态来判断后续的操作,具体的解决方案参照上面。

			2.异步调用内部超时
				异步调用内部超时发生在服务1受理了使用方的请求后,服务1在处理请求时,在调用服务2的过程超时了,这和三状态同步调用内部超时的场景
			相似,由于异步调用模式使用的是受理模式,所以一旦受理,我们应该尽最大努力将用户请求的操作处理成功,因此,在服务1调用服务2超时的场景
			下,服务1需要根据服务2的查询接口获得最新的状态,根据状态补偿后续的操作,这和三状态同步调用内部超时的解决方案一致,不同的是此场景下
			一旦处理成功,则需要异步回调通知使用方,而在三状态同步调用内部超时的场景下,只需要等待使用方查询,不需要通知,也无法通知。

			3.异步调用回调超时
				回调超时的问题在生产中经常出现,通常发生于这样的场景下:服务1受理后成功的调用了依赖服务2,获得明确的处理结果,但是在将处理结果
			通知使用方时出现超时。由于使用方有可能是公司内部的也可能是外部的,网络环境复杂,发生超时的概率很大,因此,大多数公司都会开发一个
			通知子系统,来专门处理回调通知。
				由于服务1通过回调通知使用方,所以服务1需要保证通知一定可送达,如果遇到超时,则服务1负责重新继续补偿,通常会设计一个通知时间按
			一定间隔递增的策略,例如:指数回退,直到通知成功为止,通知是否成功以对方回写状态为准。

		3.消息队列异步处理模式的解决方案
			消息队列异步处理模式多用于松疏松耦合的项目,这些项目通常是主流程中无法处理耗时的任务,恰好耗时的任务又不是核心的流程的一部分。
		这类交互使用消息队列进行解耦,电商交易系统成功处理交易后,需要发送消息到消息队列服务器,后续的流程由物流平台处理,也不需要将处理结果
		反馈给交易平台。使用消息队列解耦后,处理流程被分为两个阶段:生产者投递和消费者处理,在不同阶段会产生不同的超时问题。

			1.消息对垒的生产者超时
				这种场景参照2.3.4节。

			2.消息队列的消费者超时
				对于消息队列的处理机与消息队列之间的超时后者网络问题,通常可以通过消息队列提供的机制来解决。一般消息队列会提供如下两种
			方案来消费信息:
				1.自动增长消费的偏移量
					在一个消费者从消息服务器中取走消息后,消息队列的消息偏移量自动增加,即消息一旦被从消息队列中取走,则不再存在于服务器中,
				假如消息处理机对消息处理失败,则也无法从消息服务器中找回。
				2.手工提交消费的偏移量
					在一个消费者从消息服务器中取走消息后,处理机先把消息持久到本地数据中,然后告诉消息服务器已经消费消息,消息服务器才会移除
				消息,如果在没有告诉消息服务器已经消费消息之前,持久失败或者发生了其他问题,则消息仍然存在于消息服务器中,消息处理器下次还可以
				继续消费消息。
				
				如果允许丢消息,则我们使用第一种方式处理,这种方式的并发量高,性能好,但是如果我们对消息处理的准确性要求比较高,则必须采用
			第二种方式。

	2.4.4 超时补偿的原则 
		对于上面的场景,我们都需要对服务间同步超时造成的后果进行处理,而处理方法有快速失败和内部补偿两种,补偿方式也有调用方补偿和接收方补偿
	两种,具体使用哪种?
		服务间调用超时补偿的原则:
		1.服务1调用服务2,如果服务2响应服务1并且告诉服务1消息已接收,那么服务1的任务就结束了;如果服务2处理失败,那么服务2应该负责重试或者
		补偿。这种情况下,服务2通常接收消息后先持久再告诉服务1成功,随后服务2才开始处理持久的消息,避免服务进程被杀掉而导致消息丢失。
		2.服务1调用服务2,如果服务2没有明确的接收响应,例如网络超时,那么服务1应该持续进行重试,直到服务2明确表示已经接收消息。在这种情况下
		容易出现重复的消息,因此在服务2中通常需要保证滤重或者幂等性。

		那么什么是明确的响应呢?如果是底层的网络通信,则必须拿到对方返回的报文,从报文中找到状态位,状态位是成功的才算是明确的响应。如果是
	一次api调用,则必须拿到明确的返回值,并检查返回值中的关键状态,发生超时等异常都属于没有明确的响应。

2.5 迁移开关的设计 
	在迁移过程中必须使用开关,开关一般都会基于多种维度设计,例如:全局的,用户的,角色的,商户的,产品的,等等。如果在迁移过程中遇到问题,
则我们需要关闭开关,迁回老系统,这需要我们的新系统兼容老系统的数据,老系统也兼容新系统的数据。
	有的开关设计在应用层次,通过一个curl语句调用,没有权限控制,这样的开关在服务池的每个节点中都有可能是不一致的;还有的系统将开关配置在中心化
的配置系统,数据库或者缓存中,处理的每个请求都通过统一的开关来判断是否迁移等,这样的开关有一个致命的缺点:在服务请求的过程中,开关可能会变,各个
节点之间的开关可能不同步,不一致,导致重复的请求可能既走到新逻辑又走了老逻辑,如果新逻辑和老逻辑没有保证幂等,则这个请求就被重复处理了。
	这里推荐使用订单开关,不管我们在什么维度上设计了开关,在接收到服务请求后,在请求创建的关联实体(例如:订单)上标记开关,对于以后的处理流程,
包括同步和异步的处理流程,都通过订单的开关来判断,而不是通过全局的或者基于配置的开关来判断,这样在创建订单时,开关已经确定,不再变更,若一份
数据不再发生变化,那么它永远是并发安全的,且不会有不一致的问题。	

 

 

 

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值