限流算法

推荐链接:

面试必备:四种经典限流算法讲解

5种限流算法,7种限流方式,挡住突发流量?

固定窗口计数器

固定窗口计数器(Fixed Window Counter)是一种简单的限流算法。它将时间分成固定长度的窗口(如1秒或1分钟),在每个窗口中统计请求数。当某个窗口内的请求计数超过限流阀值时,该窗口剩余时间内的请求都被拒绝;窗口结束后计数清零,重新开始统计。该算法实现非常简单,仅需一个支持原子加减的计数器即可,但存在明显的窗口边界效应。例如,如果限制1秒内最多5个请求,1秒钟末尾来了5个请求,下一个窗口开始瞬间又来了5个请求,总共10个请求瞬间涌入就会突破限流阀值。

  • 优点:实现简单、高效,开销低。适用于流量比较平稳、对延迟容忍度高的场景,如控制接口的粗粒度访问频率。
  • 缺点:存在“临界点”问题(窗口边界效应),无法平滑处理突发的临界流量。在极端突发情况下,可能短时间内超过系统承载。由于只按固定周期限制,实时性不够精细。

滑动窗口计数器

滑动窗口计数器(Sliding Window Counter)通过将固定窗口进一步细分来平滑限流。它将整个统计周期(如1秒)划分为多个更小的时间片(如每0.2秒一个),在每个小时间片中单独计数。每次请求到来时,累加当前时间所在小片的计数;然后将所有在整个滑动窗口范围内的小片计数相加,与阈值比较。随着时间流逝,窗口以微小步长向前滑动,自动丢弃过期片段中的请求。这样可以一定程度上解决固定窗口在边界时出现的“大脉冲”问题。

  • 优点:精度较高,可根据划分粒度调节限流效果。实现相对简单,易于理解和扩展。相比固定窗口,能部分缓解边界突增问题。
  • 缺点:依然不擅长处理突发高并发流量;一旦达到阈值,超出的请求会被直接拒绝。实现复杂度和内存开销略高于固定窗口(需维护多个小窗口计数器)。适用于流量变化一般、需要比固定窗口更平滑限流的场景,但若业务有频繁突发的请求浪涌,可能仍不足够灵活。

滑动日志算法

滑动日志算法(Sliding Log)是一种精确度更高的限流方法。它不使用固定窗口,而是为每个请求都记录时间戳,并将所有请求按时间排序保存。每次新请求到来时,只需清理日志中超出时间范围的旧记录,然后统计当前时间窗口内(例如过去1秒)的请求总数,与阈值比较。如果超过限制,则拒绝该请求。这种方法没有固定窗口边界的问题,限流非常精准和公平,不会出现窗口交界处被意外放行或拒绝的现象。但缺点是内存开销大:需要保存大量请求时间戳记录,随着请求量增长资源消耗显著。

  • 优点:高精度、实时性好,不会出现固定窗口的临界效应,使得限流更加平滑和公平。能够精确控制单位时间内的请求数,无突发漏洞。
  • 缺点:需要记录所有请求时间,内存消耗大。实现复杂度较高,通常需要借助哈希表、排序数据结构等来高效维护时间戳。适用于对准确度要求极高、且流量规模相对可控的场景,如关键业务的精细限流。对于海量突发流量场景,因为记录每次请求会消耗大量资源,可能不够实用。

漏桶算法

漏桶算法(Leaky Bucket)将请求看作“水滴”注入一个固定容量的桶中,桶底有一个以恒定速率漏水的小孔。每个新请求到来时都加入桶中;如果桶满了(超出容量),多余的请求就被丢弃。而桶会持续以恒定的速率处理(漏出)请求,即使瞬间来了大量请求,出桶速度保持不变,保证系统稳定处理。该算法本质上把不规则的输入流平滑为固定输出流,对防止突发洪峰非常有效。

  • 优点:能够平滑请求流量,避免瞬间高并发导致系统崩溃;输出速率可控,通过调整漏水速度和桶容量灵活适应不同需求。适用于对输出速度要求恒定的场景,如后台任务消费、生产者-消费者模型,保证系统按固定速率服务请求。
  • 缺点:不能应对突发的流量骤增;即使系统空闲,桶也只以固定速率“漏水”,突发请求只能排队等候。实现上需要维护一个队列或计数器来缓存请求,会占用额外内存。对流量波动大的场景,需要仔细调参,否则要么延迟过大要么丢弃过多请求。

令牌桶算法

令牌桶算法(Token Bucket)维护一个“令牌桶”,桶有固定容量,并以固定速率向其中添加令牌。当请求到来时,如果桶中有令牌,则取出一个令牌放行该请求,否则拒绝。桶中的令牌最多累积到满;在处理空闲时刻,积累的令牌可用于应对突发流量。与漏桶不同,令牌桶允许一定量的突发流量,因为空闲时累计的令牌可以在瞬时释放,突发请求只要桶中有令牌就能继续处理。

  • 优点:能够平滑控制请求速率,同时允许突发流量。当流量突然增多时,可以利用桶中剩余的令牌快速响应,而不是像漏桶那样一律排队等待。算法稳定性高,可通过调整生成令牌速率和桶容量适应不同需求;如Guava的RateLimiter即基于该算法。
  • 缺点:实现复杂度比固定窗口等高,需要维护令牌的生成和消耗逻辑。如果短时间内有大量请求而积累的令牌用完,新请求仍会被拒绝,需要对参数(速率、容量)精心设置。对时间精度要求高,如果时钟不同步或者延迟误差,可能影响限流效果。

Redis 分布式限流

分布式系统中,为了让多个实例共享限流状态,常采用Redis作为集中式存储,并借助原子操作保证多个实例间的一致性。通常做法是:每个实例对同一资源发送限流请求时,都访问同一个Redis键。例如可以使用固定窗口策略——为每个时间窗口创建一个Redis键(如apiKey:当前分钟),调用 INCR 将计数+1,并在键初次创建时设置过期时间;当计数超过阈值时拒绝请求。Redis的单线程特性和Lua脚本支持使得这种操作可以原子执行,避免了并发冲突。例如,使用MULTI/EXEC或Lua脚本将INCREXPIRE打包成单次操作,就可以保证即使同时多个实例发起请求,也不会出现数据竞争。此外,对于滑动窗口和滑动日志限流,也可以利用Redis的有序集合(ZSET)存储各个请求的时间戳,并在每次请求时原子地新增和删除过期成员,从而实现精确的滑动计数。

  • 一致性保障:关键在于将限流逻辑交由Redis单点原子执行。借助Lua脚本或事务,可以让INCRHINCRBY等更新操作变成原子操作,从而确保并发情况下计数正确。只要所有服务实例都指向同一个Redis key,就能做到全局限流。
  • 挑战与方案:Redis自身单点可能成为瓶颈,可通过Redis集群或分片方案分担负载。但使用Redis Cluster时需注意:如果一个Lua脚本要操作多个键,这些键必须在同一槽位(Hash Tag)上,否则会报错pandaychen.github.io。因此,往往会通过设计让所有限流相关的键使用相同的Hash Tag,或直接使用单节点Redis(依赖外部HA)。另一个挑战是可用性:如果Redis宕机或网络抖动,可能导致短暂限流规则失效。对此可以考虑启用持久化(AOF)、哨兵自动故障转移等保证Redis可靠性。总的来说,Redis分布式限流的核心思路是共享限流状态,让所有实例“看到”同一套计数数据,从而在集群内统一应用限流决策。

算法对比

算法 是否支持突发流量 平滑程度 实现复杂度 资源(内存/计算) 典型场景
固定窗口计数器 较低(有边界效应) 简单的QPS限制,对准确性要求不高
滑动窗口计数器 较高(可细化) 需要比固定窗口更平滑的流量控制
滑动日志算法 很高(精确) 高(存储时间戳) 追求精确限流、请求量适中时
漏桶算法 否(限速稳定) 很高(输出恒定) 限制输出速率,防止请求尖峰
令牌桶算法 高(允许突发) 低/中 需要平滑输出且允许突发场景
Redis分布式 视具体算法而定 视具体算法而定 较高 中/高 分布式环境下全局限流

总结

固定窗口和滑动窗口算法简单、开销低,适用于快速实现限流策略;其中滑动窗口能稍微缓解窗口边界的突发效应,但对极端突发流量仍不够友好。

滑动日志算法在准确性和公平性上优势最大,但需要记录每次请求时间,资源消耗最高。

漏桶和令牌桶算法都能有效平滑输出流量,但侧重点不同:漏桶以恒定速率处理请求(不支持突发),适合输出受限的场景;令牌桶允许短时突发,但算法实现稍复杂,可根据空闲时段积累令牌平滑应对流量峰值。

分布式限流(如基于Redis的方案)则将上述任意算法扩展到多实例环境,关键在于通过Redis的原子操作来共享计数状态并保证一致性。

总体而言,选用哪种限流算法需综合考虑系统对突发流量的容忍度、实现成本和资源消耗:对极端突发容忍度高的场景,可优先考虑令牌桶;对精确性要求极高的,可采用滑动日志;而对于大部分场景,固定窗口或滑动窗口配合简单的分布式计数即可满足需求。