限流算法详解:从计数器到令牌桶的实践与对比
在高并发场景中,限流是保护系统稳定的关键手段,通过限制单位时间内的请求量,避免服务因过载而崩溃。常见的限流方案包括计数器算法、漏桶算法和令牌桶算法,每种算法适用于不同的业务场景,各有优劣。
计数器算法:简单直观的流量控制
计数器算法是最基础的限流方式,通过统计单位时间内的请求数,超过阈值则拒绝新请求。分为固定窗口和滑动窗口两种实现。
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 网关、高并发接口 |
选择建议
- 优先令牌桶:大多数业务场景(尤其是互联网服务)推荐使用,平衡了灵活性和可控性;
- 漏桶备选:后端处理能力固定且不接受突发时选用;
- 计数器兜底:简单场景或快速验证时使用,需注意窗口问题。
实践工具与实现
- 令牌桶:Nginx 的
limit_req模块(基于令牌桶)、Guava 的RateLimiter; - 计数器:Redis 的
INCR+EXPIRE、Java 的AtomicInteger; - 漏桶:可通过队列 + 定时任务实现(如 Java 的
DelayQueue)。
总结
限流算法的核心目标是保护系统不被过载,选择时需结合业务场景的流量特征(是否有突发、是否允许等待)和系统的处理能力。令牌桶算法因兼顾灵活性和可控性,成为工业界的首选;滑动窗口和漏桶适用于特定场景;固定窗口则适合简单需求。在实际应用中,还需结合监控和动态调整阈值,确保限流策略既能保护系统,又不影响用户体验