decr不能直接用于秒杀扣减,因为它仅保证单key原子性,而秒杀需“检查库存是否充足+扣减”两个动作原子执行;若先get再decr,高并发下必然超卖;lua脚本在redis服务端原子执行,可安全实现判断、扣减、返回一体化逻辑。

为什么不能直接用 DECR 做秒杀扣减
因为 DECR 只能保证单 key 的原子性,但秒杀需要「检查库存是否充足 + 扣减」两个动作原子执行。如果先 GET 再 DECR,并发下必然超卖——这是最典型的竞态条件。Lua 脚本在 Redis 中以原子方式执行,整个脚本运行期间不会被其他命令打断,这才是安全前提。
EVAL 脚本里必须做三件事
一个可靠的秒杀扣减 Lua 脚本至少要完成:检查库存是否 > 0、扣减库存、返回扣减结果(成功/失败)。不能省略任何一步,也不能把判断逻辑放在客户端。
- 用
redis.call("GET", KEYS[1])拿当前库存,注意返回值是字符串,需用tonumber()转换 - 判断前先检查是否为
nil(比如 key 不存在),否则tonumber(nil)得到nil,后续比较会出错 - 扣减后建议立即用
redis.call("SET", KEYS[1], new_stock)回写(尤其当初始值可能不是整数时),避免浮点精度或类型隐式转换问题 - 返回值统一用数字:1 表示成功扣减,0 表示库存不足,不要返回字符串,方便客户端解析
示例脚本:
Redis 缓存和数据结构管理技能。通过自然语言操作 Redis,支持 String、Hash、List、Set、ZSet、Stream 等数据结构操作。当用户提到 Redis、缓存、消息队列、会话存储时使用此技能。
if redis.call("EXISTS", KEYS[1]) == 0 then
return 0
end
local stock = tonumber(redis.call("GET", KEYS[1]))
if stock >= 1 then
redis.call("DECR", KEYS[1])
return 1
else
return 0
end
调用时 KEYS 和 ARGV 别传错位置
Redis 的 Lua 执行上下文里,KEYS 是必须的键名数组(由 EVAL 第二个参数指定长度),ARGV 是额外参数。秒杀场景通常只需传库存 key,所以 KEYS[1] 就是库存 key,ARGV 一般用不上。如果误把数量塞进 ARGV 并在脚本里当成扣减量,就失去了幂等性和预期语义——秒杀就是扣 1,不该由客户端决定扣多少。
- 正确调用:
EVAL "script" 1 stock_key - 错误写法:
EVAL "script" 0 stock_key 1(此时KEYS长度为 0,KEYS[1]访问越界,报错ERR Error running script (call to f_...): @user_script:1: attempt to index a nil value) - 别在脚本里硬编码 key 名,否则无法复用;也别用
redis.call("KEYS", "*"),这在集群模式下不支持且性能差
集群环境下 EVAL 报 CROSSSLOT 错误怎么办
Redis Cluster 要求脚本中所有 key 必须落在同一个 slot。秒杀库存 key 如果没做 hash tag,很可能分散在不同节点。最简单有效的解法:给 key 加花括号强制路由到同一 slot,比如把 seckill:stock:1001 改成 seckill:{stock}:1001,这样 {stock} 决定 slot,所有同类库存 key 都会落到同一分片。
- 确认是否集群:执行
CLUSTER INFO,看cluster_state:ok且cluster_known_nodes> 1 - 检查 key slot:
CLUSTER KEYSLOT seckill:stock:1001,对比多个 key 是否一致 - 不改 key 名的话,只能改用
EVALSHA+SCRIPT LOAD配合客户端重试,但复杂度高,不如从 key 设计入手
真正难的不是写对脚本,而是让所有相关 key 在集群中可预测地共存于一个 slot,这点容易被忽略,上线后才暴露。










