分布式里面的补偿机制

分布式对外高可用,对内如何让憋出的内伤消化消化

一:补偿机制的意义§
举例一个常见场景:
客户端->购物车微服务->订单微服务->支付微服务

为什么要考虑补偿机制呢?
因为一次跨机器的请求通信可能会通过DNS,网卡,交换机,路由机,负载均衡等设备,这些设备都不是一直稳定的,在数据传输的过程中只要一个问题出错,就会有问题的产生.

在分布式里面,一次完整的业务流程是由多次跨机器的通信构成,那么产生问题的概率就会成倍的增加.

但是这个并不表示真正的系统无法处理请求,所以我们应该尽可能的消化这些异常.

另:补偿,事务补偿或者叫做重试,他们之间的关系是什么?

不需要太纠结这些名字,因为目的都是一样的,消化掉某个操作产生的异常,通过内部机制将这些异常产生的不一致消化掉.

不管通过什么方式,只要通过额外的方式解决问题,都可以理解成补偿的操作.所以事务补偿和重试都是补偿的子集.前者都是逆向操作,后者是一个正向操作…

只是从结果来看,两者的意义不同,事务的补偿意味着放弃

在这里插入图片描述

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

在这里插入图片描述

因为[补偿]已经是一个额外的流程呢,既然能够走额外的流程,说明时效性并不是第一考虑的因素,所以做补偿的核心要点是:宁可慢,不可错.

因此,不要草率的就确定呢补偿的实施方案,需要谨慎的评估,虽然说错误没有办法100%避免,但是要抱着这样的少发生错误的心态.

二:补偿应该怎么做?§
主流的补偿方式就是前文提到的两种:回滚(事务的补偿),和重试

[回滚]
回滚分为两种形式:

  • 显示回滚(逆向调用接口)
  • 隐式回滚(无需逆向调用接口)

最常见的显示回滚就是做两件事:

首先是确定失败的步骤和状态,从而确定回滚范围.一个业务流程,往往实在设计之初就制定好了,所以确定回滚的范围比较容易,但这唯一需要注意的一点是:如果在一个业务处理中涉及到的服务并不是都提供了「回滚接口」,那么在编排服务时应该把提供「回滚接口」的服务放在前面,这样当后面的工作服务错误时还有机会「回滚」。

简单的来说就是让回滚接口有被调用到的机会.最优的选择就是放在第一个.

其次就是要能提供回滚操作使用到的业务数据.回滚时提供的数据越多,越有利于程序的健壮性,因为程序可以在收到回滚操作的时候做业务检查,比如检查账户是否相等,金额是否一致.

在这个中间态的数据结构和数据大小并不确定.所以最好是将相关数据序列化成一个JSON,然后存到nosql里面.

[隐式回滚]相对来说使用的场景比较少.它意味着这个回滚的动作不需要我们额外的处理,下游服务内部有类似"预占"并且"超时失效"的机制.

例如:
在电商的场景里面.会将订单中的商品预占库存,等待用户在多少分钟内支付.如果没有支付,就释放库存.

[重试]§
这个操作最大的好处就是不需要提供额外的接口[逆向接口].这对于代码的维护和长期开发的成本有优势,而且业务是变化的.逆向接口也需要变化.所以更多时候可以考虑重试.

不过相对与回滚来说,重试使用的场景要少一些.

  • 下游系统返回请求超时,被限流中等临时状态的时候,我们就可以考虑重试了.
  • 而如果是返回余额不足,无权限的明确业务错误,就不需要重试.
  • 一些中间件或者RPC框架,返回503,404这种没有预期恢复时间的错误,也不需要重试了.

为了进行重试,我们还需要指定一个重试的策略,主流的充实策略主要是以下几种:

立即重试:有时候故障是暂时性的,可能因为网络数据包冲突或者硬件组件高峰流量等事件造成的,在这种情况下,适合立即重试的操作.不过立即重试的操作不应该超过一次,如果立即重试失败,应该改用其他策略.
固定间隔:这个很好理解,策略1和策略2多用于前端系统的交互操作中.

增量间隔:这个也很好理解.

return (retryCount - 1) * incrementInterval;

这个的主要使用目的是让重试失败的优先级往后面排.让新的重试入队.

指数间隔:和增量没有什么大区别,不过就是增长的幅度大一些.

全抖动:在递增的基础上,增加随机性,适用于某一时刻产生的大量请求进行压力分散的场景.

return random(0 , 2 ^ retryCount);

等抖动:在指数间隔和全抖动之间寻找一个中庸的方案,降低随机性的使用.

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

3、4、5、6策略的表现情况大致是这样。(x轴为重试次数)

在这里插入图片描述

为什么说重试有坑呢?§
正如前面说到的那样.出于对开发成本考虑,如果重试对接口的调用,就需要考虑到幂等性的问题.

幂等性是一个数学概念,后引申为程序概念.

这就意味着可能多次调用,如果没有保证幂等性的话,就会产生错误的操作.

所以,一旦某个功能支持重试,那么整个链路上的解耦都需要考虑幂等性的问题,不能因为多次调用而导致业务数据的变化.

满足幂等性的实现思路就是将其过滤掉:
1.给每一个请求定一个唯一的标识.
2.在进行重试的时候,判断整个请求是否已经执行过,或者正在执行.如果是,就抛弃请求.

第一点:可以使用全局ID生成器或者生成服务.或者粗暴一些,使用Guid,或者UUID.给每一个请求赋值

第二点:
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中做。在生产端赋值全局唯一标识,在消费端通过唯一标识消重。

重试的最佳实践§
重试特别石化在高负载的情况下被降级.当然也应当收到限流和熔断机制的影响.当重试和限流熔断一起搭配使用才是最佳的.

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

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

一定要给「重试」制定一个终止策略。当回滚的过程很困难或代价很大的情况下,可以接受很长的间隔及大量的重试次数,DDD中经常被提到的「saga」模式其实也是这样的思路。不过,前提是不会因为保留或锁定稀缺资源而阻止其他操作(比如1、2、3、4、5几个串行操作。由于2一直没处理完成导致3、4、5没法继续进行)。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值