Skip to content

05.4 - 限流与计数器设计

定位: 使用 Redis 实现高性能的限流机制——保护后端服务不被突发流量冲垮 面试高频度: ⭐⭐⭐⭐ 考查方式: 滑动窗口实现、令牌桶 vs 漏桶、Redis 限流的优缺点


一、这是什么?为什么需要它?

是什么

限流(Rate Limiting)是控制系统在一定时间窗口内处理的请求数量上限的一种机制。

为什么用 Redis 做限流?


二、方案对比与实现

2.1 固定窗口计数器

最简单的限流方式:每分钟只能请求 100 次

INCR "rate_limit:user:1001:202407121500" → 第一次 → 返回 1 → 第二次 → 返回 2 ... → 第 101 次 → 返回 101 > 100 → 拒绝

到期重置: EXPIRE 60 秒

固定窗口的"突刺"问题: 14:59:59 → 100 次请求(窗口满了) 15:00:00 → 100 次请求(窗口重置) 连续 200 次请求在 1 秒内通过

2.2 滑动窗口

ZSet 实现(每个请求一个成员):

  • ZREMRANGEBYSCORE rate:user:1001 0 (now-60s) — 移除窗口外的
  • ZCARD rate:user:1001 — 计数
  • ZADD rate:user:1001 now request_id — 加新请求

2.3 令牌桶

2.4 对比总结

方案精确度内存占用实现复杂度场景
固定窗口低(突刺)低(1 key)最简单非严格限流
滑动窗口中(N 个成员)简单API 限流 ★
令牌桶高(允许突发)低(1 key)流量整形 ★
漏斗(Leaky Bucket)高(平滑)流量整形

三、实战验证

3.1 固定窗口限流

bash
# 简单的固定窗口限流:每分钟 5 次
redis-cli

# 请求 1-5
127.0.0.1:6379> INCR "rate:api:user1001"
(integer) 1
127.0.0.1:6379> INCR "rate:api:user1001"
(integer) 2
...
127.0.0.1:6379> INCR "rate:api:user1001"
(integer) 5

# 第 6 次请求(应该限制)
127.0.0.1:6379> INCR "rate:api:user1001"
(integer) 6  # > 5 → 拒绝

# 查看过期时间
127.0.0.1:6379> TTL "rate:api:user1001"
(integer) 45  # 还有 45 秒过期

3.2 滑动窗口限流

bash
# 加载 Lua 脚本
redis-cli --eval sliding_window.lua rate:sliding , $(date +%s) 60 5

# Lua 脚本内容(上面 2.2 节)
# 测试:同一秒内多次请求
redis-cli --eval sliding_window.lua rate:sliding , $(date +%s) 60 5
# (integer) 1  # 通过
redis-cli --eval sliding_window.lua rate:sliding , $(date +%s) 60 5
# (integer) 1
# ... 6 次后
# (integer) 0  # 拒绝

3.3 常见限流场景

bash
# 场景 1:用户等级限流
# VIP 用户 1000 QPS,普通用户 100 QPS
redis-cli SET "rate_limit:vip:1000" "" NX
redis-cli SET "rate_limit:normal:100" "" NX

# 场景 2:全局限流(所有用户共享配额)
# 整个接口每分钟 10000 次
redis-cli INCR "rate:global:api:orders"
# ... > 10000 拒绝

# 场景 3:基于 ZSet 的并发限流
# 获取当前正在处理的请求数
redis-cli ZADD "concurrent:orders" $(date +%s%3N) "req:123"
redis-cli ZCARD "concurrent:orders"
# > 阈值拒绝
redis-cli ZREMRANGEBYSCORE "concurrent:orders" 0 $(date +%s%3N)

四、面试视角

追问答案要点
固定窗口和滑动窗口的区别?固定窗口在窗口边界有突刺风险(秒级超量);滑动窗口粒度更细(分片平滑)
令牌桶和漏桶的区别?令牌桶允许短时间突发(积累的令牌一次用完);漏桶强制恒速输出(无突发)
Redis 限流的瓶颈在哪?单线程模型下大量 INCR 或 ZSet 操作可能成为瓶颈,建议配合 Lua 减少网络往返
高并发场景下 Redis 限流会不会成为瓶颈?单个限流 key 的 INCR 约 10 万 QPS(够用),超大量需要本地 + Redis 两级限流
Lua 脚本在限流中的优势?原子性(避免竞态)+ 减少网络 RTT(多命令一次发送)
限流信息的过期时间怎么设置?至少 = 窗口大小 + 1 秒,避免窗口未结束 key 就过期
竞态问题:两个请求同时 INCR 得到同一个值?INCR 是原子的,不会出现。但如果用 GET+SET 组合就会出现——必须用 INCR 或 Lua

📚 相关链接

  • **缓存三大问题** — 限流是雪崩的兜底方案
  • **Lua脚本** — 限流算法的原子性保障
  • **Pipeline** — 批量操作的限流保护
  • ← 返回 **缓存设计与应用索引**

Knowledge4J — Java 知识库