计数器
算法思想:其思想是在固定时间窗口内对请求进行计数并与阀值进行比较判断是否需要限流,一旦到了时间临界点则将计数器清零。假设以1分钟作为一个固定窗口,窗口流量限制为100。那么1分钟的最后1s进来100个流量,下一个窗口的第一秒进来了100个流量,缓缓的增加处理和一下子涌入对于程序来说是不一样的,其实1分钟内流量是超出系统能承受范围的。所以,计数器模式无法应对突发的流量。
滑动时间窗口
算法思想1:滑动窗口算法将一个大的时间窗口分成多个小窗口,每次大窗口向后滑动一个小窗口,并保证大的窗口内流量不会超出最大值。是固定窗口的一种改进,但从根本上并没有真正解决固定窗口算法的临界突发流量问题。
算法思想2:(假设设置1分钟限制100请求量)每次请求先判断list长度是否有达到100,达到则继续判断list里面的最后一个元素的时间,如果时间超过60S则放行,最后将当前时间插入列表并截取列表中前100个值。需要记录每个请求到达的时间点,因此对内存的占用会比较多。
String key = "接口名" + "userId";
int listLength = llen(key);
if (listLength < 100) {
lpush(key, 时间戳);
return "放行";
}
Long time = lindex(key, -1);
if (当前时间戳 - time < 60) {
return "限制";
}
lpush(key, 时间戳);
ltrim(key, 0, 99);
return "放行";
漏桶
算法思想:桶的容量是固定的,当有请求到来时先放到木桶中,处理请求的worker以固定的速度从木桶中取出请求,如果木桶已经满了则限流。和消息队列思想有点像,削峰填谷。经过漏桶这么一过滤,请求就能平滑的流出,面对突发请求,服务的处理速度和平时是一样的,这其实不是我们想要的,在面对突发流量我们希望在系统平稳的同时,提升用户体验即能更快的处理请求,而不是和正常流量一样,循规蹈矩的处理。
令牌桶
算法思想:有一个令牌管理员,根据限流大小,定速往令牌桶里放令牌。如果令牌数量满了,超过令牌桶容量的限制,那就丢弃。系统在接收到一个用户请求时,都会先去令牌桶要一个令牌,如果拿到令牌,那么就处理这个请求的业务逻辑,如果拿不到令牌,就直接拒绝这个请求。令牌桶在应对突发流量的时候,桶内假如有 100 个令牌,那么这 100 个令牌可以马上被取走,而不像漏桶那样匀速的消费。所以在应对突发流量的时候令牌桶表现的更佳。
限流结论
- 固定窗口算法实现简单,性能高,但是会有临界突发流量问题,瞬时流量最大可以达到阈值的2倍。
- 为了解决临界突发流量,可以将窗口划分为多个更细粒度的单元,每次窗口向右移动一个单元,于是便有了滑动窗口算法。滑动窗口当流量到达阈值时会瞬间掐断流量,所以导致流量不够平滑。
- 想要达到限流的目的,又不会掐断流量,使得流量更加平滑?可以考虑漏桶算法!需要注意的是,漏桶算法通常配置一个FIFO的队列使用以达到允许限流的作用。由于速率固定,即使在某个时刻下游处理能力过剩,也不能得到很好的利用,这是漏桶算法的一个短板。
- 限流和瞬时流量其实并不矛盾,在大多数场景中,短时间突发流量系统是完全可以接受的,令牌桶算法就是不二之选了,令牌桶以固定的速率v产生令牌放入一个固定容量为n的桶中,当请求到达时尝试从桶中获取令牌。当桶满时,允许最大瞬时流量为n;当桶中没有剩余流量时则限流速率最低,为令牌生成的速率v。