Lua脚本保证库存扣减原子性防超卖,Pipeline仅提升批量吞吐;每个SKU扣减必须单独调用Lua脚本,不可合并多key操作;需预热库存并设过期时间。

在 Spring Boot 中用 Redis 管道(Pipeline)和 Lua 脚本配合实现秒杀库存扣减,核心在于:**Lua 脚本保证原子性,避免超卖;管道提升批量操作吞吐,但不解决单次扣减的并发安全问题**。真正防超卖必须依赖 Lua 脚本——它在 Redis 服务端一次性执行判断与扣减,杜绝竞态条件。
为什么不能只靠 Pipeline?
Pipeline 只是把多个命令打包发送、减少网络往返,不改变每个命令的执行逻辑。如果用 pipeline 连续执行 GET + DECR,中间仍可能被其他客户端插入,导致超卖。所以关键不是“怎么发”,而是“发什么”——必须把库存检查和扣减封装进一段 Lua 脚本里,在 Redis 单线程中完整执行。
用 Lua 脚本实现原子扣减(推荐方案)
在 resources 目录下新建 seckill-deduct.lua:
if redis.call("exists", KEYS[1]) == 0 then
return -1 -- 库存 key 不存在
end
local stock = tonumber(redis.call("get", KEYS[1]))
if stock <= 0 then
return 0 -- 库存不足
end
redis.call("decr", KEYS[1])
return 1 -- 扣减成功Java 中调用示例(使用 StringRedisTemplate):
立即学习“Java免费学习笔记(深入)”;
Redis 缓存和数据结构管理技能。通过自然语言操作 Redis,支持 String、Hash、List、Set、ZSet、Stream 等数据结构操作。当用户提到 Redis、缓存、消息队列、会话存储时使用此技能。
private static final String DEDUCT_SCRIPT = "seckill-deduct.lua";
private final DefaultRedisScript<Long> deductScript;
<p>public SeckillService(StringRedisTemplate redisTemplate) {
this.deductScript = new DefaultRedisScript<>();
this.deductScript.setScriptSource(new ResourceScriptSource(
new ClassPathResource(DEDUCT_SCRIPT)));
this.deductScript.setResultType(Long.class);
}</p><p>public boolean tryDeductStock(String skuId) {
String key = "seckill:stock:" + skuId;
Long result = redisTemplate.execute(
deductScript,
Collections.singletonList(key)
);
return result != null && result == 1L;
}说明:
-
KEYS[1] 是库存 key,通过
Collections.singletonList(key)传入 - 脚本返回
-1(key 不存在)、0(库存不足)、1(成功),业务层可据此做不同响应 - 整个 if-check-decr 过程在 Redis 内原子执行,天然防超卖
Pipeline 的合理使用场景(辅助优化)
当一次秒杀需处理多个 SKU 或记录多个日志时,可用 pipeline 批量提交,但每个 SKU 的扣减仍必须各自走 Lua 脚本调用**:
List<RedisCommand<?, ?, ?>> commands = new ArrayList<>();
for (String skuId : skuIds) {
String key = "seckill:stock:" + skuId;
// 每个 sku 都单独执行 Lua 脚本(不可合并成一个脚本跨 key 操作,除非你明确允许事务隔离失效)
commands.add(RedisCommand.of(
"EVALSHA",
scriptSha,
1,
key
));
}
// 使用 pipeline 批量发送这些 EVALSHA 命令(需提前 SCRIPT LOAD 获取 sha1)
redisTemplate.getConnectionFactory().getConnection().pipelined(...);注意:
- 不要试图在一个 Lua 脚本里循环扣减多个 key——Redis 6.0+ 才支持多 key 原子操作(且要求所有 key 在同一 slot),秒杀场景更推荐单 key 设计
- 若用
EVALSHA替代EVAL,需先调用SCRIPT LOAD缓存脚本,提升性能 - Pipeline 不提供事务回滚能力,失败需业务侧重试或补偿
补充:库存预热与过期策略
秒杀开始前,务必通过后台任务将库存写入 Redis 并设置合理过期时间:
redisTemplate.opsForValue().set(
"seckill:stock:" + skuId,
String.valueOf(initStock),
Duration.ofHours(24)
);避免因 key 不存在导致 Lua 脚本直接返回 -1;同时防止库存长期滞留影响后续活动。

















