Redis 实现限流功能的几种方法
一、问题背景
在实际业务场景中,限流是保护系统的重要手段:在一段时间(period)内,限定某个行为(action)的最大次数(max_count)。本文介绍如何基于 Redis 实现多种限流方案。
二、限流类型总览
| 限流类型 | 核心思想 | 优点 | 缺点 |
|---|---|---|---|
| 固定窗口限流 | 时间窗口固定,到期自动清零 | 实现简单 | 存在窗口边界突击流量问题 |
| 滑动窗口限流 | 窗口随时间滑动,统计窗口内请求数 | 精确解决边界问题 | 实现稍复杂 |
| 漏斗限流 | 容量固定,速率固定 | 精确控制容量和速率 | 需要 Redis 模块支持 |
| 令牌桶限流 | 令牌以固定速率放入桶中 | 支持突发流量 | 实现复杂 |
三、固定窗口限流
3.1 什么是固定窗口限流
将时间划分为固定的窗口,例如每 5 分钟为一个窗口:
|---5min---|---5min---|---5min---|---5min---| 20:00 20:05 20:10 20:15在每个窗口内独立计数,窗口到期后计数清零。
3.2 Redis 实现
-- 固定窗口限流实现localkey="***"..user_id..":"..actionlocallimit=10-- 最大次数localperiod=10-- 时间窗口(秒)-- 方式1:INCR + EXPIRE(存在问题)redis.call('INCR',key)redis.call('EXPIRE',key,period)-- 方式2:SET + INCR(正确实现,解决竞态条件)-- 使用 SET + EXPIRE 原子操作,避免窗口切换时丢失数据redis.call('SET',key,0,'EX',period,'NX')localcount=redis.call('INCR',key)returncount<=limit关键点:使用 SET + EXPIRE 代替单独 EXPIRE,避免 INCR 和 EXPIRE 之间进程崩溃导致数据丢失。
可通过 Pipeline 保证两个命令同时发送:
# Python 示例pipe=redis.pipeline()pipe.set(key,0,ex=period,nx=True)pipe.incr(key)res=pipe.execute()returnres[1]<=limit3.3 固定窗口的局限性
假设 5 分钟内限定 10 次请求:
20:04-20:05 发生 9 次请求 20:05-20:06 发生 9 次请求在 20:04-20:06 这 2 分钟内,实际发生了 18 次请求,远超每 5 分钟 10 次的限制。
问题根源:固定窗口的边界不连续,在边界处可能发生突发流量。
四、滑动窗口限流
4.1 核心思想
滑动窗口的核心是窗口随时间连续滑动,而非固定边界:
传统固定窗口: |-----5min-----|-----5min-----| 20:00 20:05 20:10 滑动窗口: 现在时刻的窗口持续向前滑动 |----5min-----|----5min-----| 20:01 20:064.2 Redis 实现(ZSET)
localfunctionis_action_allowed(red,user_id,action,period,max_count)localkey="***"..user_id..":"..actionlocalnow=redis.call('TIME')-- 获取当前时间戳(毫秒)-- 1. 记录当前行为(score 和 member 都用时间戳)red:zadd(key,now,now)-- 2. 移除窗口之前的行为记录red:zremrangebyscore(key,0,now-period*1000)-- 3. 获取窗口内的行为数量localcount=red:zcard(key)-- 4. 设置过期时间,避免冷用户持续占用内存red:expire(key,period+1)returncount<=max_countend流程图:
时间轴:[--窗口period--|---未来---] ↑now ZSET 存储:score=时间戳, member=时间戳 ZREMRANGEBYSCORE:删除 score < now-period 的旧记录 ZCARD:统计剩余元素数量,即窗口内请求数4.3 为什么用 ZSET 而非 LIST
| 数据结构 | 适用场景 |
|---|---|
| ZSET | 支持按时间范围删除,适合滑动窗口 |
| LIST | 只能按索引删除,无法按时间范围清理 |
五、漏斗限流(Redis-Cell)
5.1 什么是漏斗限流
漏斗限流的核心是容量固定 + 速率固定,能精确控制元素的容量和速率:
漏斗模型: [入口] -> (容量固定) -> [出口] ↓ 速率恒定- 漏斗容量:最多能容纳多少请求
- 漏斗速率:单位时间内能处理多少请求
5.2 Redis-Cell 模块安装
Redis-Cell 是 Redis 的第三方模块,采用 Rust 编写,需要单独安装:
# 下载并编译gitclone https://github.com/brandur/redis-cellcdredis-cellcargobuild--releasecptarget/release/libredis_cell.so /path/to/modules/# 启动 Redis 加载模块redis-server--loadmodule/path/to/modules/libredis_cell.so5.3 CL.THROTTLE 命令详解
CL.THROTTLE key capacity operations seconds[quota]参数说明:
| 参数 | 含义 | 示例 |
|---|---|---|
| key | 漏斗容器名称 | user:123:login |
| capacity | 漏斗容量(最大容纳请求数) | 10 |
| operations | 单位时间内的操作次数 | 5 |
| seconds | 单位时间(秒) | 60 |
| quota | 单次行为消耗的令牌数(可选,默认1) | 1 |
示例:每 60 秒最多 5 次请求,漏斗容量 10
CL.THROTTLE user:123:login10560返回结果:
1) (integer) 0 # 是否被限流(0=允许,1=拒绝) 2) (integer) 7 # 漏斗剩余容量 3) (integer) 7 # 如果被拒绝,还需要等多久(秒) 4) (integer) -1 # 预留字段 5) (integer) 60 # 下次请求的间隔时间5.4 流速计算
流速 = operations / seconds = 5 / 60 ≈ 0.083 请求/秒这意味着每秒只能处理约 0.083 个请求,即约 12 秒处理 1 个请求。
六、令牌桶限流
6.1 核心思想
令牌桶的核心是令牌以固定速率放入桶中:
令牌桶: -> [桶容量] -> 请求消耗令牌 -> 通过 ↑ 固定速率放入令牌- 桶容量:最大令牌数
- 令牌添加速率:每秒添加多少令牌
- 请求消耗:每个请求消耗 1 个令牌
6.2 特点
| 特点 | 说明 |
|---|---|
| 支持突发流量 | 桶满时可一次性处理多个请求 |
| 令牌非即时补充 | 需要等待令牌生成 |
6.3 与漏斗限流的区别
| 对比维度 | 漏斗限流 | 令牌桶限流 |
|---|---|---|
| 速率 | 匀速 | 匀速(令牌补充) |
| 突发能力 | 不支持 | 支持(桶满时) |
| 实现难度 | 较简单 | 较复杂 |
七、四种限流方案对比
| 维度 | 固定窗口 | 滑动窗口 | 漏斗限流 | 令牌桶 |
|---|---|---|---|---|
| 实现复杂度 | 低 | 中 | 低 | 高 |
| 边界突击 | 有 | 无 | 无 | 无 |
| 突发流量支持 | 不支持 | 不支持 | 不支持 | 支持 |
| 精度控制 | 低 | 中 | 高 | 高 |
| 额外依赖 | 无 | 无 | Redis-Cell | 无 |
八、面试追问 FAQ
| 问题 | 回答要点 |
|---|---|
| Q: 为什么固定窗口需要 SET + INCR 组合? | 单独 INCR + EXPIRE 在进程崩溃时可能丢失数据,SET+EXPIRE 原子操作保证一致性 |
| Q: 滑动窗口为什么要设置过期时间为 period+1? | 避免窗口边界附近过期导致数据丢失,确保跨窗口的请求仍被统计 |
| Q: 漏斗限流和令牌桶限流各适用于什么场景? | 漏斗:需要精确控制速率的 API 限流;令牌桶:允许突发流量的场景(如秒杀) |
| Q: Redis-Cell 是原子操作吗? | 是,CL.THROTTLE 整个命令是原子的,无需担心并发问题 |
| Q: 滑动窗口的 ZSET 会不会无限增长? | 不会,每次请求都会清理窗口外的旧数据,且有 expire 保证清理 |
九、相关题目
| 题目 | 考察点 |
|---|---|
| Redis 固定窗口限流如何保证原子性? | SET + INCR + Pipeline |
| 滑动窗口限流为什么用 ZSET 而不是 LIST? | 按时间范围删除的能力 |
| 漏斗限流如何计算流速? | operations / seconds |
| 令牌桶和漏斗限流的本质区别? | 突发流量支持 |
十、总结
| 限流方案 | 实现难度 | 精度 | 突发流量 | 推荐场景 |
|---|---|---|---|---|
| 固定窗口 | 低 | 低 | 不支持 | 简单场景 |
| 滑动窗口 | 中 | 中 | 不支持 | 需要精确控制 |
| 漏斗限流 | 低 | 高 | 不支持 | API 限流 |
| 令牌桶 | 高 | 高 | 支持 | 秒杀/抢购 |
核心结论:根据业务场景选择合适的限流方案,简单场景用固定窗口,精确控制用滑动窗口或漏斗限流,需要突发能力用令牌桶。
根据零声教育教学写作https://github.com/0voice
