Redis 窗口计数限流器的边界与过期策略
来源:17golang原创
时间:2026-10-10 17:19:17 309浏览 收藏
Redis 窗口计数限流器真正难处理的地方,不是把数字加一,而是决定这个数字什么时候过期。若每次请求都重新执行 EXPIRE,看起来像是给计数器续命,实际可能把固定窗口变成滑动窗口;若先执行 INCR,再单独执行 EXPIRE,客户端在两条命令之间断开,还可能留下没有 TTL 的脏键。
官方地址:https://redis.io/docs/latest/
固定窗口限流的稳妥做法是:先把用户标识和窗口编号组成独立键,再让 Redis 原子执行计数增加与“仅首次设置过期时间”。这样后续请求只增加计数,不会不断推迟窗口结束时间。
先把“窗口”说清楚
假设接口限制为每 60 秒最多 100 次请求,至少有三种常见语义。第一种是自然时间窗口,例如 12:00:00 到 12:00:59;第二种是以用户第一次请求为起点的 60 秒窗口;第三种是每次请求都把 TTL 延长 60 秒,形成一个随流量移动的窗口。三者都能用计数器表示,但过期策略不能混用。
| 策略 | 计数键 | TTL 行为 | 适合场景 |
|---|---|---|---|
| 固定时间桶 | 包含窗口编号 | 新键首次写入时设置 | 简单、吞吐高、可接受边界突发 |
| 首次请求锚定 | 同一主体一个键 | 首次计数时设置,之后不刷新 | 希望窗口从第一次请求开始计算 |
| 滑动续期 | 同一主体一个键 | 每次请求都刷新 | 只关心“最近一段时间是否活跃” |

计数键要把主体和窗口隔离开
固定窗口的键名可以写成 rate:{user:42}:202610101015,其中最后一段是按 60 秒取整后的窗口编号。对于 Redis Cluster,花括号中的内容还可以让同一用户相关键落在同一个哈希槽;如果脚本只访问一个计数键,这个写法也便于后续扩展。
窗口编号应由应用侧根据统一时间计算,不能把本机格式化后的自然语言日期直接当作稳定协议。示例中用命令变量模拟一个已经算好的窗口编号:
# 用主体标识和窗口编号组成独立计数键,避免相邻窗口共用数字
SET rate:{user:42}:202610101015 0
# 计数器只保存整数;读取结果可以直接用于和阈值比较
INCR rate:{user:42}:202610101015
# 观察当前剩余时间,-1 表示键存在但没有过期时间
TTL rate:{user:42}:202610101015
不要把所有用户都写进一个固定键,也不要只用用户 ID 而不带窗口编号。前者无法分摊计数,后者会让窗口切换依赖“什么时候清零”,并且更容易把清零操作和请求竞争放在一起。
用一次原子执行绑定 INCR 和 EXPIRE
Redis 的 INCR 会在键不存在时从 0 开始增加,且单条命令本身是原子的。问题出在后续过期命令:单独发送两条命令时,应用可能在第一条成功后失去连接。把两步放进 Lua 脚本,Redis 会在脚本运行期间按原子语义处理它们。
-- KEYS[1] 是当前主体的计数键,ARGV[1] 是窗口保留秒数
local current = redis.call('INCR', KEYS[1])
-- 只有第一次创建计数值时设置 TTL,后续请求不改变窗口终点
if current == 1 then
redis.call('EXPIRE', KEYS[1], ARGV[1])
end
-- 返回当前计数,调用方据此判断是否超过阈值
return current
执行时把键作为 KEYS[1] 传入,把秒数作为 ARGV[1] 传入:
# 第一个参数 1 表示后面只有一个 Redis 键
# 60 表示首次计数后保留 60 秒,不会在每次请求时重新计时
EVAL "local n=redis.call('INCR',KEYS[1]); if n==1 then redis.call('EXPIRE',KEYS[1],ARGV[1]); end; return n" 1 rate:{user:42}:202610101015 60
如果脚本返回 101,说明计数已经超过 100,业务层应拒绝本次请求或返回明确的限流响应。阈值判断和计数更新最好在同一段脚本里完成,避免客户端先读后写造成并发间隙;不过脚本只负责“判定”,不能把真实业务处理也塞进 Redis。
最容易误用的是“每次都刷新 TTL”
EXPIRE 可以覆盖已有 TTL,而 INCR 不会清除键原有的过期时间。因此,下面两种写法的含义完全不同:
# 固定窗口:只在第一次计数时设置过期时间
local n = redis.call('INCR', KEYS[1])
if n == 1 then
redis.call('EXPIRE', KEYS[1], ARGV[1])
end
return n
# 滑动窗口:每一次请求都将窗口向后推迟
local n = redis.call('INCR', KEYS[1])
redis.call('EXPIRE', KEYS[1], ARGV[1])
return n
第二段并不是“更安全的固定窗口”。只要请求持续到达,键的 TTL 就持续被刷新,计数器可能长期不消失;它适合“最近 60 秒有多少活动”或“连续空闲 60 秒才重置”这类语义。若业务要的是固定 60 秒配额,就必须把刷新动作限定在首次写入。

窗口边界会带来突发流量
固定窗口的计数器很简单,但它不是精确的滑动时间窗口。一个客户端可以在 12:00:59 的最后一瞬间消耗 100 次,又在 12:01:00 的新窗口立刻消耗 100 次。两次都没有超过各自窗口的阈值,却可能在很短时间内形成接近 200 次的突发。
这不是 TTL 写错,而是固定窗口的定义本身。可以按业务风险做选择:
- 后台任务、低风险读接口:接受边界突发,使用固定窗口计数器。
- 登录、验证码、写入接口:降低单窗口阈值,并在网关或业务层增加更长周期的第二道限制。
- 需要平滑速率的接口:考虑滑动窗口、令牌桶或漏桶,不要只靠把固定窗口 TTL 改成每次刷新。
如果采用固定时间桶,可以把 TTL 设为略大于一个窗口,让旧键在切换后仍有短暂的清理余量;但 TTL 过长会增加无效键驻留时间,TTL 过短又可能让观测和故障排查变得困难。关键是让“计数有效期”和“数据清理期”有明确区分。
上线前检查这几个边界
第一,检查返回值类型和异常路径:键里如果被写入非整数文本,INCR 会返回错误,调用方应把它作为数据异常处理,而不是当作“未限流”。第二,检查脚本的运行时间,不要在脚本里遍历大量键或执行与单个主体无关的逻辑。第三,检查集群键:脚本访问的所有键都应落在同一个哈希槽,单键计数最容易满足这个约束。
第四,检查客户端超时和重试。限流脚本已经执行成功但响应丢失时,盲目重试可能把计数再加一次;调用方需要结合请求幂等性决定是否重试。第五,检查监控指标,至少区分“未超限”“达到阈值”“超过阈值”“Redis 执行错误”和“业务处理失败”。
# 用 TTL 和当前计数做低成本排查,确认窗口是否仍在按预期结束
GET rate:{user:42}:202610101015
TTL rate:{user:42}:202610101015
# 清理实验键;生产环境不要用不受控的模式匹配批量删除
DEL rate:{user:42}:202610101015
常见问题
INCR 后每次都 EXPIRE,为什么计数器不容易过期?
因为每次请求都把过期时间重新推迟了。持续流量会让键持续存活,这实现的是滑动续期语义,不是首次请求锚定的固定窗口。
只用 EXPIRE NX 能不能替代 Lua?
EXPIRE key seconds NX 能表达“没有 TTL 时才设置”,但如果 INCR 和 EXPIRE 仍是两次独立请求,客户端可能在中间失败。需要把“计数成功后设置 TTL”作为一个不可分割的动作时,应使用事务或脚本;脚本更适合同时返回计数结果。
固定窗口边界突发能不能靠延长 TTL 消除?
不能。延长 TTL 只改变键的清理时间,不会改变窗口编号和计数规则。要平滑边界,应更换算法或增加多时间尺度限制。
Redis 窗口计数限流的核心决策可以压缩成一句话:先定义窗口,再定义键,最后让计数和首次过期原子绑定。只要不把“每次刷新 TTL”误当成固定窗口,也不忽略相邻窗口叠加的边界突发,计数器就能保持简单,并且让后续的监控、扩展和算法升级都有清晰落点。
-
490 收藏
-
397 收藏
-
351 收藏
-
390 收藏
-
290 收藏
-
113 收藏
-
451 收藏
-
343 收藏
-
468 收藏
-
461 收藏
-
320 收藏
-
154 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 立即学习 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 立即学习 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 立即学习 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 立即学习 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 立即学习 485次学习