一. 业务背景
第三方服务接口存在频率调用限制(例如,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
标签:限频,窗口,请求,算法,限流,漏桶,var,滑动,分布式 From: https://www.cnblogs.com/qcqc/p/17338018.html