秒杀库存预扣减必须用Lua脚本原子执行“读-判-写”,禁用DECRBY等无判断命令以防超卖;Redis仅作资格筛选,后续需校验、下单、DB扣减并失败回滚;须配合用户限流、订单幂等、超时释放三重防护。

为什么不能直接用MySQL扣减秒杀库存
高并发下直接操作MySQL会瞬间打满连接池,行锁升级成表锁,超时、死锁、主从延迟全来。Redis的原子操作和内存速度才是秒杀预扣减的合理选择——但要注意它只是“预扣”,不是最终落库。
关键判断:Redis扣减成功 ≠ 秒杀成功,只是获得一个参与资格;后续仍需校验库存、生成订单、扣减DB真实库存,失败要回滚Redis(即加回去)。
INCRBY 和 DECRBY 不适合秒杀预扣减
这两个命令无法做“扣减前判断是否足够”,容易超卖。比如库存剩1件,两个请求同时 DECRBY 1,都会返回0,但实际只该放行1个。
必须用原子脚本保证“读-判-写”一体:
- 用
EVAL执行Lua脚本,Redis单线程保证原子性 - 脚本内先
GET当前库存,判断 ≥1 再DECR,返回结果标识是否扣减成功 - 不要用
SETNX模拟锁——它不解决库存数值逻辑,且易因网络中断导致锁残留
示例Lua脚本(Spring Boot中通过 RedisTemplate.execute() 调用):
Redis 缓存和数据结构管理技能。通过自然语言操作 Redis,支持 String、Hash、List、Set、ZSet、Stream 等数据结构操作。当用户提到 Redis、缓存、消息队列、会话存储时使用此技能。
if redis.call("GET", KEYS[1]) == false then
return -1
end
local stock = tonumber(redis.call("GET", KEYS[1]))
if stock <= 0 then
return 0
end
return redis.call("DECR", KEYS[1])
如何用 RedisTemplate 安全执行预扣减脚本
Spring Boot默认的 RedisTemplate 支持Lua脚本执行,但要注意几个硬坑:
- 脚本必须提前加载到Redis服务端(用
RedisScript包装),避免每次传输增大RTT - key必须显式传入(
KEYS[1]),不能硬编码在脚本里;多个key用Collection传,避免乱序 - 返回值是
Long类型:-1=key不存在,0=库存不足,>0=扣减后剩余值(可据此判断是否抢到了) - 别忽略异常:
JedisConnectionException表示Redis不可用,此时应降级为排队或拒绝,而不是重试加重压力
Java调用片段:
DefaultRedisScript<Long> script = DefaultRedisScript.<Long>builder()
.scriptSource(new ResourceScriptSource(new ClassPathResource("seckill-deduct.lua")))
.returnValueType(Long.class)
.build();
Long result = redisTemplate.execute(script, Collections.singletonList("seckill:stock:1001"));
预扣减成功后,怎么防刷、防重复提交、防超时未支付
Redis预扣减只是第一道闸门,后面三件事不做,照样崩:
- 用户维度限流:用
INCR+EXPIRE组合,例如INCR user:123:seckill:today+EXPIRE user:123:seckill:today 86400,限制每人每天最多抢3次 - 订单幂等:前端提交带唯一
requestId,后端用SET requestId EX 60 NX占位,存在则直接返回“处理中”,避免重复下单 - 超时释放:预扣减成功后启动异步任务(如RabbitMQ延迟消息),15分钟后检查订单是否创建成功;没创建就用Lua脚本
INCR把库存加回去
注意:Redis里存的库存是“待确认数”,不是“已售出数”,两者必须靠最终一致性对账,不能混为一谈。

















