直接用decr不能安全扣库存,因其仅保证原子性而不校验库存是否充足,导致库存为0时继续扣减变为负数造成超卖;必须用lua脚本在服务端原子执行“读-判-写”三步逻辑。

为什么直接用 DECR 不能安全扣库存
因为 DECR 只保证原子性,不检查业务逻辑边界。比如库存为 0 时再 DECR 会变成 -1,这不是“扣减失败”,而是“超卖”。真实场景需要「原子地判断 + 扣减 + 返回结果」三步合一。
用 EVAL 执行 Lua 脚本实现原子校验扣减
Redis 6.0 仍依赖 Lua 脚本来封装条件逻辑,EVAL 是唯一能在一个请求中完成「读-判-写」的机制。脚本必须把 key、期望最小值、扣减量作为参数传入,避免硬编码。
- 脚本示例:
EVAL "if tonumber(redis.call('GET', KEYS[1])) >= tonumber(ARGV[1]) then return redis.call('DECRBY', KEYS[1], ARGV[2]) else return -1 end" 1 stock_key 1 1 -
KEYS[1]是库存 key,ARGV[1]是最小允许值(如 1),ARGV[2]是扣减量 - 返回
-1表示库存不足,非负数表示扣减后剩余值 - 注意:Lua 中
redis.call()返回的是字符串,必须用tonumber()转换后再比较
为什么不用 SET stock_key new_val NX 替代
NX 只能防止覆盖,无法基于旧值做条件更新。你得先 GET 再算新值再 SET,中间存在竞态窗口——两次网络往返之间,其他客户端可能已修改该 key。
Redis 缓存和数据结构管理技能。通过自然语言操作 Redis,支持 String、Hash、List、Set、ZSet、Stream 等数据结构操作。当用户提到 Redis、缓存、消息队列、会话存储时使用此技能。
- 即使加
WATCH+MULTI/EXEC,在高并发下失败重试成本高,且 Redis 6.0 的乐观锁在争抢激烈时成功率急剧下降 -
EVAL脚本在服务端执行,全程无上下文切换,是目前最稳的方案 - 别信“用
INCRBY配合负数”的说法——它照样会越界,没做下限检查
实际部署要注意的三个细节
脚本看似简单,但上线后容易在边界和运维上翻车。
- 脚本不能含
redis.replicate_commands()以外的随机操作(如math.random),否则主从同步可能出错 - 库存 key 建议带业务前缀(如
order:stock:1001),避免不同商品共用 key 导致误扣 - 首次运行前用
EXISTS确保 key 存在,或用SET stock_key 100 NX EX 86400初始化并设过期,否则GET返回 nil 会让tonumber(nil)得到 0,造成逻辑错乱
真正难的不是写对一行 EVAL,而是确认所有客户端都走同一套脚本路径、所有初始化逻辑一致、所有错误码被正确识别——漏掉任意一环,压测时都会突然冒出超卖订单。










