这是一个非常经典的**“看似可行,实则巨坑”**的方案。
因为为了防止库存回补,所以你需要保证库存不能为负数, 所以,你在代码层面的逻辑是:
- redis.decr(key)
- if (返回值 < 0) { redis.incr(key); return "失败"; }
结论是:千万不要这样写。
虽然逻辑上看起来“借了再还”最后是平的,但在生产环境(特别是高并发或不稳定网络环境)下,这会引发数据永久不一致和性能雪崩。
主要有以下 3 个致命缺陷:
1. 致命缺陷:网络中断或服务崩溃导致“只减不加”
这是最大的风险点。因为 DECR 和 INCR 是两次独立的网络请求,它们之间没有原子性保护。
场景推演:
- 当前库存为 0。
- 线程 A 发起 DECR,Redis 执行成功,库存变为 -1。
- 线程 A 收到返回值 -1,准备发起 INCR 进行补偿回滚。
- 就在这毫秒之间,发生了意外: 情况一: 线程 A 所在的服务器宕机/重启了(比如被 Kubernetes OOM Kill 了)。 情况二: 网络抖动,线程 A 发出的 INCR 请求包丢了,或者 Redis 的响应包丢了导致超时异常。
- 结果: INCR 永远没有被执行。
- 后果: Redis 里的库存永久停留在 -1。 接着有人退款(真正的回滚),库存变成了 0。 本来应该恢复成 1 个库存给别人抢,现在因为那个永久的 -1,这 1 个库存被“吞”了。 这就是典型的“库存泄露”。
2. 性能缺陷:Redis 写压力翻倍(写放大)
Redis 的读性能极高,但写性能相对昂贵(涉及到 AOF 刷盘、主从同步)。
场景推演: 假设库存 0,这时候来了 10,000 个并发请求。
- 使用 Lua 脚本:
- 10,000 个请求 -> Redis 执行 Lua -> 判断 stock <= 0 -> 直接返回 0。
- Redis 实际发生的写操作:0 次(因为没有实际扣减)。
- Redis 只是做了一次内存读取和逻辑判断,速度飞快,不产生 AOF 日志。
- 使用 DECR + INCR 补偿方案:
- 10,000 个请求 -> 10,000 次 DECR(库存狂掉到 -10000)。
- 10,000 个请求发现小于 0 -> 10,000 次 INCR(库存慢慢加回 0)。
- Redis 实际发生的写操作:20,000 次!
- 这 2 万次操作全部要写 AOF 日志,全部要同步给从节点(Slave)。
- 后果: 在没货的时候,反而把 Redis 的磁盘 IO 和网络带宽打满了,可能直接把 Redis 搞挂。
3. 竞态与监控干扰
- 监控报警失效: 正常运维中,我们通常会设置“库存 < 0 报警”。如果你用这种方案,库存会频繁在负数跳动,报警系统会一直响,导致你无法区分是“真的出 Bug 了”还是“正常的补偿逻辑”。
- 并发视觉干扰: 在高并发瞬间,你通过 Redis 客户端查看库存,可能会看到 -500 这种数字,很难排查问题。
总结
Lua 脚本的本质不仅仅是逻辑判断,更是为了“原子性”和“减少无效写操作”。
|
特性 |
Lua 脚本方案 |
DECR + INCR 补偿方案 |
|
原子性 |
完全原子(要么全做,要么不做) |
无原子性(两个独立步骤,中间可能断开) |
|
故障后果 |
即使服务崩了,库存依然准确 |
服务崩了 = 库存永久变负 |
|
Redis 写压力 |
库存不足时,0 写操作 |
库存不足时,2倍 写操作(最致命) |
|
推荐指数 |
⭐⭐⭐⭐⭐ |
☠️ (绝对禁止) |
所以,宁愿多写几行 Lua 脚本代码,也不要为了省事用 DECR 后再 INCR,风险成本太高了。

被折叠的 条评论
为什么被折叠?



