超卖不是Redis的问题,而是未使用原子操作:必须用DECRBY或Lua脚本替代GET+IF+SET三步;DECR需检查返回值判断库存是否充足;复杂逻辑须用预加载Lua脚本保证服务端原子性。

直接结论:超卖不是Redis的问题,而是你没用对原子操作——别再写GET+IF+SET三步了,必须用DECRBY或Lua脚本。
为什么stringRedisTemplate.opsForValue().get() + set()一定会超卖
这是最典型的错误模式:先取库存值,判断是否大于0,再减1后存回去。哪怕Redis是单线程,应用层这三步之间存在毫秒级时间窗口,两个并发请求完全可能同时读到库存=1,都判断通过,都执行set("1") → 最终库存还是1,但两个订单都生成了。
常见错误现象:
- 压测时库存10件,最终落库订单23单
- 日志里反复出现“扣减成功”,但数据库库存字段为负数
- 单机测试不超卖,上多实例立刻复现
根本原因不是Redis不可靠,而是你把原子逻辑拆到了JVM里执行。
用decrement()必须配合返回值判断
DECR和DECRBY命令本身是原子的,但Java客户端调用后不检查返回值,就等于白用。
正确做法:
- 确保库存初始值存的是整数类型(不能是
"10"字符串,否则抛ERR value is not an integer) - 必须捕获返回值:
Long afterStock = template.opsForValue().decrement(key) - 只根据
afterStock >= 0决定业务是否成功,而不是看“有没有异常” - 如果
afterStock为负,说明已售罄,应立即拒绝,不能回退或重试
示例关键代码:
Long remaining = stringRedisTemplate.opsForValue().decrement("sku:1001");
if (remaining >= 0) {
// 扣减成功,可继续下单流程
} else {
// 库存不足,返回"已抢光"
}
复杂逻辑必须用Lua脚本保证原子性
当需要“查库存→扣减→写日志key→判断是否归零→触发通知”等多步联动时,单个Redis命令不够用。Lua是在Redis服务端原子执行的唯一可靠方案。
注意点:
- 脚本中所有key必须显式传入
KEYS数组,不能硬编码,否则集群模式下会报错 - 返回值类型要和Java端接收一致(如Lua里
return 5,Java需用(Long)强转) - 不要在Lua里做耗时操作(如HTTP调用、文件读写),它会阻塞整个Redis实例
- 本地调试可用
redis-cli --eval,生产环境建议预加载脚本避免每次传输
典型脚本片段(deduct_stock.lua):
local stock = redis.call('GET', KEYS[1])
if not stock then return -1 end
local stockNum = tonumber(stock)
if stockNum <h3>容易被忽略的底层细节</h3><p>很多人以为用了<code>DECR</code>或<code>Lua</code>就万事大吉,但实际线上仍出问题,往往卡在这几个地方:</p>
- Redis连接池配置不合理,高并发下连接耗尽,导致请求排队,间接放大竞态窗口
- 库存key没做分片(比如全用
sku:1001),导致单个key成为热点,CPU打满 - 没配
maxmemory-policy,内存溢出后Redis开始驱逐key,库存数据意外丢失 - Lua脚本里用了
redis.call('EXPIRE', ...)但key本身没设置过期,返回nil导致脚本中断失败
防超卖的底线不是“用了什么技术”,而是“所有路径都收敛到一个原子出口”。只要有一处绕过DECR或Lua直连数据库扣减,整个防线就失效。











