RateLimiter默认用EVAL动态执行Lua脚本,导致Redis集群下CROSSSLOT错误、网络开销大;需手动预加载脚本并用evalsha调用,配合{rate}哈希标签和NOSCRIPT降级机制保障可靠性。

网关层做 Redis 限流,RateLimiter 类库本身不直接支持预加载 Lua 脚本,它默认用 EVAL 动态执行;想真正实现「预置脚本 + evalsha」必须绕过它的封装,自己控制脚本加载与执行流程。
为什么 RateLimiter 的默认 Lua 执行不可靠
Spring Cloud Gateway 或常见网关 SDK 中集成的 RateLimiter(比如基于 Redis 的 RedisRateLimiter)底层会调用一段固定 Lua 脚本做原子计数+过期设置。但问题在于:
- 它每次都是用
EVAL发送完整脚本字符串,不走script load+evalsha流程 - Redis 集群模式下,
EVAL要求所有 key 在同一个 slot,而限流 key 若含动态前缀(如ip:192.168.1.100),容易触发CROSSSLOT错误 - 高频请求下反复传输相同脚本,增加网络和序列化开销
手动预加载 Lua 脚本并替换 RateLimiter 执行逻辑
核心是:不依赖 RateLimiter 自带的脚本执行器,改用 StringRedisTemplate.execute() 显式调用已加载脚本的 SHA1。
实操要点:
- 在 Spring Boot 启动时,用
redisTemplate.getConnectionFactory().getClusterConnection().scriptLoad(...)加载你的限流 Lua 脚本,保存返回的 SHA1 值 - 自定义一个
RedisScript实例,setScriptSource设为空,setSha1设为上一步拿到的值 - 重写网关的
RateLimiter实现(如继承AbstractRateLimiter),在isAllowed()中调用stringRedisTemplate.execute(script, keys, args) - Lua 脚本里必须用
KEYS[1]表示限流 key,ARGV[1]表示窗口秒数,ARGV[2]表示最大请求数,ARGV[3]表示当前时间戳(毫秒)
示例 Lua 关键片段(用于滑动窗口):
local key = KEYS[1]
local window = tonumber(ARGV[1])
local max = tonumber(ARGV[2])
local now = tonumber(ARGV[3])
local window_start = now - window * 1000
redis.call('ZREMRANGEBYSCORE', key, 0, window_start)
local count = tonumber(redis.call('ZCARD', key))
if count <h3>Key 设计必须规避 CROSSSLOT 问题</h3><p>网关限流 key 如果直接拼接 IP 或用户 ID,例如 <code>rate:ip:192.168.1.100</code>,在 Redis Cluster 下可能分散到不同节点——Lua 脚本无法跨 slot 操作。</p><div class="aritcle_card flexRow artxards">
<div class="artcardd flexRow">
<a class="aritcle_card_img" rel="nofollow" href="/xiazai/skill11046" title="Sentinal Redis"><img
src="https://img.php.cn/upload/skill/001/246/273/6adaf20dbae7c507.jpg" alt="Sentinal Redis" onerror="this.onerror='';this.src='/static/lhimages/moren/morentu.png'" ></a>
<div class="aritcle_card_info flexColumn">
<a rel="nofollow" href="/xiazai/skill11046" title="Sentinal Redis" class="overflowclass">Sentinal Redis</a>
<p class="overflowclass">监控Redis服务器健康状况、内存、性能及BullMQ队列。检查队列深度、失败任务,分析慢查询并诊断问题。</p>
</div>
<a rel="nofollow" href="/xiazai/skill11046" title="Sentinal Redis" class="aritcle_card_btn flexRow flexcenter"><b></b><span>下载</span>
</a>
</div>
</div><p>解决办法只有两个:</p>
- 强制所有限流 key 落在同一 slot:在 key 前加
{rate}前缀,如{rate}:ip:192.168.1.100,Redis 会只对花括号内内容哈希 - 改用单节点 Redis(仅限小规模或测试环境),绕过集群限制
注意:{rate} 不是业务前缀,是 Redis Cluster 的哈希标签(hash tag),必须成对、无嵌套、且不能含空格或特殊字符。
预加载失败时的 fallback 必须显式处理
如果脚本未成功预加载,evalsha 会抛出 NOSCRIPT 异常。网关不能因此挂掉,必须捕获并降级为 EVAL。
关键判断逻辑:
- 检查异常消息是否包含
"NOSCRIPT" - 若匹配,改用
stringRedisTemplate.execute(new DefaultRedisScript(...), keys, args)直接传脚本字符串 - 同时记录 warn 日志,并触发一次后台重加载尝试(避免后续所有请求都走
EVAL)
这个 fallback 很容易被忽略,但生产环境 Redis 集群重启、分片迁移后,脚本缓存丢失是常态。










