高并发商城 商品为了防止超卖,都做了哪些努力?
在高并发商城场景中,防止商品超卖(即实际卖出量 > 商品库存总量)是核心技术挑战之一,需要从 数据库层、缓存层、应用层、分布式协调 等多个维度构建防御体系,结合业务场景设计多层防护。以下是实际业务中常用的技术方案和努力方向:
一、数据库层:保证库存操作的原子性(最核心防线)
数据库是库存数据的最终存储载体,其操作的原子性是防止超卖的基础。
1. 行锁 + 条件更新(悲观锁思路)
通过 UPDATE
语句的 WHERE
条件同时检查库存,利用数据库行锁保证操作的原子性,这是最直接有效的方案:
-- 扣减库存时,必须带库存校验条件
UPDATE product_stock
SET stock = stock - 1, updated_at = NOW()
WHERE product_id = ? AND stock >= 1; -- 核心:只有库存足够时才执行扣减
- 原理:数据库会对匹配
product_id
的行加排他锁(X锁),同一时间只有一个事务能执行该更新,其他事务需等待锁释放。 - 优势:严格保证库存不会超卖,适合库存精度要求极高的场景(如限量商品、奢侈品)。
- 注意:需确保
product_id
有索引,避免行锁升级为表锁;控制事务长度,减少锁持有时间(如将非核心操作移到事务外)。
2. 乐观锁(版本号/库存字段校验)
适合 读多写少、并发冲突低 的场景,通过版本号或库存字段本身实现无锁化更新,减少锁竞争:
-- 方案1:用版本号(推荐)
UPDATE product_stock
SET stock = stock - 1, version = version + 1, updated_at = NOW()
WHERE product_id = ? AND version = ? AND stock >= 1;-- 方案2:用库存字段本身(适合简单场景)
UPDATE product_stock
SET stock = stock - 1, updated_at = NOW()
WHERE product_id = ? AND stock = ?; -- 扣减前先查询当前库存,作为条件
- 原理:更新时校验“扣减前的库存状态”,若期间库存被其他事务修改(版本号变化或库存值变化),则更新失败(返回行数为0),应用层需重试或返回失败。
- 优势:无锁竞争,吞吐量高,适合普通商品日常销售。
- 注意:高并发下可能出现多次重试失败,需设置合理重试次数(如3次),避免无限重试导致性能下降。
3. 库存预扣减 + 事务补偿
针对 秒杀、大促 等场景,提前将库存从“可用库存”移到“预占库存”,后续再确认或释放:
-- 1. 预占库存(下单时)
UPDATE product_stock
SET available_stock = available_stock - 1, reserved_stock = reserved_stock + 1
WHERE product_id = ? AND available_stock >= 1;-- 2. 确认扣减(支付成功后)
UPDATE product_stock
SET reserved_stock = reserved_stock - 1, sold_stock = sold_stock + 1
WHERE product_id = ? AND reserved_stock >= 1;-- 3. 释放预占(超时未支付)
UPDATE product_stock
SET available_stock = available_stock + 1, reserved_stock = reserved_stock - 1
WHERE product_id = ? AND reserved_stock >= 1;
- 原理:通过“可用库存→预占库存→已售库存”的状态流转,将“下单”和“最终扣减”分离,避免用户未支付却占用库存导致的超卖风险。
- 优势:结合定时任务(如XX分钟未支付自动释放),灵活应对下单后未支付的场景,提升库存利用率。
二、缓存层:减轻数据库压力,前置拦截无效请求
高并发场景下(如秒杀),直接操作数据库会导致性能瓶颈,需用缓存(如Redis)前置处理库存请求。
1. Redis预存库存 + 原子扣减
将商品库存提前加载到Redis,下单时先在Redis中扣减,成功后再异步同步到数据库:
// 1. 初始化:将数据库库存加载到Redis
redisTemplate.opsForValue().set("stock:" + productId, initialStock);// 2. 扣减库存:用Redis的decr原子操作
Long remainStock = redisTemplate.opsForValue().decrement("stock:" + productId);
if (remainStock != null && remainStock >= 0) {// Redis扣减成功,发送消息到MQ,异步同步到数据库mqTemplate.send("stock-sync-topic", productId);return "下单成功";
} else {// Redis扣减失败(库存不足),直接返回return "库存不足";
}
- 原理:Redis的
decrement
是原子操作,同一时间只有一个请求能成功扣减,避免并发冲突;数据库同步通过消息队列异步执行,不阻塞主流程。 - 优势:Redis性能远高于数据库,能支撑每秒10万级的并发请求,适合秒杀等高流量场景。
- 注意:需处理“Redis与数据库一致性”问题(如Redis扣减后数据库同步失败,可通过定时任务校验修复);防止Redis宕机导致库存丢失(需持久化配置+主从架构)。
2. 缓存预热 + 库存隔离
- 缓存预热:大促前将商品库存提前加载到Redis,避免高峰期缓存穿透(大量请求直接打到数据库)。
- 库存隔离:将“秒杀库存”和“日常库存”在缓存中分开存储(如
seckill:stock:1001
和normal:stock:1001
),避免秒杀流量影响日常销售。
三、应用层:控制流量,减少无效竞争
通过限流、排队等手段,从源头减少并发请求对库存系统的冲击。
1. 接口限流(防止流量过载)
- 前端限流:按钮置灰、倒计时,避免用户重复点击;
- 后端限流:用令牌桶/漏桶算法(如Guava RateLimiter、Sentinel)限制接口QPS,超出部分直接返回“系统繁忙”:
// Sentinel限流示例:限制/product/seckill接口每秒最多1000请求
@SentinelResource(value = "seckill", blockHandler = "handleSeckillBlock")
@PostMapping("/product/seckill")
public Result seckill(...) { ... }// 限流降级处理
public Result handleSeckillBlock(...) {return Result.error("请求过于频繁,请稍后再试");
}
2. 队列化处理(将并发转为串行)
用消息队列(如RabbitMQ、Kafka)将所有下单请求排队,由消费者按顺序处理,避免同时操作库存:
// 1. 接收下单请求,直接放入队列
@PostMapping("/order/submit")
public Result submitOrder(OrderDTO dto) {mqTemplate.send("order-queue", dto); // 放入队列,立即返回“排队中”return Result.success("已进入排队,请等待结果");
}// 2. 消费者按顺序处理队列消息(单线程或固定线程池)
@RabbitListener(queues = "order-queue")
public void processOrder(OrderDTO dto) {// 此处执行库存扣减、下单逻辑(串行处理,无并发冲突)reduceStock(dto.getProductId());createOrder(dto);
}
- 原理:通过队列的FIFO特性,将高并发请求转为串行处理,库存操作天然有序,避免超卖。
- 优势:实现简单,适合秒杀等“流量集中但总量可控”的场景(如1万件商品,只需处理1万条队列消息)。
四、分布式协调:跨服务/跨节点的库存一致性
在分布式系统(多服务实例、多数据库节点)中,需保证不同节点对库存的操作一致。
1. 分布式锁(控制跨节点并发)
用Redis的 SETNX
或ZooKeeper的临时节点实现分布式锁,确保同一商品在多节点间只有一个操作能执行:
// Redis分布式锁示例
String lockKey = "lock:stock:" + productId;
Boolean locked = redisTemplate.opsForValue().setIfAbsent(lockKey, "1", 10, TimeUnit.SECONDS);
if (Boolean.TRUE.equals(locked)) {try {// 获得锁,执行库存扣减reduceStock(productId);} finally {// 释放锁redisTemplate.delete(lockKey);}
} else {// 未获得锁,重试或返回失败return "系统繁忙,请重试";
}
- 注意:需设置锁超时时间,避免服务宕机导致锁永久持有;推荐用Redisson等成熟框架,处理锁自动续期、重入等问题。
2. 最终一致性方案(柔性事务)
对于非核心商品(允许短暂不一致,最终一致),可采用TCC(Try-Confirm-Cancel)或SAGA模式:
- TCC:Try阶段预扣减库存,Confirm阶段确认扣减,Cancel阶段回滚预扣减;
- SAGA:将库存扣减拆分为“扣减→补偿”步骤,若扣减失败,自动执行补偿操作(如恢复库存)。
五、监控与兜底:发现并修复异常
即使多层防护,仍可能因极端情况(如网络分区、缓存穿透)导致库存异常,需通过监控和兜底方案补救:
- 实时监控:监控库存字段(
available_stock
、sold_stock
)的差值,若sold_stock > 初始库存
,立即告警; - 定时校验:定时比对Redis库存与数据库库存,发现不一致时自动修复(以数据库为准);
- 人工兜底:大促期间安排运维人员值守,异常时手动冻结商品、调整库存。
总结:多层防护的核心逻辑
高并发商城防超卖的本质是 “在性能与一致性之间找平衡”,不同业务场景选择不同方案组合:
- 秒杀/限量商品:
Redis原子扣减 + 数据库行锁 + 队列化 + 限流
(优先保证一致性和抗流量能力); - 日常销售商品:
数据库乐观锁 + 缓存预热
(优先保证吞吐量); - 分布式场景:
分布式锁 + 最终一致性方案
(保证跨节点一致性)。
没有“银弹”方案,需结合业务量级、库存精度要求、技术成本综合设计,核心原则是“多层防护,层层兜底”。