如何设计一个API限流系统?

2025年 阅读约 15 分钟 面试指南 · 系统设计

深入解析API限流系统设计:固定窗口(临界问题)、滑动窗口(Redis ZSet实现)、令牌桶(Guava RateLimiter)、漏桶算法对比,Redis+Lua实现分布式限流,Sentinel源码分析,附面试模拟问答。

一句话总结

限流算法对比:固定窗口(简单但有临界突刺问题)→ 滑动窗口(Redis ZSet,精确但开销大)→ 漏桶(恒定速率,适合保护下游)→ 令牌桶(允许突发,最常用)。分布式限流用 Redis + Lua 脚本保证原子性。生产推荐:单机用 Guava RateLimiter,分布式用 Sentinel 或自研 Redis 方案。

初级理解

四种限流算法对比

算法原理突发流量实现难度
固定窗口每个时间窗口计数,超阈值拒绝临界突刺简单
滑动窗口窗口滑动,统计窗口内请求数平滑中等
漏桶请求进桶,恒定速率流出不允许中等
令牌桶恒定速率放令牌,请求需获取令牌允许(桶容量)中等
# 固定窗口问题(临界突刺) # 窗口1(0~1秒):500 请求(阈值 500) # 窗口2(1~2秒):500 请求 # 实际 0.5~1.5 秒内:1000 请求!(两个窗口交界处) # 系统承受了 2 倍阈值流量
一句话总结:固定窗口有临界问题,令牌桶最常用(允许突发),漏桶最平滑。

中级深入

滑动窗口 — Redis ZSet 实现

# Redis Lua 脚本实现滑动窗口限流 local key = KEYS[1] # 限流 key local window = tonumber(ARGV[1]) # 窗口大小(秒) local limit = tonumber(ARGV[2]) # 限制数量 local now = tonumber(ARGV[3]) # 当前时间戳(毫秒) # 1. 移除窗口外的记录 redis.call('ZREMRANGEBYSCORE', key, 0, now - window * 1000) # 2. 统计窗口内请求数 local count = redis.call('ZCARD', key) # 3. 判断是否超限 if count < limit then redis.call('ZADD', key, now, now .. '-' .. math.random()) redis.call('EXPIRE', key, window) return 1 # 允许 end return 0 # 拒绝

令牌桶 — Guava RateLimiter

# Guava RateLimiter(单机) RateLimiter limiter = RateLimiter.create(100); # 每秒 100 个令牌 # 阻塞获取 limiter.acquire(); # 阻塞直到获取到令牌 # 非阻塞获取 if (limiter.tryAcquire()) { # 执行业务 } else { # 限流处理 } # 令牌桶原理 # 1. 以固定速率(100/s)生成令牌放入桶中 # 2. 桶有容量上限(默认 1 秒的量) # 3. 请求需要获取令牌才能执行 # 4. 桶满则丢弃多余令牌 # 5. 突发流量:桶中积累的令牌可应对短期突发
中级要点:滑动窗口用 Redis ZSet + Lua;令牌桶用 Guava RateLimiter(单机)。

高级拓展

Sentinel 分布式限流

# Sentinel 限流规则 FlowRule rule = new FlowRule(); rule.setResource("getUser"); rule.setGrade(RuleConstant.FLOW_GRADE_QPS); # QPS 限流 rule.setCount(100); # 每秒 100 个请求 FlowRuleManager.loadRules(List.of(rule)); # 使用 try (Entry entry = SphU.entry("getUser")) { # 执行业务 } catch (BlockException e) { # 限流处理 } # Sentinel 原理 # 1. 滑动窗口统计(LeapArray) # 2. 支持 QPS 和并发线程数限流 # 3. 支持流控效果:快速失败、Warm Up、排队等待 # 4. 支持集群限流(Token Server 模式)

多层限流架构

# 分层限流 # 1. Nginx 层:IP 级别限流(limit_req) # 2. 网关层:API 级别限流(Sentinel/自研) # 3. 应用层:方法级别限流(Sentinel) # 4. 业务层:用户级别限流(Redis) # 限流维度 # - 全局 QPS:保护整个系统 # - 接口 QPS:保护单个接口 # - 用户 QPS:防止单个用户滥用 # - IP QPS:防止爬虫

实战场景

场景:短信验证码接口限流

# 需求:同一手机号 60 秒内只能发 1 次,每天最多 10 次 # Redis Lua 脚本 local phone = KEYS[1] local minute_key = "sms:minute:" .. phone local daily_key = "sms:daily:" .. phone # 1. 检查 60 秒限制 local minute_count = redis.call('GET', minute_key) if minute_count and tonumber(minute_count) >= 1 then return -1 # 60 秒内已发送 end # 2. 检查每天限制 local daily_count = redis.call('GET', daily_key) if daily_count and tonumber(daily_count) >= 10 then return -2 # 超过每天限制 end # 3. 更新计数 redis.call('INCR', minute_key) redis.call('EXPIRE', minute_key, 60) redis.call('INCR', daily_key) redis.call('EXPIRE', daily_key, 86400) return 1 # 允许发送

面试模拟

面试官:限流算法有哪些?各有什么优缺点?

你:1)固定窗口:简单但有临界突刺问题;2)滑动窗口:精确但 Redis ZSet 开销大;3)漏桶:恒定速率,适合保护下游,但不允许突发;4)令牌桶:允许突发(桶容量),最常用。生产推荐令牌桶(Guava RateLimiter 单机,Sentinel 分布式)。

面试官:分布式限流怎么实现?

你:Redis + Lua 脚本保证原子性。滑动窗口用 ZSet(ZREMRANGEBYSCORE + ZCARD),令牌桶用 Hash 存令牌数。也可以用 Sentinel 集群限流(Token Server 模式)。注意 Redis 性能:限流 QPS 高时用本地缓存 + 异步同步。