什么是限流?常见的限流算法有哪些?

#中间件#面试八股

什么是限流?常见的限流算法有哪些?

结论:限流(Rate Limiting)是在入口按约定速率放行请求,超出部分拒绝、排队或降级,用来保护系统不被瞬时流量打垮。常见算法五类:固定窗口、滑动窗口、漏桶、令牌桶、并发数限流,工程上再叠加预热与自适应。限流的关键不是算法本身,而是「阈值怎么定、超限怎么处理、在哪一层限」。

一、五种算法对比

算法原理突发缺陷实现
固定窗口每秒计数,超阈值拒绝边界突刺,两窗口交界可放 2 倍精度差Redis INCR 加 EXPIRE
滑动窗口按小时间片加权统计平滑内存与计算开销Sentinel 滑动窗口
漏桶恒定速率流出不支持引入排队延迟队列加定时消费
令牌桶恒定发令牌,允许攒支持桶容量需调参Guava RateLimiter
并发数信号量控制同时处理数不适用只控在途,不控 QPSSemaphore、线程池

二、固定窗口的临界问题

 阈值 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

五、限流要回答的三个问题

  1. 在哪限:网关做全局限流,应用做接口限流,客户端做防刷;层次越靠前越省资源。
  2. 限多少:压测得到单机容量,按 70% 到 80% 设定,再按实例数分摊。
  3. 超了怎么办:返回 429、排队等待、降级兜底,三者按业务容忍度选。

六、常见追问

Q1:超限的流量怎么处理? 快速失败最常见,直接返回 429 并带 Retry-After;排队等待适合削峰但要防队列堆积;降级返回兜底内容适合读接口。

Q2:限流粒度怎么定? 按接口、按用户、按 IP、按租户。粒度越细,存储与统计成本越高;通常用「接口级兜底 + 用户级防刷」两层组合。

Q3:集群限流和单机限流怎么配合? 入口用集群限流控总量,实例内用单机限流兜底,防止某个实例被负载均衡打到过载。

Q4:限流会不会误伤正常用户? 会,尤其是共享 IP 或 NAT 场景。按用户维度限流时要用账号而不是 IP,并给误伤留人工白名单。

小结

  • 固定窗口简单但有临界突刺,滑动窗口更平滑,令牌桶容许突发。
  • 限流要回答三个问题:在哪限、限多少、超了怎么办。
  • 阈值来自压测,不来自感觉。
笔记加载中…