不能直接用setnx做幂等,因其仅校验key存在性,无法比对请求参数一致性;必须结合lua脚本原子性地校验哈希值并设过期时间,否则相同业务id但不同payload的请求会被错误放行。

为什么不能直接用 SETNX 做幂等?
因为 SETNX 只能判断 key 是否存在,无法同时校验请求参数是否一致。比如两次相同业务 ID 但不同 payload 的请求,SETNX 会放行,导致重复执行;而真正需要的,是「同一业务标识 + 同一内容」才拒绝第二次。Lua 脚本能原子性地读、算、写,正好补上这个缺口。
Lua 脚本里怎么安全比对和存值?
核心逻辑是:拼接业务唯一键(如 "idempotent:{order_id}")作为主 key,把请求体哈希(如 sha256(payload))存为 value,并设过期时间。脚本需检查 key 是否存在,若存在则比对 value 是否一致;一致就返回 0(已存在),不一致就报错或拒绝(避免覆盖误判)。注意以下几点:
- 不要在 Lua 中调用
redis.call("GET", KEYS[1])后再用 Lua 做字符串比较——这会把数据拉到客户端,失去原子性;应改用redis.call("GET", KEYS[1]) == ARGV[1] - 必须用
ARGV[1]传入哈希值,不能拼在脚本里硬编码,否则每次变更都要重载脚本 - 过期时间建议通过
ARGV[2]传入,便于不同场景控制生命周期(如支付类 24h,日志类 1h)
示例脚本片段:
if redis.call("EXISTS", KEYS[1]) == 1 then
if redis.call("GET", KEYS[1]) == ARGV[1] then
return 0 -- 已存在且内容一致
else
return -1 -- 内容冲突,拒绝
end
else
redis.call("SETEX", KEYS[1], ARGV[2], ARGV[1])
return 1 -- 新建成功
end
客户端调用时怎么传参才不出错?
Redis 客户端调用 EVAL 或 EVALSHA 时,KEYS 和 ARGV 必须严格对应脚本里的索引。常见翻车点:
Redis 缓存和数据结构管理技能。通过自然语言操作 Redis,支持 String、Hash、List、Set、ZSet、Stream 等数据结构操作。当用户提到 Redis、缓存、消息队列、会话存储时使用此技能。
- 把哈希值当 KEY 传(即写成
KEYS[2]),导致redis.call("GET", KEYS[1])报错 “WRONGTYPE” - 没对 payload 做标准化处理(比如 JSON 字段顺序不固定、空格/换行不一致),导致两次相同语义的请求生成不同哈希
- 忘记设置超时时间,或传了字符串如
"3600"却在脚本里直接用于SETEX(它接受整数,字符串会触发 error) - 使用
EVALSHA但没提前SCRIPT LOAD,结果返回NOSCRIPT错误
推荐在客户端做一步预处理:对 payload 先 json.Marshal(Go)或 JSON.stringify(JS)并排序键名,再取 sha256,最后连同业务 ID 一起传入。
性能和边界情况要注意什么?
单次 Lua 执行是原子的,但高并发下仍可能因网络延迟或客户端重试造成“伪重复”。真实系统中必须配合外部机制:
- Redis 集群环境下,所有 KEY 必须落在同一个 slot,所以业务 ID 要加
{...}包裹(如"{idempotent:order_123}"),否则EVAL报错CROSSSLOT - 如果脚本返回
-1(内容冲突),不应静默失败,而要记录告警——这往往意味着上游重复发包或参数生成逻辑异常 - 不要依赖 TTL 精确清理;高峰期大量 key 过期可能引发 Redis 主动删除压力,建议配合后台定时扫描 +
SCAN清理陈旧 key
最易被忽略的一点:Lua 脚本里不能用 os.time() 或 math.random(),Redis 的 Lua 环境禁用了这些函数。需要时间戳必须由客户端传入 ARGV[n]。










