限流方案设计#
一句话答案#
多层限流:前端→网关(Nginx 漏桶/Gateway 令牌桶)→服务(Sentinel 滑动窗口)→DB,分布式用 Redis+Lua。
核心要点
单机: Guava RateLimiter(令牌桶) 分布式: Redis+Lua(滑动窗口) / Sentinel(规则动态配置) 网关层: Nginx limit_req(漏桶) / Gateway RequestRateLimiter
多层限流: 前端→网关→服务→DB,层层防护
算法原理深挖#
令牌桶 vs 漏桶(最高频): 两者形状像,机制相反。
| 维度 | 令牌桶 Token Bucket | 漏桶 Leaky Bucket |
|---|---|---|
| 桶里装的 | 令牌(拿到令牌才能放行) | 请求(请求排队等出水) |
| 速率控制 | 按固定速率往桶里加令牌 | 按固定速率从桶里漏请求 |
| 突发流量 | 允许(桶内积攒的令牌可一次性取完) | 不允许(出水恒速,强制整流) |
| 空闲时 | 攒令牌(攒满为桶容量) | 桶空,无积累 |
| 出口速率 | 可瞬时超过平均速率 | 恒定,绝不超过 |
- 令牌桶为何容突发: 桶容量 = 攒令牌上限。系统空闲一段时间后桶里攒满令牌,此时突然来一批请求,能一次性把存量令牌取光,瞬时放行量 = 桶容量,超过平均速率——这正是”容忍突发”的本质。
- 漏桶为何整流: 不管进水多猛,出水阀门恒速。突发流量只会让桶内排队变长(满了就丢),下游永远收到平滑的恒定速率。代价是即使下游有余力也不会加速,牺牲了突发吞吐换稳定。
- 惰性补令牌(lazy refill): 工程实现不开后台线程定时加令牌,而是每次请求来时按
(now - lastTime) × rate一次性补算应新增的令牌数,再判断够不够扣。省线程、零误差。
滑动窗口消除临界突刺: 固定窗口的致命缺陷——窗口边界。设限 100次/分,若在 0:59 涌入 100 次、1:01 再涌入 100 次,跨越两个窗口但实际 2 秒内放行了 200 次,瞬时翻倍击穿。
- 解法:把 1 分钟切成 N 个小格(如 6 格、每格 10 秒),窗口随时间向前滑动,统计「当前时刻往前推 1 分钟」覆盖到的所有格子计数之和。旧格子滑出窗口即不再计入,临界点不再有翻倍空档。格子越细,精度越高、内存开销越大。
Guava RateLimiter 预热(warmup): 令牌桶的变体 SmoothWarmingUp。系统冷启动时(缓存空、连接池未建立)直接打满速率会击垮下游,所以预热期内令牌发放速率从慢逐渐加速到稳态,给下游一个爬坡缓冲。底层用一个”梯形面积”模型计算冷启动期每个令牌的等待时间,平滑过渡,避免冷系统被瞬时打满。
Redis + Lua 滑动窗口(ZSET 实现): 分布式滑动窗口的标准答案。
- 用一个 ZSET 存请求记录:
member= 请求唯一 ID,score= 请求时间戳(毫秒)。 - Lua 脚本里原子完成四步:①
ZREMRANGEBYSCORE key 0 (now - window)滑掉窗口外的过期记录;②ZCARD key统计窗口内当前请求数;③ 若未超阈值则ZADD key now reqId记入本次;④EXPIRE key兜底防泄漏。 - 为何要 Lua:判断 + 写入若分两条命令,并发下会有竞态;Lua 在 Redis 单线程里串行执行不被打断,保证「读计数-判断-写入」原子。
面试回答(2分钟版)
限流方案我会按多层防护来设计。前端层限制按钮点击频率,比如3秒内只能提交一次。网关层用Nginx的limit_req模块做漏桶限流,或者Spring Cloud Gateway配合Redis实现令牌桶,按接口维度控制QPS。服务层用Sentinel做细粒度限流,支持按接口、用户ID、IP三个维度设规则,滑动窗口算法统计请求数,规则可以通过Nacos动态配置不用重启。分布式场景下多个服务节点要统一计数,用Redis加Lua脚本实现原子的判断和计数操作,保证多节点限流准确。单机场景简单的话直接用Guava RateLimiter,底层是令牌桶算法,支持预热和平滑限流。限流触发后返回HTTP 429状态码和友好提示,不能直接丢弃请求不响应。设计时要注意限流阈值不能太严影响正常用户,建议先用监控跑一段时间拿到基线QPS再设定合理阈值。
追问与易错
追问方向:
- “网关层和服务层限流区别?”→ 网关层做粗粒度全局限流(按 IP/接口维度,保护整个集群),服务层做细粒度业务限流(按用户/资源维度,保护具体服务);两层配合,网关挡住大部分恶意流量,服务层精细控制
- “按 IP/用户/接口 分别限流?”→ IP 限流防爬虫/DDoS(单 IP 100次/分),用户限流防刷单(单用户 10次/分),接口限流保护核心资源(下单接口 5000 QPS);三个维度同时生效,任一触发即拒绝
- “限流触发返回什么?”→ 返回 HTTP 429 Too Many Requests 状态码 + Retry-After 头告知客户端多久后重试 + 友好的 JSON 错误提示;不能直接丢弃不响应,否则客户端会超时重试反而加重压力
易错点:
- ❌ 只在网关限流——服务层也需要
- ❌ 限流越严越安全——太严影响正常用户