滑动窗口算法实现分布式第三方请求限频

发布时间 2023-04-26 09:17:51作者: 浅草青晨

一. 业务背景

 第三方服务接口存在频率调用限制(例如,1s5次,超过5次返回超出频率),己方服务存在并发处理的情况,为了保证服务的成功率,且达到第三方限制的最大吞吐量,故需要一个限频调用的算法

二.实现思路

常见限频算法一般有五种,漏桶算法、令牌桶算法、固定窗口算法,滑动窗口算法,漏斗算法,五种算各有优劣,详情见下表

  简介 优点 缺点 常见使用场景  
漏桶算法 漏桶算法是水先进入到漏桶里,漏桶再以一定的速率出水,当流入水的数量大于流出水时,多余的水直接溢出。把水换成请求来看,漏桶相当于服务器队列,但请求量大于限流阈值时,多出来的请求就会被拒绝服务。漏桶算法使用队列实现,可以以固定的速率控制流量的访问速度,可以做到流量的平整化处理。  限流的绝对平均化。

 不适合突发请求场景、请求延迟高:当短时间内有大量的突发请求时,即便此时服务器没有任何负载,每个请求也都得在队列中等待一段时间才能被响应。

   
漏斗算法

漏斗有一定的容量,并且以一定速率漏水,漏斗的剩余空间即允许请求的空间。

漏斗算法的模型和漏桶算法在模型上是一致的,容器叫法不同,一个叫漏斗,一个叫漏桶,剩余空间直接决定了请求是否可以通过,只不过在漏斗算法中,一旦通过,请求便可以立即访问;

而漏桶算法中,请求通过后,会被暂存在容器中,等待被匀速处理,两者的差别即在于此。

 预热限流和平滑限流兼备。      
令牌桶算法
 简单来说,对于每一个请求,都需要获取令牌来访问,如果没有获得令牌,则需要触发限流策略。系统会以一个恒定的速度,往固定容量的令牌桶中放入令牌,如果此时有客户端请求进来,则需要先从令牌桶算法中获取令牌,然后才能访问系统。



 预热限流和平滑限流兼备。      
固定窗口限流 固定窗口是限流算法里最简单也是最容易实现的一种算法。比如我们规定,对于A接口来说,我们1分钟的访问次数不能超过100个。那么我们我们可以设置一个计数器counter,其有效时间为1分钟(即每分钟计数器会被重置为0),每当一个请求过来的时候,counter就加1,如果counter的值大于100,就说明请求数过多 简单

窗口切换时无法保证无法很好的控制临界值,常见的请求分布是不均匀的,例如每秒限制100次,前最后一秒的10ms请求100次,后最后一秒的前10ms请求100次,相当于200次/0.02s。

   
滑动窗口限流  

以当前时间为截止时间,往前取一定的时间作为时间窗口,比如:往前取 10s 的时间

当有新的请求进入时,删除时间窗口之外的请求,对时间窗口之内的请求进行计数统计,若未超过限制,则进行放行操作;若超过限制,则拒绝本次服务。

 可以避免固定窗口临界的缺点  时间区间越长,临界精度越高,占用资源越多    

 三.需求分析

 因为第三方的限频模式可能有多种多样,所以此处采用了滑动窗口算法的精确限频

四.代码实现

 这里调整了滑动窗口算法的逻辑,返回了需要等待的时间,保证了请求的成功率,缺点就是消耗资源增加,并且请求可能会延迟很久

 private async Task Sleep(RequestInfo requestInfo)
        {
            if (requestInfo is null || requestInfo.Key.IsNullOrEmpty())
            {
                return;
            }

            TimeSpan? remainingWaitTime;

            do
            {
                remainingWaitTime = await GetRemainingWaitTimeAsync(requestInfo);

                if (remainingWaitTime.HasValue)
                {
                    _logger.LogInformation("延迟时间:" + remainingWaitTime.Value.TotalMilliseconds);
                    await Task.Delay(remainingWaitTime.Value);
                }
                await Task.Delay(TimeSpan.FromMilliseconds(10)); // 做个时间切片,减少循环次数
            } while (remainingWaitTime.HasValue);
        }

        private static async Task<TimeSpan?> GetRemainingWaitTimeAsync(RequestInfo requestInfo)
        {
            var rules = requestInfo.RateLimitRules;
            long timeStamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();

            // 创建一个管道
            var pipe = RedisHelper.StartPipe();

            foreach (var rule in rules)
            {
                var key = "rate_limit:" + requestInfo.Key + ":" + rule.Period.TotalMilliseconds;
                pipe.ZAdd(key, (timeStamp, timeStamp))
                .ZRemRangeByScore(key, 0, timeStamp - (long)rule.Period.TotalMilliseconds)
                .ZCard(key)
                .Expire(key, rule.Period); // 为键设置过期时间
            }
            // 执行管道中的所有命令并获取结果
            var result = pipe.EndPipe();

            // 检查每个限流规则是否超出限制,并计算最长的剩余等待时间
            TimeSpan? longestRemainingWaitTime = null;
            for (int i = 0; i < rules.Count; i++)
            {
                // 上面每次循环执行四条命令  所以次数×4
                int currentCount = Convert.ToInt32(result[4 * i + 2]);
                if (currentCount >= rules[i].Limit)
                {
                    var key = "rate_limit:" + requestInfo.Key + ":" + rules[i].Period.TotalMilliseconds;
                    var scores = await RedisHelper.ZRangeByScoreWithScoresAsync<long>(key, 0, 0);
                    var minScore = scores.Length > 0 ? (long)scores[0].score : timeStamp;
                    var remainingMilliseconds = timeStamp - minScore;
                    var currentRemainingWaitTime = TimeSpan.FromMilliseconds(remainingMilliseconds);
                    if (longestRemainingWaitTime == null || currentRemainingWaitTime > longestRemainingWaitTime)
                    {
                        longestRemainingWaitTime = currentRemainingWaitTime;
                    }
                }
            }

            return longestRemainingWaitTime;
        }

  

 

五.参考文档

https://www.cnblogs.com/duanxz/p/4123068.html

https://www.cnblogs.com/Kingfans/p/16263774.html