限流
限流是保护系统的第一道闸门:主动拒绝过量请求,保护自己不被冲垮。面试必考四算法和分布式限流。
四种算法
令牌桶与滑动窗口
| 算法 | 机制 | 优点 | 缺点 |
|---|---|---|---|
| 固定窗口 | 每窗口计数,超阈值拒绝 | 实现简单(INCR + EXPIRE) | 窗口边界突刺:交界处可双倍流量 |
| 滑动窗口 | 细粒度窗口滚动计数 | 精确,无突刺 | 存储开销(每请求记时间戳) |
| 令牌桶 | 匀速补令牌,有令牌才放 | 允许突发、平滑 | 实现稍复杂(Lua) |
| 漏桶 | 恒定速率流出 | 绝对平滑 | 不允许突发,队列积压 |
面试要点:
- 固定窗口的突刺问题必答(前后窗口交界瞬间可放行 2 倍)
- 令牌桶是生产主流:允许突发符合业务直觉(秒杀前 1 秒允许集中请求)
- 漏桶适合必须匀速的场景(写入下游、定时任务)
分布式限流
单机限流(进程内计数)在多实例下各自为政,总量失控。分布式限流:
| 方案 | 机制 | 特点 |
|---|---|---|
| Redis + Lua | 计数存 Redis,Lua 原子操作 | 标准方案,见 Redis 事务与 Lua 篇 |
| 网关限流 | Nginx/API 网关统一限 | 第一道防线,全局视角 |
| 消息队列削峰 | 请求入队按能力消费 | 不拒绝而是排队(见异步化篇) |
-- Redis 固定窗口限流(Lua 原子)
local current = redis.call("INCR", KEYS[1])
if current == 1 then
redis.call("EXPIRE", KEYS[1], ARGV[1])
end
if current > tonumber(ARGV[2]) then
return 0
end
return 1要点:Redis 是单线程,INCR + EXPIRE 天然原子;分布式限流引入 Redis 依赖,Redis 挂了要降级(本地限流兜底),别因限流组件变单点。
限流粒度
| 粒度 | 例子 | 目的 |
|---|---|---|
| 接口级 | 登录接口 10 次/分钟 | 防刷 |
| 用户级 | 每用户 100 次/分钟 | 公平使用 |
| IP 级 | 每 IP 1000 次/分钟 | 防爬 |
| 全局限流 | 系统总 QPS 上限 | 保护系统 |
分层组合:网关按 IP/全局限,应用按用户/接口限。限流要有反馈:被限的请求返回 429 + 重试头(Retry-After),客户端才知道退避。
面试追问
- 四种算法的区别? 固定窗口简单有突刺,滑动窗口精确有开销,令牌桶允许突发,漏桶绝对匀速
- 令牌桶怎么实现? Lua 维护令牌数和上次补充时间,按速率补令牌,有令牌才放行。允许突发
- 固定窗口的突刺? 窗口交界瞬间:前窗口尾和后窗口头各能放满,双倍流量。滑动窗口解决
- 分布式限流怎么做? Redis + Lua 原子计数。注意限流组件本身的可用性,挂了下沉本地限流
- 被限流返回什么? 429 Too Many Requests,带 Retry-After。客户端按指示退避重试