一句话总结
限流算法对比:固定窗口(简单但有临界突刺问题)→ 滑动窗口(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 高时用本地缓存 + 异步同步。