【IT老齐063】大型电商整点秒杀业务场景下的库存超卖预防方案
·
概述
视频链接: [IT老齐063] 大型电商整点秒杀业务场景下,商品库存如何预防超卖现象产生
视频地址: https://www.bilibili.com/video/BV1Qv411u7K7
什么是库存超卖?
库存超卖是指在高并发场景下,由于多个用户同时购买同一件商品,导致实际销售数量超过库存数量的现象。在秒杀场景中,这个问题尤为突出。
超卖产生的根本原因
- 并发访问:大量用户同时访问商品库存
- 数据竞争:读取-判断-扣减库存的非原子操作
- 网络延迟:请求处理时间差异造成的时序问题
传统方案及其局限性
1. 数据库悲观锁方案
-- 伪代码示例
SELECT * FROM goods_stock WHERE id = ? FOR UPDATE;
-- 检查库存是否充足
-- 扣减库存
缺点:
- 性能较差,串行化执行
- 容易造成死锁
- 数据库压力大
2. 数据库乐观锁方案
-- 伪代码示例
UPDATE goods_stock SET stock = stock - 1 WHERE id = ? AND stock > 0;
缺点:
- 高并发下失败率高
- 用户体验差
- 需要重试机制
分布式锁解决方案
Redis分布式锁实现
public boolean tryDeductStock(String productId, int quantity) {
String lockKey = "stock_lock:" + productId;
String lockValue = UUID.randomUUID().toString();
try {
// 获取分布式锁
Boolean acquired = redisTemplate.opsForValue()
.setIfAbsent(lockKey, lockValue, Duration.ofSeconds(10));
if (Boolean.TRUE.equals(acquired)) {
// 检查库存
Integer currentStock = getCurrentStock(productId);
if (currentStock >= quantity) {
// 扣减库存
return deductStock(productId, quantity);
}
}
return false;
} finally {
// 释放锁
releaseLock(lockKey, lockValue);
}
}
Redission分布式锁
RLock lock = redissonClient.getLock("stock_lock:" + productId);
try {
if (lock.tryLock(10, TimeUnit.SECONDS)) {
// 业务逻辑:检查库存、扣减库存
return processOrder(productId, quantity);
}
} finally {
if (lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
Redis原子操作方案
Lua脚本实现原子性扣减
-- Redis Lua脚本
local key = KEYS[1]
local quantity = tonumber(ARGV[1])
local current_stock = redis.call('GET', key)
if current_stock == false then
return -1 -- 商品不存在
end
current_stock = tonumber(current_stock)
if current_stock < quantity then
return 0 -- 库存不足
end
local new_stock = current_stock - quantity
redis.call('SET', key, new_stock)
return new_stock
Java调用示例
public boolean deductStockWithLua(String productId, int quantity) {
String luaScript =
"local current_stock = redis.call('GET', KEYS[1]) " +
"if current_stock == false then return -1 end " +
"current_stock = tonumber(current_stock) " +
"if current_stock < tonumber(ARGV[1]) then return 0 end " +
"local new_stock = current_stock - tonumber(ARGV[1]) " +
"redis.call('SET', KEYS[1], new_stock) " +
"return new_stock";
Object result = redisTemplate.execute(
new DefaultRedisScript<>(luaScript, Long.class),
Collections.singletonList("stock:" + productId),
String.valueOf(quantity)
);
return Long.valueOf(0).compareTo((Long) result) < 0;
}
预扣库存方案
两阶段提交思想
- 预扣阶段:预先冻结库存
- 确认阶段:确认支付后真正扣减
public class StockService {
public boolean preDeductStock(String productId, String orderId, int quantity) {
// 检查可用库存
String availableKey = "stock_available:" + productId;
String frozenKey = "stock_frozen:" + productId;
// 使用Lua脚本保证原子性
String luaScript =
"local available = redis.call('GET', KEYS[1]) " +
"if available == false then available = 0 end " +
"if tonumber(available) < tonumber(ARGV[1]) then return 0 end " +
"redis.call('DECRBY', KEYS[1], ARGV[1]) " +
"redis.call('INCRBY', KEYS[2], ARGV[1]) " +
"redis.call('SETEX', ARGV[2], 300, ARGV[1]) " + -- 订单冻结信息
"return 1";
Object result = redisTemplate.execute(
new DefaultRedisScript<>(luaScript, Long.class),
Arrays.asList(availableKey, frozenKey),
String.valueOf(quantity), "order_frozen:" + orderId
);
return Long.valueOf(1).equals(result);
}
public void confirmOrder(String productId, String orderId) {
// 确认订单,正式扣减库存
String frozenKey = "stock_frozen:" + productId;
String confirmedKey = "stock_confirmed:" + productId;
redisTemplate.opsForValue().decrement(frozenKey);
redisTemplate.opsForValue().increment(confirmedKey);
// 删除订单冻结信息
redisTemplate.delete("order_frozen:" + orderId);
}
public void cancelOrder(String productId, String orderId) {
// 取消订单,释放冻结库存
String availableKey = "stock_available:" + productId;
String frozenKey = "stock_frozen:" + productId;
redisTemplate.opsForValue().increment(availableKey);
redisTemplate.opsForValue().decrement(frozenKey);
// 删除订单冻结信息
redisTemplate.delete("order_frozen:" + orderId);
}
}
消息队列异步处理方案
基于RabbitMQ的异步库存处理
@Component
public class AsyncStockService {
@Autowired
private RabbitTemplate rabbitTemplate;
public void asyncDeductStock(String productId, int quantity) {
StockMessage message = new StockMessage(productId, quantity, System.currentTimeMillis());
rabbitTemplate.convertAndSend("stock.deduct.queue", message);
}
@RabbitListener(queues = "stock.deduct.queue")
public void handleStockDeduction(StockMessage message) {
// 使用Redis单线程特性保证顺序执行
String luaScript =
"local current_stock = redis.call('GET', KEYS[1]) " +
"if current_stock == false then return -1 end " +
"current_stock = tonumber(current_stock) " +
"if current_stock < tonumber(ARGV[1]) then return 0 end " +
"local new_stock = current_stock - tonumber(ARGV[1]) " +
"redis.call('SET', KEYS[1], new_stock) " +
"return new_stock";
Object result = redisTemplate.execute(
new DefaultRedisScript<>(luaScript, Long.class),
Collections.singletonList("stock:" + message.getProductId()),
String.valueOf(message.getQuantity())
);
if (Long.valueOf(-1).equals(result)) {
// 库存扣减失败,发送失败消息
rabbitTemplate.convertAndSend("stock.fail.queue", message);
}
}
}
数据库层面优化
行级锁优化
-- 使用行级锁,而不是表级锁
UPDATE goods_stock
SET stock = stock - 1, version = version + 1
WHERE id = ? AND stock > 0 AND version = ?
分库分表策略
// 根据商品ID进行分库分表
public String getStockTable(int productId) {
int dbIndex = productId % dbCount;
int tableIndex = productId % tableCount;
return "goods_stock_" + dbIndex + "_" + tableIndex;
}
综合解决方案
秒杀系统架构设计
用户请求
↓
限流层 (令牌桶/漏桶算法)
↓
缓存层 (Redis预热库存)
↓
分布式锁 (Redisson)
↓
库存扣减 (Lua脚本原子操作)
↓
异步处理 (消息队列)
↓
数据库持久化
完整实现示例
@Service
public class SeckillService {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
@Autowired
private RedissonClient redissonClient;
@Autowired
private OrderService orderService;
public SeckillResult seckill(String productId, String userId) {
String lockKey = "seckill_lock:" + productId;
RLock lock = redissonClient.getLock(lockKey);
try {
if (lock.tryLock(5, TimeUnit.SECONDS)) {
// 1. 检查是否还有库存
String stockKey = "seckill_stock:" + productId;
Integer currentStock = (Integer) redisTemplate.opsForValue().get(stockKey);
if (currentStock != null && currentStock > 0) {
// 2. 扣减Redis库存
redisTemplate.opsForValue().decrement(stockKey);
// 3. 创建订单
Order order = orderService.createOrder(productId, userId);
// 4. 异步扣减数据库库存
asyncDeductDatabaseStock(productId, 1);
return SeckillResult.success(order.getId());
} else {
return SeckillResult.failure("库存不足");
}
} else {
return SeckillResult.failure("系统繁忙,请稍后再试");
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return SeckillResult.failure("系统异常");
} finally {
if (lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
}
private void asyncDeductDatabaseStock(String productId, int quantity) {
// 异步扣减数据库库存,保证最终一致性
CompletableFuture.runAsync(() -> {
// 执行数据库库存扣减操作
});
}
}
性能优化建议
1. 缓存预热
- 秒杀开始前预先加载库存到Redis
- 预先分配库存到多个key,减少热点问题
2. 限流控制
- 使用令牌桶算法控制请求速率
- 对同一用户进行请求频率限制
3. 降级策略
- 库存不足时快速失败
- 熔断机制防止系统雪崩
总结
库存超卖问题是电商系统中的经典难题,需要从多个层面综合考虑:
- 缓存层面:使用Redis实现高性能库存管理
- 锁机制:合理使用分布式锁保证数据一致性
- 异步处理:解耦核心业务逻辑
- 限流降级:保证系统稳定性
- 监控告警:及时发现和处理异常情况
通过以上方案的组合使用,可以有效解决秒杀场景下的库存超卖问题。
更多推荐


所有评论(0)