0%

限流

限流算法详解:从计数器到令牌桶的实践与对比

在高并发场景中,限流是保护系统稳定的关键手段,通过限制单位时间内的请求量,避免服务因过载而崩溃。常见的限流方案包括计数器算法漏桶算法令牌桶算法,每种算法适用于不同的业务场景,各有优劣。

计数器算法:简单直观的流量控制

计数器算法是最基础的限流方式,通过统计单位时间内的请求数,超过阈值则拒绝新请求。分为固定窗口滑动窗口两种实现。

1. 固定窗口计数器(Fixed Window)

原理
  • 将时间划分为固定长度的 “窗口”(如 1 秒),维护一个计数器;
  • 每个请求进入时,计数器加 1;若计数器超过阈值(如 100 次 / 秒),则拒绝请求;
  • 窗口结束时,计数器重置为 0。
示例
  • 阈值:100 次 / 秒,窗口大小 1 秒;
  • 第 0-1 秒内,若请求达 100 次,后续请求被拒绝;
  • 第 1 秒结束,计数器重置,第 1-2 秒重新计数。
优缺点
  • 优点:实现简单(如用 Redis 的INCR+EXPIRE即可);
  • 缺点:存在 “窗口边界” 问题,可能出现突发流量。例如:
    • 第 0.9 秒内收到 99 次请求,第 1.1 秒内又收到 99 次请求;
    • 两个窗口均未超限,但 200ms 内总请求达 198 次,可能压垮服务。
适用场景
  • 对流量精度要求不高的场景(如非核心业务接口);
  • 需快速实现限流的简单系统。

2. 滑动窗口计数器(Sliding Window)

原理
  • 将固定窗口进一步划分为多个 “子窗口”(如 1 秒划分为 10 个 100ms 的子窗口);
  • 每个子窗口维护独立计数器,请求进入时更新对应子窗口的计数;
  • 计算 “滑动窗口” 内的总请求数(即最近 N 个子窗口的计数之和),若超过阈值则拒绝请求;
  • 随着时间推移,窗口以子窗口为单位 “滑动”,丢弃最旧的子窗口数据。
示例
  • 阈值:100 次 / 秒,窗口大小 1 秒,划分为 10 个 100ms 子窗口;
  • 第 0-100ms:30 次请求(子窗口 1 计数 30);
  • 第 100-200ms:20 次请求(子窗口 2 计数 20);
  • 总请求数 = 30+20+…(累计最近 10 个子窗口),超过 100 则限流。
优缺点
  • 优点:缓解固定窗口的边界问题,流量控制更平滑;
  • 缺点:子窗口划分越多,计算复杂度越高(需维护多个计数器)。
适用场景
  • 对流量稳定性有一定要求的场景(如 API 网关限流);
  • 可接受轻微性能损耗换取更高精度的场景。

漏桶算法(Leaky Bucket)

漏桶算法通过 “缓存请求 + 固定速率处理” 的方式,强制限制流量的输出速率,平滑突发流量。

原理

  • 想象一个固定容量的 “漏桶”,请求进入时先放入桶中;
  • 桶底有一个 “小孔”,以固定速率(如 10 次 / 秒)向外 “漏出” 请求进行处理;
  • 若桶满,新请求直接被拒绝;若桶空,则停止漏出。

核心特性

  • 输入:请求速率可快可慢(允许突发);
  • 输出:速率固定(由漏桶的 “漏出速率” 决定)。

示例

  • 漏桶容量:100 个请求,漏出速率:10 次 / 秒;
  • 突发 150 次请求:100 个进入桶中,50 个被拒绝;
  • 之后每秒处理 10 个,10 秒后桶空。

优缺点

  • 优点:严格控制输出速率,避免后端服务被突发流量冲击;
  • 缺点:无法应对 “良性突发流量”(如短时高峰但后端能处理)。

适用场景

  • 后端服务处理能力固定,需严格限制输入速率的场景(如数据库访问);
  • 流量波动大,但需确保处理平稳的场景(如日志收集)。

令牌桶算法(Token Bucket)

令牌桶算法是工业界最常用的限流方案,通过 “发放令牌” 的方式控制请求速率,既能限制平均速率,又能应对突发流量。

原理

  • 维护一个固定容量的 “令牌桶”,系统按固定速率(如 100 个 / 秒)向桶中放入令牌;
  • 桶满时,多余令牌被丢弃;
  • 请求进入时,需从桶中获取 1 个令牌:
    • 若获取成功,继续处理请求;
    • 若令牌不足,请求被限流(等待或拒绝)。

核心特性

  • 令牌生成:匀速生成,确保长期平均速率可控;
  • 突发处理:桶中积累的令牌可应对短时突发请求(只要令牌充足)。

示例

  • 令牌桶容量:100 个,生成速率:50 个 / 秒;
  • 系统空闲时,桶中积累 100 个令牌;
  • 突发 150 次请求:消耗 100 个令牌,剩余 50 个请求因无令牌被限流;
  • 之后每秒生成 50 个令牌,逐步处理剩余请求。

优缺点

  • 优点:兼顾平均速率控制和突发流量处理,灵活性高;
  • 缺点:实现较复杂(需维护令牌生成和桶状态)。

适用场景

  • 大多数高并发场景(如 API 网关、秒杀系统);
  • 允许短时突发但需控制长期速率的场景(如用户登录、商品查询)。

三种算法的对比与选择

算法 核心思想 优点 缺点 典型应用场景
固定窗口 单位时间计数 实现简单 窗口边界问题 简单限流、非核心接口
滑动窗口 细分窗口平滑计数 缓解边界问题 复杂度较高 API 网关、中等精度需求
漏桶 固定速率输出 严格控制输出速率 不支持良性突发 数据库访问、日志收集
令牌桶 令牌控制访问 支持突发,灵活性高 实现复杂 秒杀系统、API 网关、高并发接口

选择建议

  1. 优先令牌桶:大多数业务场景(尤其是互联网服务)推荐使用,平衡了灵活性和可控性;
  2. 漏桶备选:后端处理能力固定且不接受突发时选用;
  3. 计数器兜底:简单场景或快速验证时使用,需注意窗口问题。

实践工具与实现

  • 令牌桶:Nginx 的limit_req模块(基于令牌桶)、Guava 的RateLimiter
  • 计数器:Redis 的INCR+EXPIRE、Java 的AtomicInteger
  • 漏桶:可通过队列 + 定时任务实现(如 Java 的DelayQueue)。

总结

限流算法的核心目标是保护系统不被过载,选择时需结合业务场景的流量特征(是否有突发、是否允许等待)和系统的处理能力。令牌桶算法因兼顾灵活性和可控性,成为工业界的首选;滑动窗口和漏桶适用于特定场景;固定窗口则适合简单需求。在实际应用中,还需结合监控和动态调整阈值,确保限流策略既能保护系统,又不影响用户体验

欢迎关注我的其它发布渠道