服务端限流算法

一、单机限流

一、固定窗口算法

固定窗口算法通过在单位时间内维护一个计数器,能够限制在每个固定的时间段内请求通过的次数,以达到限流的效果

算法实现起来也比较简单,可以通过构造方法中的参数指定时间窗口大小以及允许通过的请求数量,当请求进入时先比较当前时间是否超过窗口上边界,未越界且未超过计数器上限则可以放行请求。
优点:实现简单。
缺点:无法应对突发流量的情况。比如每秒允许放行100个请求,即允许的qps是100。但是在0秒到0.1秒间可能就进入了100个请求,0.1秒之后进来的请求都会被拒绝执行

 

二、滑动时间窗口算法

滑动窗口算法在固定窗口的基础上,进行了一定的升级改造。它的算法的核心在于将时间窗口进行了更精细的分片,将固定窗口分为多个小块,每次仅滑动一小块的时间。并且在每个时间段内都维护了单独的计数器,每次滑动时,都减去前一个时间块内的请求数量,并再添加一个新的时间块到末尾,当时间窗口内所有小时间块的计数器之和超过了请求阈值时,就会触发限流操作

优点:实现简单,对比固定窗口算法,滑动窗口对流量的控制更加精细化
缺点:
1) 对比固定窗口算法,滑动窗口算法也需要更多的存储空间,用来维护每一块时间内的单独计数。
2) 流量较大时,容易在一个时间片的开始就用完了限速额,直到窗口向前滑动一格才再次有额度,导致实际处理的流量不均匀

三、漏桶算法(Leaky Bucket)

漏桶算法的思想:

  • 漏桶有一定的容量P,水以不变的速率V-in流入漏桶中,漏桶以恒定的V-out速率漏出水

  • 当V-in > V-out,水就会在桶中堆积

  • 堆积的水超过同的容量P时,就溢出丢弃

  • 当V-in < V-ou时,会消耗桶中储存的水

优点:漏桶可以起到整流的效果,因为漏出速率恒定,短时高流量会在桶内堆积,低流量会消耗桶内累计的请求,对下游(有限的,取决于桶容量)屏蔽这种波动。

缺点:不管当前系统的负载压力如何,所有请求都得进行排队,即使此时服务器的负载处于相对空闲的状态,这样会造成系统资源的浪费。由于漏桶的缺陷比较明显,所以在实际业务场景中,使用的比较少

四、令牌桶算法(Token Bucket)

 

令牌桶算法的核心思想是,请求需要拿到令牌才能被处理,通过控制令牌的产生速率,就能控制实际服务的qps。假设令牌的生成速度是每秒100个,并且第一秒内只使用了70个令牌,那么在第二秒可用的令牌数量就变成了130,在允许的请求范围上限内,扩大了请求的速率。当然,这里要设置桶容量的上限,避免超出系统能够承载的最大请求数量。

  • 令牌桶有固定容量,并且以恒定速率rate产生令牌,即每1/rate秒产出一个令牌,例如限流1000qps,则每1/1000秒产出一个令牌

  • 新产生的令牌会放入令牌桶,桶满则丢弃

  • 请求需要从桶中取走一个令牌才能被处理,若桶空则被限流

漏桶算法和令牌桶算法的异同:

  • 流量 <= 处理速度: 两者速度相同,漏桶保持空,令牌桶保持满

  • 流量 远大于 处理速度:两者速度相同,漏桶保持满,令牌桶保持空

  • 流量 <= 处理速度,偶尔有突发流量时:

    • 漏桶的漏出速率是固定的,即使下游处理能力有富余,整个系统的处理速度也是固定的。

    • 令牌桶的可以在流量低峰期储存富余的令牌(<=桶容量),可以消耗桶里的令牌来处理掉一定的突发流量

测试一下,qps限制为5的ratelimiter:

public void acquireTest() {
RateLimiter rateLimiter = RateLimiter.create(5);
for (int i = 0; i < 10; i++) {
double time = rateLimiter.acquire();
log.info("等待时间:{}s",time);
}
}

运行结果:

可以看到,每200ms左右产生一个令牌并放行请求,也就是1秒放行5个请求,使用RateLimiter能够很好的实现单机的限流。

ratelimiter怎么解决突发流量:使用预消费的机制,申请令牌的数量不同,不会影响申请的响应时间,例如acquire(1)acquire(1000)这两个请求会消耗同样的时间返回结果,但是会影响下一个请求的响应时间。一个消耗大量令牌的任务到达空闲的RateLimiter,会被立即批准执行,但是当下一个请求进来时,将会额外等待一段时间,用来支付前一个请求的时间成本。

RateLimiter和Throttling的区别:


 

Ratelimiter

Throttling

实现

Ratelimiter基于令牌桶的思想,令牌产出速率是固定,决定了qps限制也是精确的(令牌桶的缓存允许部分突发流量)

Throttling基于并发度计数器,类似漏桶的思想,但漏出速度不是固定的,所以能通过的qps也不是固定的

目标和适用场景

Ratelimiter的目标主要为保护下游资源(如db读写),避免发出过高请求使受保护逻辑过载。

适用于一些异步消费场景,如consumer、binlogResolver

Throttling目标只要为保护自身,防止下游故障向影响自身及向上扩散。

常用场景:同步的rpc调用场景

优劣

  • 能放行多少qps是固定的,初始值很好配置

  • 无自适应能力

  • qps不固定,取决于下游服务性能。因此初始值很难准确计算,基本靠压测

  • 负反馈机制,有一定自适应能力,下游rpc延迟上涨,则限流更多请求

  • 因为依赖反馈,像发kafka这种异步调用不适用

 五、计数器算法

对于每次服务调用,可以通过AtomicLong#incrementAndGet()方法来给计数器加1并返回最新值,通过这个最新值和阈值进行比较,使用AtomicLong可以实现请求并发量的控制

二、集群限流

可以用redis实现,执行redis的incr即可

三、单机实现频控和分布式频控

1、单机并发限制

public class LocalThrottling {

private final AtomicInteger concurrency = new AtomicInteger();
private int threshold;

public LocalThrottling(int threshold) {
this.threshold = threshold;
}

public boolean tryEnter() {
if (concurrency.incrementAndGet() > threshold) {
return false;
}
return true;
}

public int leave() {
return concurrency.decrementAndGet();
}

}

2、单机频控

public class LocalFrequencyControl {

private static Cache<String, AtomicInteger> cache;
private static Cache<String, Long> blackIpCache;
  
  // expireMillisSec是频控的时间区间,例如3分钟限制5次请求,expireMillisSec就是三分钟对应的时间
public LocalFrequencyControl(int maxSize, long expireMilliSec, long blackIpMilliSec) {
cache = CacheBuilder.newBuilder() //
.expireAfterWrite(expireMilliSec, TimeUnit.MILLISECONDS) //
.build();

blackIpCache = CacheBuilder.newBuilder()
.expireAfterWrite(blackIpMilliSec, TimeUnit.MILLISECONDS)
.build();
}

public boolean incAndCheck(String k, int quota) {
int res = inc(k, 1);
if (res > quota) {
// 限流后的处理,加黑名单
Long firstTimeMillis = blackIpCache.getIfPresent(k);
if (firstTimeMillis == null) {
synchronized (blackIpCache) {
Long nowCache = blackIpCache.getIfPresent(k);
if (nowCache == null) {
blackIpCache.put(k, System.currentTimeMillis());
}
}
}
return false;
}
return true;
}

public Integer inc(String k, int v) {
AtomicInteger inCache = cache.getIfPresent(k);
if (null == inCache) {
synchronized (cache) {
AtomicInteger nowCache = cache.getIfPresent(k);
if (null == nowCache) {
cache.put(k, new AtomicInteger(v));
return v;
} else {
return nowCache.addAndGet(v);
}
}
}
return inCache.addAndGet(v);
}


public Integer inc(String k) {
return inc(k, 1);
}

public void delete(String k) {
cache.invalidate(k);
}

public void clear() {
cache.cleanUp();
}

}

3、分布式频控

public class RedisFrequencyControl {

private final JedisCommands freCmd = null;
private final JedisCommands blackListCmd = null;

private int quota;
private long blackIpSec;

public RedisFrequencyControl(int quota, long blackIpSec) {
this.quota = quota;
this.blackIpSec = blackIpSec;
}

public boolean incrAndCheck(String k, int v, int expire) {
Long res = freCmd.incrBy(k, v);
if (res == null) {
return true;
}
if (res >= quota) {
// 限流后加黑名单
Long res2 = blackListCmd.incrBy(k, 1);
if (res2 == 1) {
blackListCmd.expire(k, expire);
}
blackListCmd.expire(k, blackIpSec);
return false;
}
if (res == v && expire > 0) {
// 第一次设置的时候,加入过期时间
freCmd.expire(k, expire);
}
return true;
}

四、Nginx限流实现

Nginx限流主要是两种方式,即限制单位时间内的请求数和限制并发连接数。
限制单位时间内的请求数:用的是漏桶算法,可以限制访问的qps。漏桶算法不能处理突发流量,Nginx可以通过设置burst关键字开启对突发请求的缓存处理,而不是直接拒绝

 

 

posted @   MarkLeeBYR  阅读(133)  评论(0编辑  收藏  举报
编辑推荐:
· 开发者必知的日志记录最佳实践
· SQL Server 2025 AI相关能力初探
· Linux系列:如何用 C#调用 C方法造成内存泄露
· AI与.NET技术实操系列(二):开始使用ML.NET
· 记一次.NET内存居高不下排查解决与启示
阅读排行:
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY
· 【自荐】一款简洁、开源的在线白板工具 Drawnix
点击右上角即可分享
微信分享提示