分布式系统关注点——99%的人都能看懂的「补偿」以及最佳实践

若是这是第二次看到个人文章,欢迎 文末扫码订阅我哟~ 👉
本文长度为 4229字,建议阅读 11分钟。

这是本系列中既「数据一致性」后的第二章节——「高可用」的完结篇。html

前面几篇中z哥跟你聊了聊作「高可用」的意义,以及如何作「负载均衡」和「高可用三剑客」(熔断、限流、降级,文末会附上前文链接:))。此次,咱们来聊一聊在保证对外高可用的同时,憋出的“内伤”该如何经过「补偿」机制来自行消化。前端

1、「补偿」机制的意义?

以电商的购物场景为例:程序员

客户端 ---->购物车微服务 ---->订单微服务 ----> 支付微服务。

这种调用链很是广泛。算法

那么为何须要考虑补偿机制呢?sql

正如以前几篇文章所说,一次跨机器的通讯可能会通过DNS 服务,网卡、交换机、路由器、负载均衡等设备,这些设备都不必定是一直稳定的,在数据传输的整个过程当中,只要任意一个环节出错,都会致使问题的产生。json

而在分布式场景中,一个完整的业务又是由屡次跨机器通讯组成的,因此产生问题的几率成倍数增长。微信

可是,这些问题并不彻底表明真正的系统没法处理请求,因此咱们应当尽量的自动消化掉这些异常。网络

可能你会问,以前也看到过「补偿」和「事务补偿」或者「重试」,它们之间的关系是什么?数据结构

你其实能够不用太纠结这些名字,从目的来讲都是同样的。就是一旦某个操做发生了异常,如何经过内部机制将这个异常产生的「不一致」状态消除掉架构

题外话:在Z哥看来,无论用什么方式,只要经过额外的方式解决了问题均可以理解为是「补偿」,因此「事务补偿」和「重试」都是「补偿」的子集。前者是一个逆向操做,然后者则是一个正向操做。

只是从结果来看,二者的意义不一样。「事务补偿」意味着“放弃”,当前操做必然会失败。

图片描述
▲事务补偿

「重试」则还有处理成功的机会。这两种方式分别适用于不一样的场景。

图片描述
▲重试

由于「补偿」已是一个额外流程了,既然可以走这个额外流程,说明时效性并非第一考虑的因素,因此作补偿的核心要点是:宁肯慢,不可错

所以,不要草率的就肯定了补偿的实施方案,须要谨慎的评估。虽然说错误没法100%避免,可是抱着这样的一个心态或多或少能够减小一些错误的发生。

2、「补偿」该怎么作?

作「补偿」的主流方式就前面提到的「事务补偿」和「重试」,如下会被称做「回滚」和「重试」。

咱们先来聊聊「回滚」。相比「重试」,它逻辑上更简单一些。

「回滚」

Z哥将回滚分为2种模式,一种叫「显式回滚」(调用逆向接口),一种叫「隐式回滚」(无需调用逆向接口)。

最多见的就是「显式回滚」。这个方案无非就是作2个事情:

首先要肯定失败的步骤和状态,从而肯定须要回滚的范围。一个业务的流程,每每在设计之初就制定好了,因此肯定回滚的范围比较容易。但这里惟一须要注意的一点就是:若是在一个业务处理中涉及到的服务并非都提供了「回滚接口」,那么在编排服务时应该把提供「回滚接口」的服务放在前面,这样当后面的工做服务错误时还有机会「回滚」

其次要能提供「回滚」操做使用到的业务数据。「回滚」时提供的数据越多,越有益于程序的健壮性。由于程序能够在收到「回滚」操做的时候能够作业务的检查,好比检查帐户是否相等,金额是否一致等等。

因为这个中间状态的数据结构和数据大小并不固定,因此Z哥建议你在实现这点的时候能够将相关的数据序列化成一个json,而后存放到一个nosql类型的存储中。

「隐式回滚」相对来讲运用场景比较少。它意味着这个回滚动做你不须要进行额外处理,下游服务内部有相似“预占”而且“超时失效”的机制的。例如:

电商场景中,会将订单中的商品先预占库存,等待用户在 15 分钟内支付。若是没有收到用户的支付,则释放库存。


下面聊聊能够有不少玩法,也更容易陷入坑里的「重试」。


「重试」

「重试」最大的好处在于,业务系统能够不须要提供「逆向接口」,这是一个对长期开发成本特别大的利好,毕竟业务是每天在变的。因此,在可能的状况下,应该优先考虑使用「重试」

不过,相比「回滚」来讲「重试」的适用场景更少一些,因此咱们第一步首先要判断,当前场景是否适合「重试」。好比:

  • 下游系统返回「请求超时」、「被限流中」等临时状态的时候,咱们能够考虑重试
  • 而若是是返回“余额不足”、“无权限”等明确没法继续的业务性错误的时候就不须要重试
  • 一些中间件或者rpc框架中返回Http50三、404等没有什么时候恢复的预期的时候,也不须要重试

若是肯定要进行「重试」,咱们还须要选定一个合适的「重试策略」。主流的「重试策略」主要是如下几种。

策略1.当即重试。有时故障是候暂时性,多是因网络数据包冲突或硬件组件流量高峰等事件形成的。在此状况下,适合当即重试操做。不过,当即重试次数不该超过一次,若是当即重试失败,应改用其它的策略。

策略2.固定间隔。应用程序每次尝试的间隔时间相同。 这个好理解,例如,固定每 3 秒重试操做。(如下全部示例代码中的具体的数字仅供参考。)

策略1和策略2多用于前端系统的交互式操做中。

策略3.增量间隔。每一次的重试间隔时间增量递增。好比,第一次0秒、第二次3秒、第三次6秒,九、十二、15这样。

return (retryCount - 1) * incrementInterval;

使得失败次数越多的重试请求优先级排到越后面,给新进入的重试请求让道。

策略4.指数间隔。每一次的重试间隔呈指数级增长。和增量间隔“异曲同工”,都是想让失败次数越多的重试请求优先级排到越后面,只不过这个方案的增加幅度更大一些。

return 2 ^ retryCount;

策略5.全抖动。在递增的基础上,增长随机性(能够把其中的指数增加部分替换成增量增加。)。适用于将某一时刻集中产生的大量重试请求进行压力分散的场景。

return random(0 , 2 ^ retryCount);

策略6.等抖动。在「指数间隔」和「全抖动」之间寻求一个中庸的方案,下降随机性的做用。适用场景和「全抖动」同样。

var baseNum = 2 ^ retryCount;
return baseNum + random(0 , baseNum);

三、四、五、6策略的表现状况大体是这样。(x轴为重试次数)
图片描述


为何说「重试」有坑呢?

正如前面聊到的那样,出于对开发成本考虑,你在作「重试」的时候多是复用的常规调用的接口。那么此时就不得不提一个「幂等性」问题。

若是实现「重试」选用的技术方案不能100%确保不会重复发起重试,那么「幂等性」问题是一个必需要考虑的问题。哪怕技术方案能够确保100%不会重复发起重试,出于对意外状况的考量,尽可能也考虑一下「幂等性」问题。

幂等性:无论对程序发起几回重复调用,程序表现的状态(全部相关的数据变化)与调用一次的结果是一致的话,就是保证了幂等性。

这意味着能够根据须要重复或重试操做,而不会致使意外的影响。对于非幂等操做,算法可能必须跟踪操做是否已经执行。

因此,一旦某个功能支持「重试」,那么整个链路上的接口都须要考虑幂等性问题,不能由于服务的屡次调用而致使业务数据的累计增长或减小。

知足「幂等性」其实就是须要想办法识别重复的请求,而且将其过滤掉。思路就是:

  1. 给每一个请求定义一个惟一标识。
  2. 在进行「重试」的时候判断这个请求是否已经被执行或者正在被执行,若是是则抛弃该请求。

第1点,咱们可使用一个全局惟一id生成器或者生成服务(能够扩展阅读,分布式系统中的必备良药 —— 全局惟一单据号生成)。 或者简单粗暴一些,使用官方类库自带的Guid、uuid之类的也行。

而后经过rpc框架在发起调用的客户端中,对每一个请求增长一个惟一标识的字段进行赋值。

第2点,咱们能够在服务端经过Aop的方式切入到实际的处理逻辑代码以前和以后,一块儿配合作验证。
图片描述

大体的代码思路以下。

【方法执行前】

if(isExistLog(requestId)){  //1.判断请求是否已被接收过。  对应序号3
    var lastResult = getLastResult();  //2.获取用于判断以前的请求是否已经处理完成。  对应序号4
    if(lastResult == null){  
        var result = waitResult();  //挂起等待处理完成
        return result;
    }
    else{
        return lastResult;
    }  
}
else{
    log(requestId);  //3.记录该请求已接收
}

//do something..


【方法执行后】
logResult(requestId, result);  //4.将结果也更新一下。

若是「补偿」这个工做是经过MQ来进行的话,这事就能够直接在对接MQ所封装的SDK中作。在生产端赋值全局惟一标识,在消费端经过惟一标识消重。

3、「重试」的最佳实践

再聊一些Z哥积累的最佳实践吧(划重点:)),都是针对「重试」的,的确这也是工做中最经常使用的方案。

「重试」特别适合在高负载状况下被「降级」,固然也应当受到「限流」和「熔断」机制的影响。当「重试」的“矛”与「限流」和「熔断」的“盾”搭配使用,效果才是最好。

须要衡量增长补偿机制的投入产出比。一些不是很重要的问题时,应该「快速失败」而不是「重试」。

过分积极的重试策略(例如间隔过短或重试次数过多)会对下游服务形成不利影响,这点必定要注意。

必定要给「重试」制定一个终止策略。

当回滚的过程很困难或代价很大的状况下,能够接受很长的间隔及大量的重试次数,DDD中常常被提到的「saga」模式其实也是这样的思路。不过,前提是不会由于保留或锁定稀缺资源而阻止其余操做(好比一、二、三、四、5几个串行操做。因为2一直没处理完成致使三、四、5无法继续进行)。

4、总结

这篇咱们先聊了下作「补偿」的意义,以及作补偿的2个方式「回滚」和「重试」的实现思路。

而后,提醒你要注意「重试」的时候须要考虑幂等性问题,而且z哥也给出了一个解决思路。

最后,分享了几个z哥总结的针对「重试」的最佳实践。

但愿对你有所帮助。


Question
你以前有哪些时候是经过本身人工来作「补偿」的经历吗?欢迎吐槽~
z哥本身就有屡次熬到半夜才把“意外”形成的混乱清理干净,刻骨铭心啊😂。

相关文章:

做者:Zachary

出处:https://www.cnblogs.com/Zacha...

▶关于做者:张帆(Zachary,我的微信号:Zachary-ZF)。坚持用心打磨每一篇高质量原创。欢迎扫描下方的二维码加入哦~。
按期发表原创内容: 架构设计丨分布式系统丨产品丨运营丨一些思考

若是你是初级程序员,想提高但不知道如何下手。又或者作程序员多年,陷入了一些瓶颈想拓宽一下视野。欢迎关注个人公众号「跨界架构师」,回复「技术」,送你一份我长期收集和整理的思惟导图。
若是你是运营,面对不断变化的市场一筹莫展。又或者想了解主流的运营策略,以丰富本身的“仓库”。欢迎关注个人公众号「跨界架构师」,回复「运营」,送你一份我长期收集和整理的思惟导图。

图片描述

相关文章
相关标签/搜索