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 < max then
redis.call('ZADD', key, now, now)
redis.call('EXPIRE', key, window + 1)
return {1, count + 1}
else
return {0, count}
endKey 设计必须规避 CROSSSLOT 问题
网关限流 key 如果直接拼接 IP 或用户 ID,例如 rate:ip:192.168.1.100,在 Redis Cluster 下可能分散到不同节点——Lua 脚本无法跨 slot 操作。
解决办法只有两个:
- 强制所有限流 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 集群重启、分片迁移后,脚本缓存丢失是常态。


















