当前位置: 首页
数据库
Redis Lua脚本封装ZSET实现滑动窗口高精度限流

Redis Lua脚本封装ZSET实现滑动窗口高精度限流

热心网友 时间:2026-07-14
转载

滑动窗口限流需用Lua脚本保证ZSet清理、统计、写入的原子性,避免高并发竞态条件。脚本需执行ZREMRANGEBYSCORE、ZCARD、ZADD三步骤,key需区分业务上下文并显式设置过期时间,时间戳由应用层传入以防窗口漂移。

说到滑动窗口限流,绕不开 Lua 脚本——这并非锦上添花,而是底线要求。原因很简单:ZSet 的清理、统计、写入这三步操作,如果拆成多个 Redis 命令拼接,在高并发下必然翻车。先把这个结论摆在这儿,后面慢慢解释为什么。

如何使用Redis Lua脚本实现滑动窗口限流_封装ZSET操作实现高精度流量控制

为什么不能用多个 Redis 命令拼接实现滑动窗口

直接在应用层调用 ZREMRANGEBYSCOREZCARDZADD 三步操作,逻辑上看似没问题,但高并发下等着踩坑。两个请求几乎同时执行,都查到当前请求数为 99,都判断“没超限”,结果一起写入,变成了 101 次——这就是典型的竞态条件。

根子在于:ZSet 的清理、统计、写入不是原子的。网络延迟加上应用层调度间隙,让并发请求有机可乘。哪怕你在应用层加了本地锁,也只对单机有效,分布式环境照样崩溃。

  • 固定窗口(INCR + EXPIRE)只能做到“每秒重置”,无法支持“任意 1 秒内”统计
  • Pipeline 虽然能减少网络往返,但改不了多命令非原子的本质,仍然可能被其他客户端的命令插队
  • 光靠 ZCOUNT 不清理旧数据,内存持续增长,早晚撑爆

EVAL 脚本中必须包含的三个核心操作

那么,一个能用的 Lua 脚本到底要干哪几件事?三件,一件不能少:

  • ZREMRANGEBYSCORE 清理窗口外数据:范围设为 0now - window_ms,注意左闭右闭语义,避免边界时间点被重复计入两个窗口
  • ZCARD 获取当前窗口内请求数量:别绕弯子用 ZCOUNT 再算一次,ZCARD 更快、结果更确定
  • 仅当未超限时,才用 ZADD 写入新请求:score 必须是毫秒级时间戳(比如 System.currentTimeMillis()),member 建议用唯一标识(UUID 或 req_+毫秒+随机数),避免 score 相同导致覆盖

核心代码长这样:

local windowStart = now - window
redis.call('ZREMRANGEBYSCORE', key, 0, windowStart)
local current = redis.call('ZCARD', key)
if current < threshold then
redis.call('ZADD', key, now, requestId)
redis.call('EXPIRE', key, math.ceil(window / 1000) + 1)
return 1
else
return 0
end

key 设计与过期策略的隐性陷阱

key 不是随便起的名字,它直接决定了限流粒度和内存安全。这里有几个要点千万注意:

  • key 名一定要带上业务上下文,比如 rate:login:ip:192.168.1.100rate:api:/order/create:uid:12345,避免不同资源共用同一个 key 导致误限
  • 必须显式调用 EXPIRE 设置过期时间,否则空 ZSet 永远留在内存里。过期时间建议设为 window_ms / 1000 + 1 秒,比窗口稍微长一点,防止刚清空就过期,结果新请求一来又要重建
  • 别指望 ZSet 自己会“自动过期”——它不会自动删空 key,得靠 EXPIRE 老老实实地控制生命周期

漏掉 EXPIRE 是线上最容易踩的坑之一:ZSet 看上去没数据(ZCARD 返回 0),但 key 本身还占着内存,积少成多直接拖垮 Redis。这不是危言耸听。

Java 中调用 Lua 脚本的最小可行封装

Spring Boot 项目里,别手写 EVAL 字符串拼接了,用 DefaultRedisScript 封装一次,到处复用:

  • 把脚本文件(sliding_window.lua)放在 resources/ 下,内容保持纯 Lua,不带 Java 变量插值
  • 声明 DefaultRedisScript bean,resultType 设为 Long.class,因为返回值就是 1 或 0
  • 调用时传入 KEYS[1](key)、ARGV[1](当前毫秒时间戳)、ARGV[2](窗口毫秒数)、ARGV[3](阈值)、ARGV[4](requestId)

调用代码大概长这样:

redisTemplate.execute(limitScript,
Collections.singletonList(key),
String.valueOf(now),
String.valueOf(windowMs),
String.valueOf(maxCount),
requestId);

注意:now 必须由应用层传入(而不是在 Lua 里用 redis.call("TIME")),否则各节点系统时间不一致会导致窗口漂移。这个细节很多人忽略,等到出问题才后悔。

说到底,真正难的从来不是写对那几行 Lua,而是想清楚 key 怎么分、时间戳谁来提供、过期时间设多长、member 怎么防冲突——这些细节不抠准,脚本再漂亮,上线就是事故。

来源:https://www.php.cn/faq/2753726.html

游乐网为非赢利性网站,所展示的游戏/软件/文章内容均来自于互联网或第三方用户上传分享,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系youleyoucom@outlook.com。

同类文章
更多
PostgreSQL触发器实现分表逻辑自动路由

PostgreSQL触发器实现分表逻辑自动路由

在PostgreSQL中使用触发器实现分表路由时,必须使用BEFOREINSERT拦截写入。继承模型通过RETURNNULL取消主表插入并手动写入子表;声明式分区需确保目标分区存在后RETURNNEW。动态建表时INHERITS必须带空括号,分区键值需严格校验避免误路由,高并发下需用异常捕获处理建表竞态。

时间:2026-07-14 07:13
MySQL 8.0并行查询索引统计不准确解决方案

MySQL 8.0并行查询索引统计不准确解决方案

MySQL8 0不支持真正并行查询,索引统计不准源于高并发写入与采样缺陷。可通过直方图绕过采样问题,并检查innodb_stats_persistent、采样页数等参数。分区表需指定分区分析,同时排查长事务以从根本上解决问题。

时间:2026-07-14 07:13
MySQL报错包过大超限的解决方法

MySQL报错包过大超限的解决方法

解决MySQL报错需同步调大服务端和客户端max_allowed_packet参数。通过SETGLOBAL或配置文件修改服务端,客户端命令行、JDBC等也要相应设置。注意权限、配置格式及应用层可能覆盖。云数据库需通过控制台修改。

时间:2026-07-14 07:13
如何不停业务在线开启MySQL GTID模式

如何不停业务在线开启MySQL GTID模式

MySQL5 7+在线开启GTID需三步:先设为OFF_PERMISSIVE等待所有事务完成,再切换为ON_PERMISSIVE,最后设为ON,所有节点需同步依次执行。需确保enforce_gtid_consistency为ON且无匿名事务残留。集成环境如phpEnv必须修改配置文件my ini并重启。主从初始化时使用--set-gtid-purged=ON

时间:2026-07-14 07:12
MySQL 5.7带减号数据库名授权失败解决方法

MySQL 5.7带减号数据库名授权失败解决方法

数据库名含减号需用反引号包裹库名;授权时指定`库名` *,用户须拥有WITHGRANTOPTION才能转授权限,最后执行FLUSHPRIVILEGES强制重载权限表。

时间:2026-07-14 07:12
热门专题
更多
刀塔传奇破解版无限钻石下载大全 刀塔传奇破解版无限钻石下载大全
洛克王国正式正版手游下载安装大全 洛克王国正式正版手游下载安装大全
思美人手游下载专区 思美人手游下载专区
好玩的阿拉德之怒游戏下载合集 好玩的阿拉德之怒游戏下载合集
不思议迷宫手游下载合集 不思议迷宫手游下载合集
百宝袋汉化组游戏最新合集 百宝袋汉化组游戏最新合集
jsk游戏合集30款游戏大全 jsk游戏合集30款游戏大全
宾果消消消原版下载大全 宾果消消消原版下载大全
  • 热门数据榜