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

一. 业务背景

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

二.实现思路

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

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

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

   
漏斗算法

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

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

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

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



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

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

   
滑动窗口限流  

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

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

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

 三.需求分析

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

四.代码实现

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
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

posted @   浅草青晨  阅读(138)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· winform 绘制太阳,地球,月球 运作规律
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 上周热点回顾(3.3-3.9)
· 超详细:普通电脑也行Windows部署deepseek R1训练数据并当服务器共享给他人
点击右上角即可分享
微信分享提示