限流算法#
一句话答案#
五种限流算法:计数器、固定窗口、滑动窗口、漏桶(匀速)、令牌桶(允许突发,最灵活),Sentinel 用滑动窗口。
核心要点
| 算法 | 特点 |
|---|---|
| 计数器 | 简单但有边界突发 |
| 固定窗口 | 临界点翻倍问题 |
| 滑动窗口 | 解决临界问题 |
| 漏桶 | 匀速流出 |
| 令牌桶 | 允许突发(Guava RateLimiter) |
生产: Sentinel(滑动窗口) / Guava RateLimiter(令牌桶) / Nginx(漏桶)
令牌桶 vs 漏桶:实现机制#
令牌桶——惰性补令牌 + 桶内积攒: 核心是不需要后台线程定时放令牌,而是请求到来时按时间差「惰性」补:
newTokens = (now - lastRefillTime) × rate // 这段时间本该生成的令牌数
tokens = min(capacity, tokens + newTokens) // 补进桶,但不超过容量
lastRefillTime = now
if (tokens >= n) { tokens -= n; 放行 } else { 拒绝/排队 }plaintext- 为什么允许突发: 空闲时段令牌在桶里积攒到
capacity,一旦来一批请求可以瞬间消耗掉这批存量令牌,故能容忍capacity大小的突发。 - 匀速近似: 突发过后桶被掏空,后续只能按
rate补令牌的速度放行,长期平均速率仍是rate。
漏桶——固定速率出队,恒速整流: 请求先入队(桶),由固定速率的「出水口」匀速取出处理:
- 出队速率恒为
rate,与流入速率无关,故输出绝对平滑、不容突发;桶满则丢弃(或拒绝)新请求。 - 代价:突发流量被强行削平后排队,增加延迟;空闲时也不会「攒额度」,无法利用历史空闲。
一句话区别: 令牌桶限的是平均速率且放过突发(桶里有票就走),漏桶限的是瞬时输出速率(出水口卡死恒定)。
滑动窗口如何消除临界突刺#
固定窗口的临界问题: 窗口 [0:00, 1:00) 限 100 QPS,攻击者在 0:59 打满 100 个、在 1:01 又打满 100 个——这 2 秒内实际通过了 200 个,达到限额的 2 倍,因为固定窗口在整点「硬重置」计数,跨窗口的流量不互相计入。
滑动窗口做法: 把 1 个大窗口切成 N 个小格(如 60 个 1 秒格),每格独立计数;统计「当前时刻往前一个完整窗口」覆盖的所有格子之和,随时间逐格向前滑动。
- 格子越多,统计粒度越细、临界误差越小(N 格时误差上界约
1/N),代价是内存与计算量增加。 - Sentinel 的
LeapArray就是这种环形数组实现,每个桶(WindowWrap)记一段时间的统计值,过期桶被复用覆盖。
各算法实现要点#
计数器/固定窗口: 一个原子计数器 + 窗口起始时间戳;请求到来 count++,超阈值拒绝;到下一窗口整点把 count 清零。最简单,但有临界突刺。
令牌桶: 见上,存 tokens 和 lastRefillTime 两个值,惰性补令牌。
漏桶: 维护一个队列 + 定速消费线程(或记录「上次漏水时间」按时间差算可漏出量),队列满则拒绝。
滑动窗口(Redis + Lua 的 ZSET 实现): 用一个 ZSET 存请求记录,score = 请求时间戳,member = 唯一请求 id,整段逻辑放 Lua 脚本保证原子:
-- KEYS[1]=限流key ARGV: now, window, limit, reqId
redis.call('ZREMRANGEBYSCORE', KEYS[1], 0, now - window) -- 滑掉窗口外的过期记录
local cnt = redis.call('ZCARD', KEYS[1]) -- 统计窗口内请求数
if cnt < limit then
redis.call('ZADD', KEYS[1], now, reqId) -- 记录本次请求
redis.call('PEXPIRE', KEYS[1], window) -- 兜底过期,防 key 泄漏
return 1 -- 放行
end
return 0 -- 拒绝lua- 关键点:
ZREMRANGEBYSCROE把[0, now-window]的旧记录删掉,等价于「窗口向前滑动」;Lua 单线程执行保证「统计 + 判断 + 写入」三步原子,避免并发超发。 - 缺点:每个请求都在 ZSET 里存一条,高 QPS 下内存占用大;可改用「分桶计数 Hash」版滑动窗口降低存储。
面试回答(2分钟版)
限流算法主要有四种,从简单到复杂依次来说。固定窗口计数器按时间窗口统计请求数,超过阈值就拒绝,实现简单但有临界点翻倍问题——两个窗口交界处瞬间可能通过两倍的流量。滑动窗口把时间窗口细分为多个小格子,每次滑动一个格子来统计,解决了临界突刺问题,Sentinel 就用的滑动窗口。漏桶算法请求以任意速率流入但以固定速率流出,像一个漏水的桶,能严格控制输出速率,Nginx 的 limit_req 就是漏桶实现,但缺点是不允许突发流量。令牌桶算法以固定速率向桶里放令牌,请求到来时必须拿到令牌才能通过,桶里可以积攒令牌所以允许一定程度的突发流量,是最灵活的方案,Guava 的 RateLimiter 就是令牌桶实现。生产中单机限流用 Sentinel 或 Guava RateLimiter,分布式限流用 Redis + Lua 脚本实现滑动窗口。
追问与易错
追问方向:
- “令牌桶和漏桶区别?”→ 漏桶以固定速率流出(严格匀速,不允许突发),令牌桶以固定速率放令牌但桶可积攒(允许一定突发流量),令牌桶更灵活适用场景更广
- “Sentinel 用哪种算法?”→ Sentinel 用滑动窗口统计(LeapArray),支持 QPS 限流和并发线程数限流,滑动窗口解决了固定窗口的临界突刺问题
- “分布式限流怎么做?”→ Redis + Lua 脚本实现滑动窗口或令牌桶(原子操作保证并发安全)、Sentinel 集群限流模式通过 Token Server 统一分配配额
易错点:
- ❌ 固定窗口没问题——有临界点翻倍问题
- ❌ 漏桶最好——不允许突发某些场景不适合