直接替换evalsha脚本哈希会出问题,因为redis缓存哈希后不再校验脚本内容,导致旧客户端突然执行不兼容的新逻辑;灰度必须在服务端基于key路由,而非客户端判断或简单取模。

为什么直接替换 EVALSHA 脚本哈希会出问题
Redis 的 Lua 脚本通过 EVAL 或 EVALSHA 执行,服务端缓存脚本哈希后不再校验内容。一旦你用新逻辑覆盖旧脚本(比如用新 SHA 替换旧 SHA 对应的逻辑),所有正在调用 EVALSHA 的客户端会突然执行不兼容的新行为——没有过渡、无法回滚、错误难以归因。
灰度发布的本质不是“换脚本”,而是“按 Key 分流到不同版本的脚本入口”。关键在于:路由决策必须在 Redis 侧完成,且不能依赖客户端传额外参数(否则灰度开关就暴露在外、不可控)。
- 常见错误:在应用层判断 key 是否命中灰度规则,再决定调用
EVALSHA_v1还是EVALSHA_v2—— 这会导致客户端逻辑膨胀、版本耦合、灰度比例难精确控制 - 更糟的情况:用
HASH_SLOT(key) % N做简单分流,但 Redis 集群 slot 分布和业务 key 模式可能让灰度流量严重倾斜
用 KEYS[1] 提取业务标识做路由判断
Lua 脚本内可安全读取 KEYS[1](前提是调用时第一个参数确实是业务 key),并基于其结构提取灰度标识,比如:
-- 示例:key 格式为 "user:1001:profile" 或 "user:1001:profile:v2" local key = KEYS[1] local version = string.match(key, ":v(%d+)$") or "1" if version == "2" then -- 执行新版逻辑 else -- 执行旧版逻辑 end
这种方式把路由逻辑下沉到脚本内部,客户端完全无感;灰度只需批量重命名 key(如加 :v2 后缀)或写入时指定新格式,无需改任何调用代码。
- 注意:不能依赖
ARGV传版本号——它不参与 script cache 计算,且容易被客户端误传、绕过灰度控制 - 避免用
string.find或正则匹配复杂结构,Lua 正则性能差,高频脚本里可能成为瓶颈 - 如果 key 本身不含版本信息,可在调用前用
EXISTS user:1001:profile:gray_flag查一次,但会多一次 Redis 往返,慎用于高吞吐场景
用 SCRIPT LOAD + 版本化脚本名隔离缓存
Redis 不允许删除已加载脚本,但你可以让不同版本脚本拥有独立哈希——只要内容不同,SCRIPT LOAD 就返回新 SHA。关键是:不要复用同一份 Lua 源码字符串。
推荐做法是,在源码头部插入唯一注释标记版本:
-- v2.1.0-20240520-gray local key = KEYS[1] -- ... 实际逻辑
每次发布新版,更新这个注释再 SCRIPT LOAD,得到新 SHA。旧脚本仍可用,新老版本共存,灰度期间两者都有效。
- 务必记录每个 SHA 对应的版本号和上线时间,排查时靠
SCRIPT DEBUG或日志反查很麻烦 - 不要手动拼接 SHA 字符串传给
EVALSHA——容易输错,建议用服务启动时SCRIPT LOAD并缓存返回值 - 集群模式下,
SCRIPT LOAD需发往所有分片(或确保目标 key 所在 slot 的节点),否则部分节点缺失脚本会报NOSCRIPT
灰度比例控制别硬编码在 Lua 里
想按 5% 流量切新版?别在脚本里写 math.random() ——这会让同一个 key 多次执行结果不一致(违反 Redis 脚本原子性前提),也难审计、难关闭。
真正可控的方式是:把灰度开关存在 Redis 自身,用原子操作读取:
local flag_key = "script_gray:user_profile:v2"
local enabled = redis.call("GET", flag_key) == "1"
if enabled then
-- 新逻辑
else
-- 旧逻辑
end
然后通过外部配置中心或运维命令统一设置 flag_key 的值,支持秒级生效、按 key 前缀分级开关、甚至结合用户 ID 做一致性哈希分流(redis.call("HGET", "gray_user_hash", math.fmod(tonumber(userid), 100)) == "1")。
- 注意
GET是 O(1),但频繁读配置 key 可能成为瓶颈;可加本地缓存(如脚本内用local cached = _G.gray_flag_cache),不过需权衡内存占用和时效性 - 绝对不要用
TIME或redis.call("TIME")做灰度窗口判断——集群各节点时间不同步,结果不可靠
灰度机制最易被忽略的点是:脚本变更后,旧 key 对应的旧逻辑是否还被其他地方(比如定时任务、离线 job)隐式依赖。上线前得确认所有访问路径都经过你控制的路由入口,否则会出现“一部分请求走新版、另一部分绕过灰度直接走旧版”的撕裂状态。











