当前位置:首页 > 文章列表 > 数据库 > Redis > 用 Redis Functions 封装滑动窗口限流:部署、版本与回滚

用 Redis Functions 封装滑动窗口限流:部署、版本与回滚

来源:17golang原创 2026-10-07 11:08:51 0浏览 收藏

落地方案可以先压缩成一句话:用一个 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]请求唯一 memberreq-8f27

FCALL 的第二个参数是 key 数量。官方要求函数访问的所有键都显式作为 key 参数传入,这对单机和集群环境都很重要。本文只访问一个键,因此调用时写 FCALL sliding_allow_v1 1 ...。

把四次操作收进一个原子边界

滑动窗口调用参数、Redis Function、有序集合状态与返回契约的静态依赖结构
图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 暂时并存

Redis Functions稳定版本、候选版本与运维控制面的静态模块关系
图2:Redis Functions 并行版本的静态模块关系;应用配置可以绑定 v1 或 v2,FUNCTION LIST 与 FUNCTION DELETE 属于运维控制面,不表示发布时间线。

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 时再加入。

版本声明
本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
模糊测试只跑到少量路径,怎样改善种子语料质量模糊测试只跑到少量路径,怎样改善种子语料质量
上一篇
模糊测试只跑到少量路径,怎样改善种子语料质量
用 Fuzz 测试字符串规范化函数的往返性质
下一篇
用 Fuzz 测试字符串规范化函数的往返性质
查看更多
最新文章
查看更多
课程推荐
  • 前端进阶之JavaScript设计模式
    前端进阶之JavaScript设计模式
    设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
    543次学习
  • GO语言核心编程课程
    GO语言核心编程课程
    本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
    516次学习
  • 简单聊聊mysql8与网络通信
    简单聊聊mysql8与网络通信
    如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
    500次学习
  • JavaScript正则表达式基础与实战
    JavaScript正则表达式基础与实战
    在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
    487次学习
  • 从零制作响应式网站—Grid布局
    从零制作响应式网站—Grid布局
    本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
    485次学习
查看更多
AI推荐
  • PubMedQA数据集详解:生物医学问答基准、功能与应用指南
    PubMedQA
    深入了解PubMedQA生物医学问答数据集,涵盖其核心功能、使用方法及在临床决策、药物研发等场景的应用,助力提升NLP模型性能。
    363次使用
  • H2O EvalGPT:开源LLM大模型评估与排行榜工具
    H2O EvalGPT
    H2O EvalGPT是H2O.ai推出的开源LLM评估平台,提供详细的大模型性能排行榜、行业特定基准测试及A/B测试功能,助您快速选择最适合项目的高性能大语言模型。
    419次使用
  • LMArena是什么?伯克利AI模型评估平台使用指南与功能解析
    LMArena
    LMArena是加州大学伯克利分校推出的AI模型匿名评测平台。通过盲测投票机制,用户可对比不同大模型回答并生成实时排行榜,助力开发者优化模型及用户选择最佳AI工具。
    433次使用
  • 斯坦福HELM:大语言模型Holistic Evaluation整体评估框架详解
    HELM
    深入了解斯坦福推出的HELM(Holistic Evaluation of Language Models)大模型评测体系。本文解析其核心功能、安装配置步骤及应用场景,涵盖准确性、公平性、鲁棒性等多维度指标,助力开发者全面优化语言模型性能。
    386次使用
  • MMBench详解:多模态大模型基准测试、功能特点与使用指南
    MMBench
    MMBench是由上海人工智能实验室等机构联合推出的多模态基准测试平台,提供细粒度能力评估、大规模数据集及VLMEvalKit工具。本文详细介绍其核心功能、安装使用方法及应用场景,助力开发者全面评估多模态模型性能。
    213次使用
微信登录更方便
  • 密码登录
  • 注册账号
登录即同意 用户协议 和 隐私政策
返回登录
  • 重置密码