Redis 如何实现限流?固定窗口、滑动窗口和令牌桶怎么选?
简化版
Redis 实现限流常见有固定窗口计数、滑动窗口、令牌桶和漏桶。固定窗口简单但边界突刺明显;滑动窗口更平滑但成本更高;令牌桶允许一定突发,适合接口限流。线上通常用 Redis + Lua 保证判断和更新原子性。
详细版
常见 Redis 限流方案:
- 固定窗口:
INCR + EXPIRE,简单高效。 - 滑动窗口:ZSet 存请求时间戳,按时间范围统计。
- 令牌桶:按速率生成令牌,请求消耗令牌。
- 漏桶:请求以固定速率流出,更强调平滑。
固定窗口适合粗粒度限制,如每分钟最多 100 次;滑动窗口适合更精确控制;令牌桶适合允许瞬时突发但限制平均速率的接口。
关键点是原子性和过期清理。多条 Redis 命令要用 Lua 或事务,否则并发下可能超发。
完整版教学
一、Redis 限流要解决什么
限流的目标是保护系统容量,避免某个用户、IP、接口或租户在短时间内打爆后端。Redis 适合做分布式限流,因为它快、集中、支持原子脚本。
限流不是简单拒绝请求,还要定义维度、窗口、阈值、突发能力和降级动作。例如“用户 1 分钟最多 60 次”和“接口全局每秒最多 1000 次”是不同限流器。
记忆钩子:限流先定“谁、多久、多少、超了怎么办”,再谈算法。
二、固定窗口最简单
固定窗口用 key 表示某个时间段的计数。请求来时 INCR,第一次设置过期时间,超过阈值就拒绝。
key = rate:user:100:202607291705
INCR key
EXPIRE key 60
如果限制每分钟 100 次,实现很简单,性能也好。但问题是窗口边界突刺:用户在 00:59 发 100 次,01:00 又发 100 次,两秒内可能通过 200 次。
三、滑动窗口更精确
滑动窗口通常用 ZSet 存请求时间戳。每次请求先删除窗口外的数据,再统计窗口内数量,未超过阈值则加入当前时间戳。
ZREMRANGEBYSCORE key 0 now-60000
ZCARD key
ZADD key now requestId
EXPIRE key 60
它能避免固定窗口边界突刺,但每次请求要维护 ZSet,成本比 INCR 高。高 QPS 场景要注意内存和 CPU。
四、令牌桶适合允许突发
令牌桶按固定速率生成令牌,桶有容量上限。请求来了,如果有令牌就通过并消耗一个;没有令牌就拒绝或排队。
假设生成速率是每秒 10 个,桶容量 50 个。系统空闲 5 秒后桶满,下一秒可以允许最多 50 个突发请求,但长期平均仍接近每秒 10 个。
new_tokens = min(capacity, old_tokens + elapsed * rate)
if new_tokens >= cost:
allow and tokens = new_tokens - cost
else:
reject
这个模型适合 API 限流,因为真实流量常有短暂突发。
五、漏桶强调平滑输出
漏桶可以理解为请求先进入桶,再按固定速率流出。桶满时新请求被拒绝。它比令牌桶更强调平滑,不太鼓励突发。
| 算法 | 优点 | 缺点 |
|---|---|---|
| 固定窗口 | 简单高效 | 边界突刺 |
| 滑动窗口 | 更精确 | 成本较高 |
| 令牌桶 | 支持突发 | 实现稍复杂 |
| 漏桶 | 输出平滑 | 突发友好性差 |
面试时可以结合业务选择:登录防刷可用固定/滑动窗口,开放 API 常用令牌桶。
六、为什么要用 Lua 保证原子性
限流通常包含读取、判断、写入、设置过期多个步骤。如果这些步骤分开执行,在并发下可能多个请求同时看到“未超过”,然后一起放行。
Lua 脚本在 Redis 中执行具有原子性,可以把判断和更新放在服务端一次完成。
客户端并发请求
|
Redis Lua:读取状态 -> 计算 -> 写回 -> 返回 allow/reject
这样既减少网络往返,也避免并发超发。
七、线上限流要考虑降级体验
限流不是算法写完就结束。被限流后返回什么、是否重试、是否排队、是否按租户隔离,都影响用户体验和系统稳定性。
还要避免 Redis 自身成为瓶颈。全站所有请求都打一个限流 key,会产生热 Key;可以按用户、接口、地域拆维度,或者在网关本地做预限流,Redis 做全局校准。
本地限流挡一层 -> Redis 全局限流 -> 后端服务保护
八、常见误区与追问
- 误区:固定窗口没有问题。 它有窗口边界突刺,精确限流场景要谨慎。
- 误区:滑动窗口一定最好。 它更精确但成本更高,高 QPS 下要评估 ZSet 开销。
- 误区:不用 Lua 也不会超发。 多命令组合在并发下可能出现竞态。
- 追问:令牌桶和漏桶怎么选? 令牌桶允许突发,漏桶强调平滑输出。
- 追问:限流 key 怎么设计? 按用户、IP、接口、租户、时间窗口组合,避免过粗或过细。
- 追问:Redis 挂了限流怎么办? 根据业务选择 fail-open 或 fail-close,并配本地限流兜底。
九、加强记忆
记住“固定简单、滑动精确、令牌突发、漏桶平滑”。Redis 实现限流的灵魂是原子脚本和合理 key 维度。
答题时先讲算法取舍,再讲 Lua 原子性和热 Key 风险,会比只背 INCR + EXPIRE 更完整。