一、问题背景在实际业务场景中限流是保护系统的重要手段在一段时间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..:..actionlocallimit10-- 最大次数localperiod10-- 时间窗口秒-- 方式1INCR EXPIRE存在问题redis.call(INCR,key)redis.call(EXPIRE,key,period)-- 方式2SET INCR正确实现解决竞态条件-- 使用 SET EXPIRE 原子操作避免窗口切换时丢失数据redis.call(SET,key,0,EX,period,NX)localcountredis.call(INCR,key)returncountlimit关键点使用 SET EXPIRE 代替单独 EXPIRE避免 INCR 和 EXPIRE 之间进程崩溃导致数据丢失。可通过 Pipeline 保证两个命令同时发送# Python 示例piperedis.pipeline()pipe.set(key,0,experiod,nxTrue)pipe.incr(key)respipe.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 实现ZSETlocalfunctionis_action_allowed(red,user_id,action,period,max_count)localkey***..user_id..:..actionlocalnowredis.call(TIME)-- 获取当前时间戳毫秒-- 1. 记录当前行为score 和 member 都用时间戳red:zadd(key,now,now)-- 2. 移除窗口之前的行为记录red:zremrangebyscore(key,0,now-period*1000)-- 3. 获取窗口内的行为数量localcountred:zcard(key)-- 4. 设置过期时间避免冷用户持续占用内存red:expire(key,period1)returncountmax_countend流程图时间轴[--窗口period--|---未来---] ↑now ZSET 存储score时间戳, member时间戳 ZREMRANGEBYSCORE删除 score now-period 的旧记录 ZCARD统计剩余元素数量即窗口内请求数4.3 为什么用 ZSET 而非 LIST数据结构适用场景ZSET支持按时间范围删除适合滑动窗口LIST只能按索引删除无法按时间范围清理五、漏斗限流Redis-Cell5.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:logincapacity漏斗容量最大容纳请求数10operations单位时间内的操作次数5seconds单位时间秒60quota单次行为消耗的令牌数可选默认11示例每 60 秒最多 5 次请求漏斗容量 10CL.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 在进程崩溃时可能丢失数据SETEXPIRE 原子操作保证一致性Q: 滑动窗口为什么要设置过期时间为 period1避免窗口边界附近过期导致数据丢失确保跨窗口的请求仍被统计Q: 漏斗限流和令牌桶限流各适用于什么场景漏斗需要精确控制速率的 API 限流令牌桶允许突发流量的场景如秒杀Q: Redis-Cell 是原子操作吗是CL.THROTTLE 整个命令是原子的无需担心并发问题Q: 滑动窗口的 ZSET 会不会无限增长不会每次请求都会清理窗口外的旧数据且有 expire 保证清理九、相关题目题目考察点Redis 固定窗口限流如何保证原子性SET INCR Pipeline滑动窗口限流为什么用 ZSET 而不是 LIST按时间范围删除的能力漏斗限流如何计算流速operations / seconds令牌桶和漏斗限流的本质区别突发流量支持十、总结限流方案实现难度精度突发流量推荐场景固定窗口低低不支持简单场景滑动窗口中中不支持需要精确控制漏斗限流低高不支持API 限流令牌桶高高支持秒杀/抢购核心结论根据业务场景选择合适的限流方案简单场景用固定窗口精确控制用滑动窗口或漏斗限流需要突发能力用令牌桶。根据零声教育教学写作https://github.com/0voice
Redis 实现限流功能的几种方法
发布时间:2026/5/21 21:27:15
一、问题背景在实际业务场景中限流是保护系统的重要手段在一段时间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..:..actionlocallimit10-- 最大次数localperiod10-- 时间窗口秒-- 方式1INCR EXPIRE存在问题redis.call(INCR,key)redis.call(EXPIRE,key,period)-- 方式2SET INCR正确实现解决竞态条件-- 使用 SET EXPIRE 原子操作避免窗口切换时丢失数据redis.call(SET,key,0,EX,period,NX)localcountredis.call(INCR,key)returncountlimit关键点使用 SET EXPIRE 代替单独 EXPIRE避免 INCR 和 EXPIRE 之间进程崩溃导致数据丢失。可通过 Pipeline 保证两个命令同时发送# Python 示例piperedis.pipeline()pipe.set(key,0,experiod,nxTrue)pipe.incr(key)respipe.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 实现ZSETlocalfunctionis_action_allowed(red,user_id,action,period,max_count)localkey***..user_id..:..actionlocalnowredis.call(TIME)-- 获取当前时间戳毫秒-- 1. 记录当前行为score 和 member 都用时间戳red:zadd(key,now,now)-- 2. 移除窗口之前的行为记录red:zremrangebyscore(key,0,now-period*1000)-- 3. 获取窗口内的行为数量localcountred:zcard(key)-- 4. 设置过期时间避免冷用户持续占用内存red:expire(key,period1)returncountmax_countend流程图时间轴[--窗口period--|---未来---] ↑now ZSET 存储score时间戳, member时间戳 ZREMRANGEBYSCORE删除 score now-period 的旧记录 ZCARD统计剩余元素数量即窗口内请求数4.3 为什么用 ZSET 而非 LIST数据结构适用场景ZSET支持按时间范围删除适合滑动窗口LIST只能按索引删除无法按时间范围清理五、漏斗限流Redis-Cell5.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:logincapacity漏斗容量最大容纳请求数10operations单位时间内的操作次数5seconds单位时间秒60quota单次行为消耗的令牌数可选默认11示例每 60 秒最多 5 次请求漏斗容量 10CL.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 在进程崩溃时可能丢失数据SETEXPIRE 原子操作保证一致性Q: 滑动窗口为什么要设置过期时间为 period1避免窗口边界附近过期导致数据丢失确保跨窗口的请求仍被统计Q: 漏斗限流和令牌桶限流各适用于什么场景漏斗需要精确控制速率的 API 限流令牌桶允许突发流量的场景如秒杀Q: Redis-Cell 是原子操作吗是CL.THROTTLE 整个命令是原子的无需担心并发问题Q: 滑动窗口的 ZSET 会不会无限增长不会每次请求都会清理窗口外的旧数据且有 expire 保证清理九、相关题目题目考察点Redis 固定窗口限流如何保证原子性SET INCR Pipeline滑动窗口限流为什么用 ZSET 而不是 LIST按时间范围删除的能力漏斗限流如何计算流速operations / seconds令牌桶和漏斗限流的本质区别突发流量支持十、总结限流方案实现难度精度突发流量推荐场景固定窗口低低不支持简单场景滑动窗口中中不支持需要精确控制漏斗限流低高不支持API 限流令牌桶高高支持秒杀/抢购核心结论根据业务场景选择合适的限流方案简单场景用固定窗口精确控制用滑动窗口或漏斗限流需要突发能力用令牌桶。根据零声教育教学写作https://github.com/0voice