为什么要限流

系统容量有上限。超过负载,响应变慢甚至崩溃——而且往往是雪崩式的(超时重试进一步增加负载)。

限流是保护手段:拒绝超出处理能力的请求,保证系统在能力范围内稳定运行。

固定窗口计数

做法:把时间切成固定长度的窗口(如 1 分钟),每个窗口内计数,超过阈值就拒绝。

优点:实现简单,内存占用小。

缺点:临界问题。

假设限流是每分钟 100 次。如果用户在第 59 秒发了 100 次,第 61 秒又发了 100 次——这两个窗口各自都没超限,但在"第 59 秒到第 61 秒"这 2 秒内,实际承受了 200 次请求。

滑动窗口计数

做法:把窗口切分成更小的格子(如 1 分钟切成 6 个 10 秒的格子),统计最近 60 秒内的所有格子。

改进:缓解了临界问题,精度取决于格子大小。格子越细越精确,但内存占用越大。

滑动窗口日志:记录每个请求的时间戳,精确统计。精度最高,但内存占用随请求量增长。

漏桶算法(Leaky Bucket)

模型:一个底部有漏水孔的桶。请求像水一样倒入桶中,桶以固定速率漏水(处理请求)。

  • 桶未满:请求入桶,排队等待处理;
  • 桶已满:新请求被丢弃(溢出)。

特点:输出速率恒定,无论输入多快。

适合:需要平滑输出的场景——比如把突发流量整形为均匀的处理速率,保护下游系统。

局限:无法处理合理的突发。即使系统当前很空闲,桶也只能以固定速率处理,突发请求必须排队,可能增加延迟。

令牌桶算法(Token Bucket)

模型:桶里装的是令牌,以固定速率生成令牌,桶满则停止生成。

请求来了要拿一个令牌才能被处理:

  • 有令牌:取走令牌,请求通过;
  • 无令牌:拒绝或排队。

特点:

  • 长期平均速率受令牌生成速率限制;
  • 允许突发——如果桶里有累积的令牌,可以一次性处理一批突发请求(最多消耗桶容量个令牌)。

这比漏桶更符合实际需求:系统通常能承受短暂的突发,只要长期平均速率不超。

应用:网络流量整形、API 限流的主流选择。

分布式限流

单机限流只保护单台机器。如果流量分散到 10 台机器,每台限 100 QPS,整体是 1000 QPS——可能超出后端承受能力。

分布式限流需要一个共享的计数器,通常放在 Redis 中:

  • 用 INCR 配合过期时间实现计数;
  • 用 Lua 脚本保证"读取—判断—自增"的原子性(否则并发下会超发)。

代价:每次请求都要访问 Redis,增加了延迟和一个依赖点。

折中方案:单机限流 + 全局配额分配(每台机器分到总配额的一部分),或者用本地令牌桶 + 定期从中心同步配额。

限流后的响应

被限流的请求应该返回 429 Too Many Requests,并带上 Retry-After 头,告诉客户端多久后重试。

客户端应该实现指数退避重试,而不是立即疯狂重试——否则会加剧系统压力。

配套措施

限流通常与其他手段配合使用:

  • 熔断:下游服务故障时,快速失败而非等待超时;
  • 降级:关闭非核心功能,把资源留给核心流程;
  • 排队:让超出部分的请求排队等待,而不是直接拒绝(适合异步任务)。

说明:本文为系统保护机制的公开知识整理。