久滴直播电商平台 · 技术博客系列 第 4 篇
🔖 标签:
Redis Lua秒杀库存一致性
前言
秒杀是电商系统中最考验技术功底的场景之一。瞬间涌入的大量请求,如果直接打到数据库上,轻则响应变慢,重则数据库宕机。更严重的是库存超卖——100 件商品卖出了 120 单,用户体验直接崩盘。
久滴直播电商平台在秒杀场景中设计了一套 Redis Lua 原子扣库存 + 双库存模型的方案,在保证高性能的同时,从根本上杜绝了超卖问题。本文将从数据模型到 Lua 脚本逐行解析,带你完整理解这套方案。
目录
- 秒杀场景下的库存超卖问题分析
- 双库存模型设计
- Lua 脚本逐行解析
- SeckillStockRedisService 三层能力
- 异常情况与降级策略
- DB 层的最终一致性保障
1. 秒杀场景下的库存超卖问题分析
先来看一个经典的超卖时序:
线程A: 读取库存 = 1 → 判断 > 0 → 扣减库存 (1→0) → 创建订单
线程B: 读取库存 = 1 → 判断 > 0 → 扣减库存 (1→0) → 创建订单
两个线程同时读到库存为 1,都判断"有库存",然后都执行了扣减——结果就是超卖了。
传统的解决方案是数据库加锁(SELECT ... FOR UPDATE)或者用乐观锁(WHERE stock >= count),但在高并发场景下,这两种方案要么性能太差,要么有大量请求失败。
最佳实践是把库存判断和扣减操作前置到 Redis 中,利用 Redis 的单线程执行模型来保证原子性。
2. 双库存模型设计
久滴平台的秒杀场景引入了双库存模型:
- SKU 维度库存(
seckill:sku_stock:{activityId}:{skuId}):每个 SKU 的独立库存 - 活动维度库存(
seckill:activity_stock:{activityId}):整个秒杀活动的总库存
为什么要双库存?考虑一个秒杀活动下有多个 SKU(如不同颜色/尺码)的场景:
- SKU 库存控制单个商品的售罄
- 活动库存控制整场秒杀的总量(防止多个 SKU 累计超卖)
两者必须同时扣减成功才算扣库存成功,这就是"双扣模型"的由来。
3. Lua 脚本逐行解析
Redis 的 Lua 脚本在 Redis 中是原子执行的——脚本执行期间不会有其他命令被插入。久滴平台用两个 Lua 脚本分别实现库存预扣和回补。
预扣脚本 seckill-stock-decr.lua
-- 秒杀库存预扣 Lua 脚本
-- KEYS[1] = seckill:sku_stock:{activityId}:{skuId}
-- KEYS[2] = seckill:activity_stock:{activityId}
-- ARGV[1] = count(扣减数量)
-- 返回: 1=成功, 0=库存不足, -1=key不存在
local skuStock = redis.call('GET', KEYS[1])
if not skuStock then return -1 end
local actStock = redis.call('GET', KEYS[2])
if not actStock then return -1 end
local count = tonumber(ARGV[1])
if tonumber(skuStock) < count or tonumber(actStock) < count then
return 0
end
redis.call('DECRBY', KEYS[1], count)
redis.call('DECRBY', KEYS[2], count)
return 1
逻辑非常清晰,分四步:
- GET SKU 库存:如果 key 不存在返回 -1(说明没有预热,需要降级到 DB)
- GET 活动库存:同理,key 不存在返回 -1
- 比较判断:两个库存都必须 >= 扣减数量,否则返回 0(库存不足)
- 双扣 DECRBY:同时扣减两个 key,由于是 Lua 脚本内执行,保证原子性
回补脚本 seckill-stock-incr.lua
-- 秒杀库存回补 Lua 脚本
-- KEYS[1] = seckill:sku_stock:{activityId}:{skuId}
-- KEYS[2] = seckill:activity_stock:{activityId}
-- ARGV[1] = count(回补数量)
-- 返回: 1=成功
redis.call('INCRBY', KEYS[1], tonumber(ARGV[1]))
redis.call('INCRBY', KEYS[2], tonumber(ARGV[1]))
return 1
回补脚本更简单——直接对两个 key 做 INCRBY。在用户取消订单或支付超时时调用,把库存"还回去"。
4. SeckillStockRedisService 三层能力
Java 层的 SeckillStockRedisService 封装了三个核心方法,分别对应库存生命周期的三个阶段:
预热(warmUp)
活动开始前,把库存数据从数据库加载到 Redis:
public void warmUpStock(Long activityId, Long skuId, int stock) {
String skuKey = buildSkuStockKey(activityId, skuId);
String actKey = buildActivityStockKey(activityId);
stringRedisTemplate.opsForValue()
.set(skuKey, String.valueOf(stock), STOCK_KEY_TTL_DAYS, TimeUnit.DAYS);
stringRedisTemplate.opsForValue()
.set(actKey, String.valueOf(stock), STOCK_KEY_TTL_DAYS, TimeUnit.DAYS);
log.info("[warmUpStock] 预热库存成功,activityId={}, skuId={}, stock={}",
activityId, skuId, stock);
}
Key 设置了 7 天的 TTL,防止僵尸 key 占用内存。
预扣(tryDecr)
用户下单时调用,返回三种结果:
public Boolean tryDecrStock(Long activityId, Long skuId, int count) {
String skuKey = buildSkuStockKey(activityId, skuId);
String actKey = buildActivityStockKey(activityId);
List<String> keys = Arrays.asList(skuKey, actKey);
try {
Long result = stringRedisTemplate.execute(decrScript, keys, String.valueOf(count));
if (result == null) return null;
if (result == 1L) return true; // 扣减成功
else if (result == 0L) return false; // 库存不足
else return null; // key 不存在,需降级
} catch (Exception e) {
log.error("[tryDecrStock] Redis 预扣异常,降级到 DB", e);
return null;
}
}
返回值设计很有讲究:
true→ 扣减成功,可以创建订单false→ 库存不足,直接拒绝null→ 异常情况(key 不存在或 Redis 不可用),降级到数据库走常规扣减逻辑
回补(incr)
订单取消时调用,把库存加回去:
public void incrStock(Long activityId, Long skuId, int count) {
String skuKey = buildSkuStockKey(activityId, skuId);
String actKey = buildActivityStockKey(activityId);
List<String> keys = Arrays.asList(skuKey, actKey);
try {
stringRedisTemplate.execute(incrScript, keys, String.valueOf(count));
log.info("[incrStock] 库存回补成功,activityId={}, skuId={}, count={}",
activityId, skuId, count);
} catch (Exception e) {
log.error("[incrStock] Redis 库存回补异常", e);
}
}
5. 异常情况与降级策略
生产环境中,Redis 不是万能的。久滴平台设计了完整的降级链路:
用户下单
↓
Redis Lua 预扣
├── 成功 → 创建订单 → DB 扣减(异步)
├── 库存不足 → 直接返回"已售罄"
├── key 不存在 → 降级到 DB 乐观锁扣减
└── Redis 异常 → 降级到 DB 乐观锁扣减
当 tryDecrStock 返回 null 时,上层业务会 fallback 到数据库层面的库存扣减——使用 CAS 乐观锁保证一致性:
UPDATE seckill_activity_sku
SET stock = stock - #{count}, sales = sales + #{count}
WHERE activity_id = #{activityId}
AND sku_id = #{skuId}
AND stock >= #{count}
6. DB 层的最终一致性保障
Redis 预扣只是"快速通道",数据库才是最终的"账本"。在 Redis 预扣成功后,系统会异步执行 DB 扣减:
- CAS 乐观锁:
WHERE stock >= count确保不超卖 - 唯一索引:
UNIQUE(activity_id, sku_id, order_id)防止重复扣减 - 定时对账:后台定时任务比对 Redis 和 DB 的库存数据,发现不一致时以 DB 为准修正
这套"Redis 预扣 + DB 兜底"的架构,既保证了秒杀的高性能(绝大部分请求在 Redis 层就被拦截),又保证了数据一致性(DB 是最终真相源)。
总结
本文拆解了久滴直播电商平台秒杀库存的核心方案:
- 双库存模型:SKU 库存 + 活动库存双维度控制,防止单品和总量两个层面的超卖
- Lua 脚本原子操作:GET → 判空 → 比较 → DECRBY 四步在一个 Redis 命令中原子完成
- 三层返回值:成功/不足/异常,异常时优雅降级到 DB
- DB 最终一致性:CAS 乐观锁 + 唯一索引 + 定时对账
下一篇我们将走进直播间弹幕系统,看看腾讯云 IM 在直播互动场景下的工程化落地。
📦 久滴直播电商平台 是一个基于 Spring Boot 3 + Vue3 + UniApp 的全栈开源直播电商解决方案。
🔗 GitHub:
https://github.com/jiudi-dev/live-mall-community(Community 版)如果觉得本文对你有帮助,欢迎 Star ⭐ 支持!

1125

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



