Redis+Lua原子扣减库存|Redisson分布式锁|性能对比|看门狗续期|电商秒杀|避免死锁
本文介绍了使用Lua脚本实现Redis原子性库存扣减的方案。通过将库存校验、扣减和幂等操作封装在Lua脚本中,利用Redis单线程特性保证原子性,避免分布式锁开销。文章详细展示了单品扣减和多商品扣减两种场景的Lua脚本实现,包括重复购买校验、库存不足判断等逻辑,并提供了Java调用示例。同时强调了生产环境中的注意事项,如幂等处理、集群一致性保障、性能优化等,建议结合监控、压测和对账机制确保系统可靠
·
用 Lua 脚本实现原子性库存扣减
一 设计思路与原子性原理
- 将“库存校验 + 扣减 + 幂等”全部下沉到 Redis 的一条 Lua 脚本中执行,Redis 以单线程顺序执行脚本,期间不会被其他命令插入,天然具备原子性,无需额外的分布式锁。
- 典型流程:
- 校验业务规则(如一人一单);2) 校验库存是否充足;3) 执行扣减;4) 写业务标记(如已购集合);5) 返回结果码。
- 该模式在高并发秒杀中已被广泛验证,可显著降低锁竞争与回滚成本,吞吐显著高于基于锁的方案。
二 单品扣减脚本与 Java 调用
- 脚本功能:校验用户是否重复购买,校验库存是否充足,扣减库存并记录用户,返回剩余库存或错误码。
- 返回码约定:-2 重复下单、-1 库存不足、≥0 扣减后剩余库存。
Lua 脚本 seckill.lua
-- KEYS[1] 库存key,KEYS[2] 已购用户集合key
-- ARGV[1] 购买数量,ARGV[2] 用户ID
local stockKey = KEYS[1]
local boughtKey = KEYS[2]
local quantity = tonumber(ARGV[1])
local userId = ARGV[2]
-- 1) 重复下单校验
if redis.call('SISMEMBER', boughtKey, userId) == 1 then
return -2
end
-- 2) 库存校验
local remain = tonumber(redis.call('GET', stockKey))
if not remain or remain < quantity then
return -1
end
-- 3) 扣减库存 & 记录用户
redis.call('DECRBY', stockKey, quantity)
redis.call('SADD', boughtKey, userId)
return remain - quantity
Java 调用(Spring Boot + StringRedisTemplate)
@Service
public class StockService {
@Autowired
private StringRedisTemplate redisTemplate;
private static final String LUA_SHA = "stock:deduct:lua"; // 预加载后的 SHA
// 预加载脚本(应用启动或首次调用时)
@PostConstruct
public void init() {
String script = """
local stockKey = KEYS[1]
local boughtKey = KEYS[2]
local quantity = tonumber(ARGV[1])
local userId = ARGV[2]
if redis.call('SISMEMBER', boughtKey, userId) == 1 then
return -2
end
local remain = tonumber(redis.call('GET', stockKey))
if not remain or remain < quantity then
return -1
end
redis.call('DECRBY', stockKey, quantity)
redis.call('SADD', boughtKey, userId)
return remain - quantity
""";
LUA_SHA = redisTemplate.execute((RedisCallback<String>) conn ->
conn.scriptLoad(script.getBytes(StandardCharsets.UTF_8)));
}
// 扣减库存
public Long deduct(String sku, int quantity, Long userId) {
List<String> keys = Arrays.asList("stock:" + sku, "bought:" + sku);
Object res = redisTemplate.execute(
(RedisConnection conn) -> conn.evalSha(
LUA_SHA, ReturnType.INTEGER, keys.size(),
keys.stream().map(String::getBytes).toArray(),
String.valueOf(quantity).getBytes(),
String.valueOf(userId).getBytes()
)
);
return (Long) res; // >=0 成功剩余;-1 售罄;-2 重复
}
}
- 要点:使用 SCRIPT LOAD + EVALSHA 减少网络传输与解析开销;脚本内完成全部判断与写入,避免竞态与回滚。
三 多商品原子扣减脚本
- 场景:一次下单涉及多个 SKU,需要“全成功或全失败”的原子性。
- 思路:先批量 MGET 获取各 SKU 库存,任一不足则立即返回“哪些不足”;全部充足再逐个 DECRBY。
Lua 脚本 multi_deduct.lua
-- KEYS: 库存key 列表;ARGV: 购买数量列表(与 KEYS 一一对应)
local stocks = redis.call('MGET', unpack(KEYS))
local args = {unpack(ARGV)}
-- 1) 任一不足,收集不足项并返回
local insufficient = {}
for i = 1, #stocks do
local remain = tonumber(stocks[i])
if not remain or remain < tonumber(args[i]) then
table.insert(insufficient, KEYS[i] .. '=' .. tostring(remain or 0))
end
end
if #insufficient > 0 then
return insufficient
end
-- 2) 全部充足,逐个扣减
for i = 1, #stocks do
redis.call('DECRBY', KEYS[i], tonumber(args[i]))
end
return {} -- 空表表示全部成功
Java 调用
public List<String> multiDeduct(Map<String, Integer> skuQtyMap) {
List<String> keys = new ArrayList<>(skuQtyMap.keySet());
List<String> qtys = keys.stream()
.map(k -> String.valueOf(skuQtyMap.get(k)))
.toList();
Object res = redisTemplate.execute((RedisConnection conn) ->
conn.evalSha(LUA_SHA_MULTI, ReturnType.MULTI, keys.size(),
keys.stream().map(String::getBytes).toArray(),
qtys.stream().map(String::getBytes).toArray()
)
);
// 返回空列表表示成功;非空为“库存不足清单”
return (List<String>) res;
}
- 说明:脚本保证了“检查 + 扣减”的原子性;若需要“事务性回滚”(某一步失败整体回滚),可在扣减前先写入预留标记并在失败时补偿,或采用“两阶段提交 + 对账补偿”。
🔥 关注公众号【云技纵横】,开始更新redis缓存进阶,包含手写缓存注解,缓存雪崩等内容哟!
四 生产级注意事项与兜底
- 幂等与防超卖:对“一人一单”使用 SET/Redis 原子操作记录已购;对“不存在的 SKU”直接返回失败,避免无效请求进入脚本。
- 集群与一致性:在 Redis 主从异步复制下,极端故障切换可能丢写;建议开启 AOF(appendfsync everysec)、合理副本数,或使用 Redis-Raft 等强一致方案;核心链路可结合 Canal + MQ 异步落库与定时对账补偿,保障最终一致性。
- 性能与稳定性:优先使用 EVALSHA;对热点 SKU 可结合 本地缓存 + Redis 多级缓存;活动高峰配置降级/静态兜底与限流,避免雪崩。
- 监控与压测:关注 命中率、RT、QPS、脚本错误率、主从延迟;压测覆盖“单品/多品、充足/不足、并发冲突”场景,验证返回码与补偿链路。
更多推荐


所有评论(0)