什么是限流?常见的限流算法有哪些?
结论:限流(Rate Limiting)是在入口按约定速率放行请求,超出部分拒绝、排队或降级,用来保护系统不被瞬时流量打垮。常见算法五类:固定窗口、滑动窗口、漏桶、令牌桶、并发数限流,工程上再叠加预热与自适应。限流的关键不是算法本身,而是「阈值怎么定、超限怎么处理、在哪一层限」。
一、五种算法对比
| 算法 | 原理 | 突发 | 缺陷 | 实现 |
|---|---|---|---|---|
| 固定窗口 | 每秒计数,超阈值拒绝 | 边界突刺,两窗口交界可放 2 倍 | 精度差 | Redis INCR 加 EXPIRE |
| 滑动窗口 | 按小时间片加权统计 | 平滑 | 内存与计算开销 | Sentinel 滑动窗口 |
| 漏桶 | 恒定速率流出 | 不支持 | 引入排队延迟 | 队列加定时消费 |
| 令牌桶 | 恒定发令牌,允许攒 | 支持 | 桶容量需调参 | Guava RateLimiter |
| 并发数 | 信号量控制同时处理数 | 不适用 | 只控在途,不控 QPS | Semaphore、线程池 |
二、固定窗口的临界问题
阈值 100 QPS,窗口按自然秒切分
0.9s ─────────────► 1.1s
│ 窗口 1 的最后 100ms 放 100 个
│ 窗口 2 的最初 100ms 又放 100 个
▼
200ms 内实际通过 200 个 ⇒ 瞬时 1000 QPS,是阈值的 10 倍
滑动窗口把 1 秒切成 10 个 100ms 的格子,按时间加权统计,能显著削平这种突刺。
三、Redis 滑动窗口实现
-- KEYS[1]=limit:api:order ARGV[1]=now(ms) ARGV[2]=window(ms) ARGV[3]=max
local key = KEYS[1]
local now = tonumber(ARGV[1])
local window = tonumber(ARGV[2])
local max = tonumber(ARGV[3])
redis.call('ZREMRANGEBYSCORE', key, 0, now - window) -- 清掉窗口外的记录
local count = redis.call('ZCARD', key)
if count >= max then
return 0
end
redis.call('ZADD', key, now, now .. ':' .. math.random(1000000))
redis.call('PEXPIRE', key, window)
return 1
ZSET 存请求时间戳,按分数范围清理,天然支持任意窗口长度;代价是每个请求都要写一次 Redis,量大时改用「分桶计数」降低内存。
四、Sentinel 规则
flow:
- resource: orderCreate
grade: 1 # 1=QPS 2=并发线程数
count: 1000
strategy: 0 # 0=直接 1=关联 2=链路
controlBehavior: 0 # 0=快速失败 1=Warm Up 2=排队等待
maxQueueingTimeMs: 500
五、限流要回答的三个问题
- 在哪限:网关做全局限流,应用做接口限流,客户端做防刷;层次越靠前越省资源。
- 限多少:压测得到单机容量,按 70% 到 80% 设定,再按实例数分摊。
- 超了怎么办:返回 429、排队等待、降级兜底,三者按业务容忍度选。
六、常见追问
Q1:超限的流量怎么处理?
快速失败最常见,直接返回 429 并带 Retry-After;排队等待适合削峰但要防队列堆积;降级返回兜底内容适合读接口。
Q2:限流粒度怎么定? 按接口、按用户、按 IP、按租户。粒度越细,存储与统计成本越高;通常用「接口级兜底 + 用户级防刷」两层组合。
Q3:集群限流和单机限流怎么配合? 入口用集群限流控总量,实例内用单机限流兜底,防止某个实例被负载均衡打到过载。
Q4:限流会不会误伤正常用户? 会,尤其是共享 IP 或 NAT 场景。按用户维度限流时要用账号而不是 IP,并给误伤留人工白名单。
小结
- 固定窗口简单但有临界突刺,滑动窗口更平滑,令牌桶容许突发。
- 限流要回答三个问题:在哪限、限多少、超了怎么办。
- 阈值来自压测,不来自感觉。