经典案例:高并发分布式限流器 (Rate Limiter)¶
限流器(Rate Limiter)是微服务架构与开放平台(如 Twitter/GitHub API、微信支付网关)中极其核心的系统自我保护设施。它通过对请求速率进行主动限制,防止恶意攻击、瞬时突发流量以及雪崩效应击垮脆弱的后端服务。
1. 为什么需要限流器?¶
graph LR
Spike["瞬时突发流量 / 爬虫刷单 / 恶意 DoS"] --> Gate["API 网关限流器"]
Gate -- "超出阈值 (拦截降级)" --> R429["返回 HTTP 429 Too Many Requests"]
Gate -- "合规流量 (平滑放行)" --> Services["核心业务集群 & 数据库 (平稳运行)"]
- 防范拒绝服务(DoS)与爬虫滥用:防止自动化爬虫或恶意调用耗尽系统线程与网络连接;
- 保护脆弱依赖与下游资源:防止底层数据库或第三方外部接口(如短信发送、银行网关)因瞬间高并发请求超载而崩溃;
- 成本控制与付费阶梯:按调用量计费的 SaaS 服务(如 OpenAI、Stripe)根据用户付费等级提供不同的调用速率上限;
- 流量整形(Traffic Shaping):将不可控的脉冲式突发流量平滑为均匀请求流。
2. 五大主流限流算法深度剖析¶
2.1 固定窗口计数器 (Fixed Window Counter)¶
- 原理:将时间划分为固定的时间窗口(例如每 1 分钟)。为每个窗口维护一个计数器,请求到达时计数器 \(+1\);若计数器超过阈值则拒绝请求;窗口结束时计数器清零。
- 致命缺陷:临界窗口突发(Boundary Spike Problem):
窗口 1 (00:00 - 01:00) 窗口 2 (01:00 - 02:00)
+---------------------------+ +---------------------------+
| | | |
| 最后 1 秒来 | | 前 1 秒来 |
| 100 个请求 | | 100 个请求 |
+---------------------------+ +---------------------------+
[ 00:59 ] [ 01:01 ]
<------- 2 秒内涌入 200 个请求! ------->
若限制为“每分钟最多 100 次”,攻击者可以在 00:59 发送 100 次,在 01:00 又发送 100 次。在短短 2 秒内系统承受了 200 次请求,实际速率达到额定值的 2 倍!
2.2 滑动窗口日志 (Sliding Window Log)¶
- 原理:记录每个请求到达的绝对时间戳(通常使用 Redis Sorted Set
ZSet存储,分值为时间戳)。 - 当新请求到达时,清理所有时间戳在
[now - window, now]之外的过期记录(ZREMRANGEBYSCORE); - 统计当前窗口内剩余的元素总数(
ZCARD); - 若数量小于限制阈值,将当前时间戳写入并放行,否则拒绝。
- 优点:精度极高,彻底杜绝了窗口边界突发问题。
- 缺点:内存开销巨大!如果一个接口限制为每小时 10 万次,每次请求哪怕只存一个 8 字节时间戳,高频调用下也会迅速耗尽内存。
2.3 滑动窗口计数器 (Sliding Window Counter)¶
固定窗口与滑动日志的折中改良方案:
前一个窗口 (权重 30%) 当前窗口 (权重 70%)
[====================] [====================]
100 次请求 30 次请求
^ 当前时间点 (当前窗口已走过 70%)
- 计算公式:
在上图中,当前窗口过去 \(70\%\),前一个窗口占 \(30\%\):
- 优点:内存极度节省(只需存两个数字),计算平滑,有效压制边界突发。
2.4 令牌桶算法 (Token Bucket,工业界首选)¶
令牌桶算法是 Google Guava RateLimiter、Spring Cloud Gateway 与大多数云厂商 API 网关的核心基础。
固定速率放入令牌 (Rate r = 10 个/秒)
|
v
+--------------+
| O O O O |
| 令牌桶 | <-- 最大容量 Capacity = 20
| O O O O |
+--------------+
| (请求到达时取走 1 个令牌)
v
[ 处理业务请求 ]
- 运行机制:
- 系统以恒定速率 \(r\) 向容量为 \(C\) 的桶中投放令牌;
- 桶满时多余的令牌溢出丢弃;
- 每个请求到达时,必须从桶中成功获取 1 个令牌才能被处理;若桶空无可用令牌,则请求被拒绝或排队等待。
- 核心优势:允许应对突发流量(Burst Traffic)! 若系统闲置了一段时间,桶中积攒了满额的 \(C\) 个令牌。此时突发涌入 \(C\) 个并发请求,能够瞬间全部放行,随后以恒定速率 \(r\) 平稳流转。
2.5 漏桶算法 (Leaky Bucket)¶
- 原理:请求像水流一样注入一个固定容量的漏桶中;桶底开有一个孔,以绝对恒定的恒定速率漏出水滴(处理请求)。若注入速度过快导致水溢出,多余的请求被直接丢弃。
- 与令牌桶的关键差异:
- 令牌桶:允许突发流量瞬间放行,后续平滑;
- 漏桶:强制平滑输出速率,绝不允许任何突发请求击中后端,输出速率永远恒定。
2.6 五大算法选型全景对照¶
| 算法 | 突发流量支持 | 内存消耗 | 实现复杂度 | 精度 | 典型适用场景 |
|---|---|---|---|---|---|
| 固定窗口 | 差(存在 2 倍突发漏洞) | 极低(单一计数器) | 极简单 | 低 | 粗粒度防护、低频统计(如每月短信上限) |
| 滑动日志 | 极佳(无任何边界突发) | 极高(存储所有请求时间戳) | 中等 | 极高 | 严格精度、高安全风控、低并发场景 |
| 滑动计数器 | 良好(平滑过渡) | 极低(仅存前后两窗口值) | 中等 | 高 | 高并发 API 网关通用限流 |
| 令牌桶 (Token Bucket) | 极佳(支持可控的瞬时爆发) | 极低(记录当前令牌数与上次时间) | 中等 | 高 | 绝大多数互联网高并发业务(首选) |
| 漏桶 (Leaky Bucket) | 差(完全禁止任何突发) | 低(固定队列容量) | 较高(需队列维持) | 高 | 保护极脆弱的第三方系统(如恒定速率发邮件) |
3. 分布式限流实现:Redis + Lua 脚本原子执行¶
在分布式集群环境中,单机内存限流(如 Guava RateLimiter)无法跨多台 Web 服务器统计全局配额,必须依赖中心化存储(Redis 集群)。
3.1 为什么必须使用 Lua 脚本?¶
限流检查涉及多个连续操作: 1. 读取当前剩余令牌数; 2. 根据时间差计算新补充的令牌; 3. 判断令牌是否充足并执行扣减; 4. 写回最新状态。
若使用普通 Redis 命令,并发下存在严重的竞态条件(Race Conditions)。利用 Redis 单线程执行 Lua 脚本的特性,能够保证上述逻辑全局原子性(Atomic)执行,且网络往返仅需 1 次 RTT!
3.2 生产级 Redis Lua 令牌桶脚本¶
-- KEYS[1]: 限流器 Key (例如 "rate_limit:user_10086")
-- ARGV[1]: 桶最大容量 Capacity
-- ARGV[2]: 令牌补充速率 Rate (每秒产生多少令牌)
-- ARGV[3]: 当前时间戳 (单位: 秒)
-- ARGV[4]: 本次请求消耗的令牌数 (通常为 1)
local key = KEYS[1]
local capacity = tonumber(ARGV[1])
local rate = tonumber(ARGV[2])
local now = tonumber(ARGV[3])
local requested = tonumber(ARGV[4])
-- 获取当前桶中令牌数及上次刷新时间
local info = redis.call("HMGET", key, "tokens", "last_updated")
local tokens = tonumber(info[1])
local last_updated = tonumber(info[2])
if tokens == nil then
-- 首次访问,初始化满桶
tokens = capacity
last_updated = now
else
-- 计算两次请求的时间差,补充新令牌
local delta = math.max(0, now - last_updated)
local generated = delta * rate
tokens = math.min(capacity, tokens + generated)
last_updated = now
end
-- 判断令牌是否充足
if tokens >= requested then
tokens = tokens - requested
redis.call("HMSET", key, "tokens", tokens, "last_updated", last_updated)
-- 设置 Key 过期时间,防止冷数据永久霸占内存
redis.call("EXPIRE", key, math.ceil(capacity / rate) * 2)
return 1 -- 放行
else
-- 令牌不足,拒绝
return 0
end
4. HTTP 协议标准与系统集成¶
当限流器决定拒绝请求时,应该如何与客户端优雅交互?
4.1 标准 HTTP 响应格式¶
- HTTP 状态码:
429 Too Many Requests - 标准 HTTP 响应头(Headers):
HTTP/1.1 429 Too Many Requests
Content-Type: application/json
Retry-After: 5
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1672531200
{
"code": 429,
"message": "API rate limit exceeded. Please retry in 5 seconds."
}
X-RateLimit-Limit:当前周期内允许的最大请求总数;X-RateLimit-Remaining:当前周期内剩余可用配额;X-RateLimit-Reset:当前限流周期重置的 Unix 时间戳;Retry-After:客户端应该等待多少秒后才允许发起重试。
4.2 容灾设计:Fail-Open vs Fail-Closed¶
当 Redis 集群故障或限流中间件自身不可用时,系统该如何决策? - Fail-Closed(默认拒绝):全部阻断请求。适合高安全性金融系统; - Fail-Open(默认放行,推荐):记录告警日志并自动放行所有请求,降级为直通模式。在绝大多数互联网业务中,宁可冒着后端短暂受压的风险,也不能让限流器自身故障导致全站瘫痪。