限流是保障系统稳定性的最后一道防线。本文系统梳理限流系统的四大算法、单机 vs 分布式限流、滑动窗口精确实现 ,以及如何在面试中设计一个完整的分布式限流服务。
1. 为什么需要限流? 1 2 3 4 5 6 不限流的后果: 突发流量 → 服务资源耗尽 → 响应变慢 → 超时堆积 → 雪崩 限流的本质: 用"拒绝部分请求"换取"剩余请求的服务质量" 宁可少服务一些用户,也不让所有用户都得到差服务
限流 vs 熔断 vs 降级的区别 :
触发条件
作用方向
目标
限流
请求量超阈值
在入口拒绝请求
保护自身
熔断
下游错误率超阈值
不调用下游
保护下游
降级
资源不足或超时
走简化逻辑
保证主路径可用
2. 四大限流算法 2.1 固定窗口计数器(Fixed Window) 1 2 3 4 5 6 7 8 9 时间轴: |---窗口1(0-60s)---|---窗口2(60-120s)---| 计数: 100请求 100请求 实现: counter = redis.incr(f"rate:{user_id}:{current_minute}") if counter > 100: reject() if counter == 1: redis.expire(f"rate:{user_id}:{current_minute}", 60)
致命缺陷:临界突破
1 2 3 59秒:100个请求(窗口1打满) 60秒:100个请求(窗口2重置,又允许100个) → 2秒内实际通过200个请求,是限额的2倍!
2.2 滑动窗口计数器(Sliding Window Log) 用时间戳日志精确统计:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 def is_allowed (user_id, limit=100 , window=60 ): now = time.time() key = f"rate:{user_id} " pipe = redis.pipeline() pipe.zremrangebyscore(key, 0 , now - window) pipe.zcard(key) pipe.zadd(key, {now: now}) pipe.expire(key, window) _, count, _, _ = pipe.execute() return count < limit
优点 :精确,无临界问题缺点 :每个请求都存一条记录,大 QPS 下内存消耗大
面试优化 :滑动窗口计数(不存日志,分格子)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 def is_allowed_sliding_count (user_id, limit=100 , window=60 , precision=60 ): now = time.time() slot = int (now * precision / window) total = 0 for i in range (precision): key = f"rate:{user_id} :{slot - i} " count = redis.get(key) or 0 total += int (count) if total >= limit: return False redis.incr(f"rate:{user_id} :{slot} " ) redis.expire(f"rate:{user_id} :{slot} " , window + 1 ) return True
2.3 漏桶(Leaky Bucket) 1 2 3 4 5 6 7 请求 → 桶(队列)→ 以固定速率流出 ┌──────────┐ 请求 →→→ │ 桶(队列) │ → 以 r req/s 匀速处理 └──────────┘ ↑ 桶满则丢弃新请求
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 class LeakyBucket : def __init__ (self, capacity=100 , leak_rate=10 ): self.capacity = capacity self.water = 0 self.last_time = time.time() self.leak_rate = leak_rate def allow (self ): now = time.time() elapsed = now - self.last_time self.water = max (0 , self.water - elapsed * self.leak_rate) self.last_time = now if self.water < self.capacity: self.water += 1 return True return False
特点:输出速率恒定 ,适合对下游的保护(防止突发打垮下游)。
2.4 令牌桶(Token Bucket)— 最常用 1 2 3 4 5 6 7 令牌以固定速率生成,放入桶中 请求来了必须先取令牌,取到才能通过 ┌──────────┐ 令牌 →→→ │ 桶(令牌) │ → 请求取走令牌后通过 └──────────┘ 每秒生成 r 个令牌,桶容量 b
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 class TokenBucket : def __init__ (self, rate=10 , capacity=100 ): self.rate = rate self.capacity = capacity self.tokens = capacity self.last_time = time.time() def allow (self, tokens_required=1 ): now = time.time() elapsed = now - self.last_time self.tokens = min ( self.capacity, self.tokens + elapsed * self.rate ) self.last_time = now if self.tokens >= tokens_required: self.tokens -= tokens_required return True return False
令牌桶 vs 漏桶核心差异 :
令牌桶允许突发 (桶满时积累了令牌,可以瞬间消耗)
漏桶输出匀速 (无论请求多密集,处理速度恒定)
3. 分布式限流 3.1 为什么单机限流不够? 1 2 3 4 5 假设限额:100 QPS/用户 部署 10 个实例,每个实例限流 100 QPS 用户实际可以:向不同实例发请求,达到 10 * 100 = 1000 QPS → 单机限流在分布式环境下形同虚设(除非请求总是打到同一实例)
3.2 基于 Redis 的集中式限流 所有实例共享 Redis 计数器:
方案一:Redis + Lua 脚本(原子操作)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 local key = KEYS[1 ]local limit = tonumber (ARGV[1 ])local window = tonumber (ARGV[2 ])local current = redis.call('INCR' , key)if current == 1 then redis.call('EXPIRE' , key, window) end if current > limit then return 0 else return 1 end
1 2 3 4 5 6 script = redis.register_script(open ('rate_limit.lua' ).read()) def is_allowed (user_id, limit=100 , window=60 ): key = f"rate:{user_id} :{int (time.time() / window)} " result = script(keys=[key], args=[limit, window]) return result == 1
方案二:Redis + Sliding Window(精确)
使用 ZSET 实现精确滑动窗口(见上文 2.2),适合对精度要求高的场景。
方案三:Nginx + lua-resty-limit(网关层限流)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 http { lua_shared_dict rate_limit 10m ; server { location /api/ { access_by_lua_block { local limit = require "resty.limit.req" local lim, err = limit.new("rate_limit" , 100 , 200 ) local key = ngx.var.remote_addr local delay, err = lim:incoming(key, true ) if not delay then if err == "rejected" then return ngx.exit(429 ) end end } } } }
3.3 本地计数器 + 定期同步(高性能方案) 当 Redis 限流成为瓶颈时(网络 RTT 叠加),用本地计数器 + 定期同步:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 class DistributedRateLimiter : def __init__ (self, global_limit=10000 , sync_interval=100ms ): self.local_count = 0 self.local_quota = global_limit / num_instances self.sync_interval = sync_interval def allow (self ): self.local_count += 1 if self.local_count <= self.local_quota: return True return False def sync_to_redis (self ): global_count = redis.incrby("global_counter" , self.local_count) self.local_count = 0 self.local_quota = recalculate_quota(global_count)
适用场景 :QPS 极高(10万+),允许轻微超量(配额分配有误差)。
4. 完整系统设计:API 限流服务 面试题:设计一个支持亿级用户的 API 限流系统
4.1 需求澄清 1 2 3 4 5 - 限流维度:用户级、IP级、API 级?→ 多维度 - 限流策略:固定 QPS?还是动态配置?→ 动态,运行时可修改 - 精度要求:允许 ±5% 误差?→ 允许小误差 - 响应时间:限流判断增加的延迟 → < 1ms - 规模:DAU 1亿,峰值 1000万 QPS
4.2 整体架构 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 ┌─────────────────────────────────┐ │ API Gateway │ │ ┌─────────────────────────┐ │ Client ────────► │ │ Rate Limit Middleware │ │ │ │ (本地令牌桶 + 异步同步) │ │ │ └────────────┬────────────┘ │ └───────────────┼─────────────────┘ │ 定期同步 (100ms) ┌───────────────▼─────────────────┐ │ Rate Limit Service │ │ (全局计数 + 配额分配算法) │ └───────────────┬─────────────────┘ │ ┌───────────────▼─────────────────┐ │ Redis Cluster │ │ (全局计数器 + 限流规则存储) │ └─────────────────────────────────┘ │ ┌───────────────▼─────────────────┐ │ Config Service │ │ (动态限流规则,运行时修改) │ └─────────────────────────────────┘
4.3 限流规则设计 1 2 3 4 5 6 7 8 9 10 11 12 13 14 rate_limit_rules: user:{user_id}:api:/search: limit: 100 window: 60s algorithm: sliding_window ip:{ip_address}: limit: 1000 window: 60s algorithm: token_bucket api:/checkout:global: limit: 50000 window: 1s algorithm: token_bucket
4.4 限流响应设计 被限流时,返回标准 HTTP 429:
1 2 3 4 5 6 7 HTTP/1.1 429 Too Many RequestsRetry-After : 30X-RateLimit-Limit : 100X-RateLimit-Remaining : 0X-RateLimit-Reset : 1718300400{ "error" : "rate_limit_exceeded" , "message" : "Too many requests, retry after 30 seconds" }
5. 常见限流场景与对应策略
场景
推荐算法
理由
API 接口保护(允许突发)
令牌桶
允许短时突发,日常匀速
下游 DB/服务保护
漏桶
匀速输出,保护下游
精确用量计费
滑动窗口
无临界问题,精确计量
简单 QPS 限制(允许误差)
固定窗口
实现简单,够用
极高 QPS + 允许小误差
本地计数器 + 定期同步
减少 Redis RTT
6. 面试常见追问 Q: 限流时如何公平对待不同用户?
A: 实现优先级队列:VIP 用户拥有更高配额(10x);在接近限额时,优先拒绝低优先级请求;可结合令牌桶为不同等级用户分配不同的桶容量和填充速率。
Q: 如何防止限流服务本身成为单点故障?
A: Redis Cluster 保证存储层高可用;Rate Limit Service 本身无状态,可水平扩展;网关本地有降级策略(Redis 不可达时,降级为本地限流;甚至临时关闭限流,优先保证业务可用)。
Q: 分布式环境下如何保证”全局精确限流”?
A: 严格意义上的全局精确限流(无任何误差)代价极高,需要每个请求都做跨实例的 Redis 原子操作,延迟显著上升。实际工程中接受 1-5% 的超量(本地计数器 + 定期同步方案),用精度换性能,这是合理的工程权衡。
总结 1 2 3 4 5 6 7 8 9 10 算法选型:令牌桶(允许突发 API 限流) vs 漏桶(保护下游) 固定窗口(简单) vs 滑动窗口(精确) 实现层次: 网关层(Nginx/Kong)→ 最早拦截,保护后端 应用层(SDK/Middleware)→ 细粒度,可按业务维度限流 服务层(Sentinel/Hystrix)→ 熔断+限流一体 分布式策略: Redis 集中计数(精确) vs 本地计数+同步(高性能)
面试核心 :限流不只是”计数器”,要展示你对突发流量处理、分布式一致性权衡、降级兜底的系统性思考。