必须用 lua 脚本实现原子扣减,因其单线程执行可消除“读-判-减”间竞态;分步操作因网络延迟与判断间隔必然导致超卖,如两请求并发读到库存=1后均扣减致-1;正确脚本需用 tonumber() 安全转换、显式比较、decrby 扣减并返回明确结果,调用时须严格匹配 keys/argv 数量与类型,且业务层须做兜底校验。

必须用 Lua 脚本,因为 Redis 中 Lua 脚本单线程原子执行,能把“读取→判断→扣减”三步合为一个不可分割的操作;Java 或 PHP 分步调用 GET 再 DECRBY 一定会超卖。
为什么分步操作必然超卖
两个请求同时读到库存=1,都判断“够扣”,接着各自执行 DECRBY,结果库存变成 -1。这不是概率问题,是确定性竞态——只要并发量上去,就必现。
- 现象:
redis.call('GET', 'stock:1001')返回 1,但下一毫秒可能已被别人改掉 - 本质:网络往返 + 应用层逻辑判断 + 再发命令,中间存在时间窗口
- 后果:哪怕只差几微秒,也会导致多个请求同时通过判断,最终库存透支
Lua 脚本怎么写才真正安全
脚本必须把“检查”和“修改”放在同一作用域内,且所有操作都走 redis.call(),不能依赖外部变量或分段执行。
Redis 缓存和数据结构管理技能。通过自然语言操作 Redis,支持 String、Hash、List、Set、ZSet、Stream 等数据结构操作。当用户提到 Redis、缓存、消息队列、会话存储时使用此技能。
- 错误写法:
local stock = redis.call('GET', KEYS[1]); if stock > 0 then redis.call('DECR', KEYS[1]) end—— 没返回值,无法知道是否真扣了 - 正确写法:用
tonumber()转换、显式比较、redis.call('DECRBY', ...)扣减,并return明确结果(如 1/0/-1) - 关键细节:必须用
DECRBY而非DECR,否则无法支持批量扣减(比如下单买 3 件) - 边界处理:
redis.call('GET', ...)返回nil时要判空,否则tonumber(nil)是nil,比较会出错
PHP/Java 调用时容易漏掉的坑
脚本写对了,调用不对照样超卖。常见失误不是语法错,而是语义错。
- PHP 中
$redis->eval($script, ['stock:1001', 3], 1)的第三个参数是numkeys,必须传 1,否则KEYS[1]取不到键名 - Java 用
redis.eval()时,args里数量要用String.valueOf(3)转字符串,Lua 里tonumber(ARGV[1])才能正确解析 - 返回值必须严格判断:
if ($result === 1),不能用==(PHP),也不能只判非零(Java 中Long类型需.equals(1L)) - 别在 Lua 里做耗时操作(比如循环、HTTP 请求),Redis 是单线程,卡住会影响所有请求
最常被忽略的一点:脚本本身没 bug,但业务没做兜底校验。比如 Redis 扣减成功后,订单创建失败,库存就“黑掉了”。得配异步对账或本地预扣+消息补偿,否则只是把超卖风险从 Redis 转移到了下游环节。










