不能直接用DECR+应用层查余额做预扣,因网络往返导致竞态:库存扣减后余额校验失败无法回滚,或余额足但库存不足造成冗余查询;Lua脚本将读余额、读库存、比对、条件扣减等全部在Redis单线程原子执行,彻底避免状态不一致。

为什么不能直接用 DECR + 应用层查余额做预扣?
因为网络往返和应用层判断之间存在竞态窗口:库存 DECR 成功后,余额校验失败,但库存已扣无法自动回滚;反之,余额够但库存不足时又多查一次。Lua 脚本把「读余额、读库存、比对、扣库存(条件性)」全部压进 Redis 单线程原子执行,彻底规避中间状态不一致。
EVAL 脚本里如何安全读取并校验两个 key?
脚本必须用 redis.call("GET", KEYS[1]) 和 redis.call("GET", KEYS[2]) 分别取用户余额和商品库存,不能依赖 ARGV 传入“预期值”——那会把校验逻辑甩给客户端,失去原子性。关键点:
- 所有读操作必须在 Lua 内完成,且用
redis.call(不用redis.pcall,除非你要捕获异常并自定义返回) - 数值比较前必须显式
tonumber()转换,Redis 返回的是字符串,"10" > "2"在 Lua 里是false - 扣减只在
if balance >= price and stock > 0全为真时执行redis.call("DECR", KEYS[2]),并用redis.call("HINCRBY", ...)记录预扣流水(可选)
示例片段:
if tonumber(balance) >= tonumber(ARGV[1]) and tonumber(stock) > 0 then
redis.call("DECR", KEYS[2])
return 1
else
return 0
end
如何让预扣结果能被后续订单创建真正消费或回滚?
单纯 DECR 库存不是最终扣减,只是“占位”。必须配套设计超时释放机制:
Redis 缓存和数据结构管理技能。通过自然语言操作 Redis,支持 String、Hash、List、Set、ZSet、Stream 等数据结构操作。当用户提到 Redis、缓存、消息队列、会话存储时使用此技能。
- 预扣时用
redis.call("EXPIRE", KEYS[2], 300)给库存 key 加 5 分钟过期(仅当首次预扣成功时设,避免重复覆盖) - 或更推荐:把预扣记录单独存到一个 hash,如
HSET prelock:order123 uid 1001 sku_id 1002 amount 299 expire_ts 171xxxxxx,用EXPIREAT控制生命周期 - 订单创建成功 → 调用另一段 Lua 执行最终扣款 + 清除预扣记录;超时未创建 → key 自动失效,库存回归可用
注意:EXPIRE 对 DECR 后的整数 key 有效,但无法对“被多个用户预扣”的场景做细粒度控制——这时必须用独立预扣记录结构。
为什么本地测试时 EVAL 返回 nil 或报错 ERR Error running script?
常见三类原因:
- 脚本里用了未声明的
KEYS:比如写了redis.call("GET", "user:1001")硬编码 key,但没把它放进KEYS数组 —— Redis 6.0+ 默认禁用全局 key 访问,必须显式传入 -
ARGV传了空值或非数字字符串,导致tonumber(ARGV[1])返回nil,后续比较直接报错 - 脚本超过 512 字节(默认限制),尤其加了注释或日志
redis.log()—— 生产环境建议删掉所有redis.log,用监控埋点替代
调试技巧:先用 redis-cli --eval 命令行跑最小脚本验证逻辑,再集成到代码;错误信息里带行号,重点看第几行的 tonumber 或 redis.call 出了问题。
HGET 检查预扣记录是否存在且未消费,而不是只依赖库存数值。

















