用 Redis Functions 封装滑动窗口限流:部署、版本与回滚
落地方案可以先压缩成一句话:用一个 Sorted Set 保存窗口内的请求,把“删掉过期成员、统计当前数量、决定是否放行、写入新请求”封装进 Redis Function;部署时让 sliding_allow_v1 与 sliding_allow_v2 使用不同名称并行存在,应用只切换函数名,回滚时再切回 v1。
这个设计同时解决两个问题。数据面上,四个 Redis 命令在一次原子函数执行中完成,不会被其他客户端插入;发布面上,稳定版本不会被候选版本直接覆盖,回滚不需要现场重写 Lua。
背景:EVAL 能运行,但不适合承担版本管理
Redis 官方文档指出,Redis Functions 从 Redis 7.0 开始提供。传统 EVALSHA 依赖脚本缓存,而缓存可能在 SCRIPT FLUSH、服务重启或副本故障转移后丢失,应用需要在运行时处理脚本重载。函数库则是数据库的一等软件制品,会随数据持久化并复制到副本,应用只依赖已注册的函数 API。
这并不意味着 EVAL 不能做限流,而是职责不同:临时脚本适合轻量、由应用携带的逻辑;Functions 更适合需要统一部署、命名、观察和回滚的服务端能力。官方还明确说明,函数执行具有原子性,并在执行期间阻塞服务器,所以函数必须短小,不能把慢查询或无界循环塞进去。
先固定调用契约,再写 Lua
本文只访问一个限流键,键名通过 KEYS[1] 传入。其余参数放在 ARGV:当前毫秒时间、窗口毫秒数、窗口配额、请求唯一标识。返回值是两个整数:第一个表示是否放行,第二个表示本次判断后的剩余额度。
| 位置 | 含义 | 示例 |
|---|---|---|
| KEYS[1] | 限流维度键 | rl:{user:42}:login |
| ARGV[1] | 当前时间,毫秒 | 1791342000000 |
| ARGV[2] | 窗口长度,毫秒 | 60000 |
| ARGV[3] | 窗口内最大请求数 | 5 |
| ARGV[4] | 请求唯一 member | req-8f27 |
FCALL 的第二个参数是 key 数量。官方要求函数访问的所有键都显式作为 key 参数传入,这对单机和集群环境都很重要。本文只访问一个键,因此调用时写 FCALL sliding_allow_v1 1 ...。
把四次操作收进一个原子边界

Sorted Set 的 score 使用毫秒时间戳,member 使用请求唯一标识。每次调用先删除 score 小于等于窗口左边界的成员,再用 ZCARD 读取窗口内数量。达到配额就拒绝;未达到配额才写入当前请求,并刷新键的过期时间。
#!lua name=rate_limit_v1
-- 封装单个限流键的滑动窗口判断
local function sliding_allow(keys, args)
local key = keys[1]
local now_ms = tonumber(args[1])
local window_ms = tonumber(args[2])
local limit = tonumber(args[3])
local member = args[4]
-- 拒绝缺失参数和非正数配置,避免产生不可控键
if not key or not now_ms or not window_ms or not limit or not member then
return redis.error_reply('ERR invalid arguments')
end
if window_ms = limit then
return {0, 0}
end
-- member 必须由调用方保证唯一,否则 ZADD 会更新旧成员
redis.call('ZADD', key, now_ms, member)
redis.call('PEXPIRE', key, window_ms)
return {1, limit - count - 1}
end
-- 注册带版本号的函数名,给并行部署和回滚留下空间
redis.register_function('sliding_allow_v1', sliding_allow)
这里没有先 ZADD 再判断,因为被拒绝的请求不应该进入集合。PEXPIRE 让长期无人访问的限流键自动清理;活跃键则由每次成功请求刷新 TTL。函数执行是原子的,但原子不等于零成本:清理大量过期成员仍会占用服务器时间,因此窗口、调用频率和单键基数都要有上限。
首次部署:加载库、查看注册结果、再调用
把上面的源码保存为 rate_limit_v1.lua。库名来自首行元数据 #!lua name=rate_limit_v1,函数名来自 redis.register_function。两者不要混淆:FUNCTION DELETE 接受库名,FCALL 接受函数名。
# 从标准输入加载完整函数库,成功时返回库名 rate_limit_v1 redis-cli -x FUNCTION LOAD
返回 1, 4 表示本次放行,窗口还剩 4 个名额;返回 0, 0 表示拒绝。生产客户端应把这两个整数映射成明确的数据结构,不要依赖日志文本做判断。
版本不要覆盖:让 v1 和 v2 暂时并存

FUNCTION LOAD REPLACE 可以整体替换同名库,但它更适合明确接受原地升级的场景。限流属于入口保护能力,我更倾向于让候选版本使用新库名 rate_limit_v2 和新函数名 sliding_allow_v2。Redis 的函数名是全局唯一的,因此只改库名、不改函数名,仍会产生冲突。
v2 的注册位置可以保持和 v1 相同的回调结构,只修改库名与函数名;算法变化则写在 v2 文件内部。加载后同时查看两套库,确认它们并存。
# 加载候选库,文件首行应声明 name=rate_limit_v2 redis-cli -x FUNCTION LOAD
应用配置中保存函数名,例如 RATE_LIMIT_FUNCTION=sliding_allow_v2,比把 Lua 源码或 SHA1 放进每个应用实例更容易审计。灰度期间 v1 与 v2 访问同一个限流键时,必须保持数据结构和 member 语义兼容;如果 v2 改变存储格式,就应使用新的 key 命名空间,避免两种实现互相破坏数据。
回滚:先切回调用,再删除候选库
并行版本的价值在这里最明显。发现 v2 的拒绝率、延迟或错误码异常时,把应用配置切回 sliding_allow_v1,不需要重新加载旧源码。确认所有实例都不再调用 v2 后,再删除候选库。
# 应用先恢复调用 sliding_allow_v1,再确认两套库仍存在 redis-cli FUNCTION LIST LIBRARYNAME 'rate_limit_v*' # 确认无调用方依赖后,按库名删除候选版本及其函数 redis-cli FUNCTION DELETE rate_limit_v2
顺序不能反过来。先删除 v2、再等待应用配置生效,会让仍在使用旧配置的实例收到“函数不存在”错误。对于多实例应用,配置下发状态和实际调用指标应成为删除前置条件。
库备份不是版本切换,但能补上灾难恢复
FUNCTION DUMP 会导出全部函数库的二进制载荷,FUNCTION RESTORE 可以用该载荷恢复,并支持 FLUSH、APPEND、REPLACE 策略。它适合备份迁移,不适合代替日常 v1/v2 灰度,因为载荷覆盖的是函数库集合,而不是单个函数。
from pathlib import Path
import redis
# 连接目标 Redis,生产环境应从受控配置读取连接信息
client = redis.Redis(host="127.0.0.1", port=6379, decode_responses=False)
# 保存完整函数库二进制载荷,不能按文本方式改写
payload = client.execute_command("FUNCTION", "DUMP")
Path("redis-functions.dump").write_bytes(payload)
# 灾难恢复时读取原始字节,并按 REPLACE 策略恢复同名库
saved = Path("redis-functions.dump").read_bytes()
client.execute_command("FUNCTION", "RESTORE", saved, "REPLACE")
恢复命令会改变目标实例的函数库集合,应该在明确的运维窗口中执行。日常回滚仍优先使用并行版本加应用配置切换,因为影响范围更小,也更容易观察。
上线前必须说清的六个边界
- Redis 版本:Functions 从 Redis 7.0 开始可用;更低版本不能直接使用本文命令。
- 唯一 member:同一个 member 再次
ZADD会更新 score,而不是增加一条记录。请求 ID 必须在限流窗口内唯一。 - 时间来源:示例由调用方传入毫秒时间。多台网关要同步时钟,并拒绝明显超前或回退的时间值。
- Cluster 键:函数访问的键必须通过 key 参数显式传入。本文只有一个键;扩展到多键时还要保证相关键位于可执行的同一哈希槽。
- 运行时成本:
ZREMRANGEBYSCORE的成本与被删除成员数有关。不要用一个全局键承载所有用户,也不要设置无界窗口。 - TTL 语义:示例只在成功写入时刷新 TTL。若业务要求拒绝请求也延长观察期,需要明确修改策略,并评估热键长期驻留的影响。
采用建议
如果限流逻辑只是一次性实验,EVAL 足够直接;如果它已经是多个应用共享的入口能力,Redis Functions 更适合承担部署单元。实现层面坚持单键、短函数、有界集合;发布层面坚持库名和函数名都带版本,候选版本与稳定版本并存;回滚层面坚持先切调用、后删库。
最重要的不是把 Lua 写得更复杂,而是把数据契约和版本契约同时固定下来:KEYS 决定函数允许访问什么,ARGV 决定调用参数,函数名决定应用调用哪个版本,库名决定运维删除哪个部署单元。这四层边界清楚后,滑动窗口限流才真正具备可维护性。
延伸问题
为什么不用固定窗口 INCR?
固定窗口计数更省内存,但窗口边界附近可能出现突发翻倍。滑动窗口保留每次请求时间,判断更平滑,代价是 Sorted Set 存储和清理成本更高。
能不能直接用 FUNCTION LOAD REPLACE 发布 v2?
可以,但同名库会被整体替换,旧实现不再作为独立部署单元保留。对需要快速回滚的入口能力,并行库名和函数名通常更稳妥。
限流函数能返回重试时间吗?
可以在拒绝时读取窗口内最早成员的 score,并计算其离开窗口的时间。但这会增加一次读取和返回契约复杂度;只有客户端确实需要精确 Retry-After 时再加入。
模糊测试只跑到少量路径,怎样改善种子语料质量
- 上一篇
- 模糊测试只跑到少量路径,怎样改善种子语料质量
- 下一篇
- 用 Fuzz 测试字符串规范化函数的往返性质
-
- 数据库 · Redis | 30分钟前 | Redis · 消息订阅 消费者组 XREADGROUP XACK Redis Streams Redis Pub/Sub
- Pub/Sub 与 Streams 不只是是否持久化:订阅模型怎么选
- 220浏览 收藏
-
- 数据库 · Redis | 4小时前 | Redis · 分页 · 缓存设计 · 排行榜 · Sorted Set · ZREVRANK Redis Sorted Set 排行榜 同分排名 排行榜翻页 历史榜单
- Sorted Set 排行榜如何处理同分、翻页与历史榜单
- 189浏览 收藏
-
- 数据库 · Redis | 9小时前 | Redis · 消息队列 · 消息重试 XPENDING XAUTOCLAIM Redis Streams XTRIM 消费者组积压
- Streams 消费者组积压怎么处理:认领、重试与裁剪
- 324浏览 收藏
-
- 数据库 · Redis | 11小时前 |
- Redis Vector Set 做语义检索:向量、元数据与过滤条件
- 195浏览 收藏
-
- 数据库 · Redis | 14小时前 | Redis ·
- Redis MEMORY STATS 里的 allocator_frag_ratio 怎么理解
- 348浏览 收藏
-
- 数据库 · Redis | 17小时前 | Redis · Redis ACL ACL DRYRUN ACL SETUSER Redis权限测试 键模式
- Redis ACL DRYRUN 怎么在授权前测试一条命令
- 288浏览 收藏
-
- 数据库 · Redis | 19小时前 |
- Redis SLOWLOG 和 LATENCY DOCTOR 应该分别看什么
- 208浏览 收藏
-
- 数据库 · Redis | 21小时前 |
- Redis 键空间通知为什么收不到过期事件
- 291浏览 收藏
-
- 数据库 · Redis | 23小时前 | lua · redis Redis Functions FUNCTION LOAD FCALL 版本发布
- Redis Functions 怎么用 FCALL 调用版本化逻辑
- 149浏览 收藏
-
- 数据库 · Redis | 1天前 | Redis · redis CLIENT TRACKING BCAST 客户端缓存 OPTIN INVALIDATE
- Redis 客户端缓存怎么用 TRACKING 避免脏读
- 115浏览 收藏
-
- 数据库 · Redis | 1天前 | redis Redis Cluster 哈希标签 多键操作 槽位
- Redis Cluster 哈希标签怎么让多键操作落在同一槽
- 463浏览 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 485次学习
-
- PubMedQA
- 深入了解PubMedQA生物医学问答数据集,涵盖其核心功能、使用方法及在临床决策、药物研发等场景的应用,助力提升NLP模型性能。
- 363次使用
-
- H2O EvalGPT
- H2O EvalGPT是H2O.ai推出的开源LLM评估平台,提供详细的大模型性能排行榜、行业特定基准测试及A/B测试功能,助您快速选择最适合项目的高性能大语言模型。
- 419次使用
-
- LMArena
- LMArena是加州大学伯克利分校推出的AI模型匿名评测平台。通过盲测投票机制,用户可对比不同大模型回答并生成实时排行榜,助力开发者优化模型及用户选择最佳AI工具。
- 433次使用
-
- HELM
- 深入了解斯坦福推出的HELM(Holistic Evaluation of Language Models)大模型评测体系。本文解析其核心功能、安装配置步骤及应用场景,涵盖准确性、公平性、鲁棒性等多维度指标,助力开发者全面优化语言模型性能。
- 386次使用
-
- MMBench
- MMBench是由上海人工智能实验室等机构联合推出的多模态基准测试平台,提供细粒度能力评估、大规模数据集及VLMEvalKit工具。本文详细介绍其核心功能、安装使用方法及应用场景,助力开发者全面评估多模态模型性能。
- 213次使用
-
- 关于golang高并发的实现与注意事项说明
- 2023-01-07 497浏览
-
- 基于Golang 高并发问题的解决方案
- 2023-02-24 342浏览
-
- golang高并发限流操作 ping / telnet
- 2023-01-28 288浏览
-
- golang-gin-mgo高并发服务器搭建教程
- 2023-01-28 178浏览
-
- Go语言中通过Lua脚本操作Redis的方法
- 2023-01-07 234浏览

