
在电商秒杀、限时促销、限量商品发售等场景中,“超卖” 是最致命的业务故障之一 —— 明明只备货 1000 件商品,最终却卖出 1200 件,不仅导致 “无货可发” 的用户投诉,还可能引发平台信誉危机与经济损失。超卖的本质是 “并发场景下库存读写不同步”,但不同业务规模(单体 / 分布式)、不同并发量级(千级 / 万级 / 十万级)的解决方案差异极大,盲目套用 “分布式锁” 可能导致性能瓶颈,仅用 “数据库乐观锁” 又扛不住高并发。
本文结合电商实战经验,梳理超卖问题的完整解决思路,从基础的数据库控制到高并发的 Redis 预扣减,覆盖全场景方案,帮你按需选择最优解。
超卖是指 “商品实际售出数量超过初始备货量”,最终导致库存为负的异常情况。典型场景:
超卖的本质是 “多线程 / 多服务同时读写库存,未做同步控制”,具体可分为 3 类场景:
冲突场景 | 技术原因 | 典型案例 |
|---|---|---|
单体应用并发读写 | 多线程同时读取库存(如读取到 1000),同时扣减(均扣为 999),最终多扣 1 次 | 单体电商秒杀,1000 并发下单导致超卖 10 件 |
分布式应用数据不一致 | 多服务实例操作同一数据库,未做分布式同步,各实例独立扣减库存 | 3 个服务实例同时处理订单,库存从 1000 扣至 997,实际应扣 3 次,却扣成 997(无超卖)?不,若读取时均为 1000,扣减后均为 999,最终库存 999,超卖 2 次 |
异步处理库存延迟 | 库存扣减用异步队列,队列堆积导致 “下单成功但库存未及时扣减”,后续用户继续下单 | 秒杀峰值时,库存扣减 MQ 队列堆积 5 分钟,期间用户持续下单,导致超卖 |
关键结论:超卖的核心是 “读库存” 与 “扣库存” 两个操作非原子性 —— 若能让 “读 + 扣” 成为不可分割的原子操作,就能从根本上避免超卖。
单体应用并发较低(如日常促销,非秒杀),无需复杂中间件,仅通过数据库控制即可解决超卖,优先选择 “低侵入、易落地” 的方案。
通过 “订单表 + 商品 ID + 用户 ID” 的唯一索引,确保 “同一用户对同一商品只能创建 1 个有效订单”,同时结合 “库存非负校验”,间接防止超卖:
CREATE TABLE `order` ( `id` bigint NOT NULL AUTO_INCREMENT COMMENT '订单ID', `user_id` bigint NOT NULL COMMENT '用户ID', `product_id` bigint NOT NULL COMMENT '商品ID', `order_status` tinyint NOT NULL DEFAULT 0 COMMENT '订单状态:0=待支付,1=已支付,2=已取消', `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', PRIMARY KEY (`id`), -- 唯一索引:同一用户对同一商品只能创建1个待支付/已支付订单 UNIQUE KEY `uk_user_product_status` (`user_id`, `product_id`, `order_status`) COMMENT '防止同一用户重复下单') ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT '订单表';CREATE TABLE `product` ( `id` bigint NOT NULL AUTO_INCREMENT COMMENT '商品ID', `product_name` varchar(100) NOT NULL COMMENT '商品名称', `stock` int NOT NULL DEFAULT 0 COMMENT '库存数量', `version` int NOT NULL DEFAULT 0 COMMENT '版本号(乐观锁用)', PRIMARY KEY (`id`)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT '商品表';@Service@Transactionalpublic class OrderService { @Autowired private OrderMapper orderMapper; @Autowired private ProductMapper productMapper; /** * 单体应用下单(唯一索引防重复下单+库存校验) */ public OrderDTO createOrder(Long userId, Long productId) { // 步骤1:查询商品库存(判断是否有货) Product product = productMapper.selectById(productId); if (product == null || product.getStock() <= 0) { throw new BusinessException("商品已售罄"); } // 步骤2:创建订单(唯一索引防止重复下单) Order order = new Order(); order.setUserId(userId); order.setProductId(productId); order.setOrderStatus(0); // 待支付 try { orderMapper.insert(order); } catch (DuplicateKeyException e) { // 唯一索引冲突,说明用户已下单 throw new BusinessException("您已下单,请勿重复购买"); } // 步骤3:扣减库存(库存非负校验) int updateCount = productMapper.decreaseStock(productId, 1); if (updateCount == 0) { // 扣减失败(可能其他线程已扣完库存),回滚事务 throw new BusinessException("商品已售罄,下单失败"); } // 步骤4:返回订单信息 OrderDTO orderDTO = convert(order, product); return orderDTO; }}// ProductMapper.xml 扣减库存SQL(带库存非负校验)<update id="decreaseStock"> UPDATE product SET stock = stock - #{count} WHERE id = #{productId} AND stock > 0; -- 关键:确保库存不会扣为负</update>通过数据库的 “行锁”(SELECT ... FOR UPDATE),锁定商品库存行,确保 “读库存” 与 “扣库存” 的原子性:
// ProductMapper.javaProduct selectByIdForUpdate(Long productId);// ProductMapper.xml(悲观锁查询)<select id="selectByIdForUpdate" resultType="com.example.Product"> SELECT id, product_name, stock, version FROM product WHERE id = #{productId} FOR UPDATE; -- 行锁:锁定该商品行,其他线程无法修改</select>@Service@Transactionalpublic class OrderService { public OrderDTO createOrderWithPessimisticLock(Long userId, Long productId) { // 步骤1:悲观锁查询商品(锁定行,其他线程等待) Product product = productMapper.selectByIdForUpdate(productId); if (product == null || product.getStock() <= 0) { throw new BusinessException("商品已售罄"); } // 步骤2:扣减库存(无需额外校验,锁已保证原子性) int updateCount = productMapper.decreaseStock(productId, 1); if (updateCount == 0) { throw new BusinessException("下单失败"); } // 步骤3:创建订单(可加唯一索引防重复下单) Order order = new Order(); order.setUserId(userId); order.setProductId(productId); orderMapper.insert(order); return convert(order, product); }}乐观锁不主动锁定数据,而是通过 “版本号” 或 “库存字段” 判断扣减前库存是否被修改:
<!-- ProductMapper.xml 乐观锁扣减库存 --><update id="decreaseStockWithVersion"> UPDATE product SET stock = stock - #{count}, version = version + 1 -- 版本号自增 WHERE id = #{productId} AND stock > 0 AND version = #{version}; -- 关键:版本号匹配才更新</update>@Service@Transactionalpublic class OrderService { // 最大重试次数(避免无限循环) private static final int MAX_RETRY_COUNT = 3; public OrderDTO createOrderWithOptimisticLock(Long userId, Long productId) { int retryCount = 0; while (retryCount < MAX_RETRY_COUNT) { // 步骤1:查询商品(含版本号) Product product = productMapper.selectById(productId); if (product == null || product.getStock() <= 0) { throw new BusinessException("商品已售罄"); } // 步骤2:乐观锁扣减库存 int updateCount = productMapper.decreaseStockWithVersion( productId, 1, product.getVersion() ); if (updateCount > 0) { // 步骤3:扣减成功,创建订单 Order order = new Order(); order.setUserId(userId); order.setProductId(productId); orderMapper.insert(order); return convert(order, product); } // 步骤4:扣减失败,重试(间隔10ms,避免CPU空转) retryCount++; try { Thread.sleep(10); } catch (InterruptedException e) { Thread.currentThread().interrupt(); break; } } // 重试次数用尽,返回失败 throw new BusinessException("下单人数过多,请稍后重试"); }}分布式应用(多服务实例、多数据库分库分表)或中高并发(如秒杀 QPS=5000)场景,数据库方案已无法支撑,需引入 Redis、消息队列等中间件,通过 “缓存预扣减 + 异步同步” 提升性能,同时保证不超卖。
通过 Redis 分布式锁,确保 “多服务实例对同一商品的库存扣减” 互斥,实现分布式环境下的原子操作:
<dependency> <groupId>org.redisson</groupId> <artifactId>redisson-spring-boot-starter</artifactId> <version>3.23.3</version></dependency>@Servicepublic class StockPreloadService { @Autowired private RedissonClient redissonClient; @Autowired private ProductMapper productMapper; /** * 库存预热:将数据库库存加载到Redis(秒杀前执行) */ public void preloadStockToRedis(Long productId) { // 1. 查询数据库库存 Product product = productMapper.selectById(productId); if (product == null) { throw new BusinessException("商品不存在"); } // 2. 存储到Redis(key=stock:product:123,value=库存数) RAtomicLong redisStock = redissonClient.getAtomicLong("stock:product:" + productId); redisStock.set(product.getStock()); }}@Service@Transactionalpublic class DistributedOrderService { @Autowired private RedissonClient redissonClient; @Autowired private ProductMapper productMapper; @Autowired private OrderMapper orderMapper; public OrderDTO createOrderWithDistributedLock(Long userId, Long productId) { // 步骤1:获取分布式锁(商品ID为锁key,过期时间30秒,自动释放) RLock lock = redissonClient.getLock("lock:product:" + productId); try { // 尝试加锁,最多等待5秒,5秒内未获取到锁则失败 boolean locked = lock.tryLock(5, 30, TimeUnit.SECONDS); if (!locked) { throw new BusinessException("下单人数过多,请稍后重试"); } // 步骤2:查询Redis库存(减少数据库访问) RAtomicLong redisStock = redissonClient.getAtomicLong("stock:product:" + productId); long currentStock = redisStock.get(); if (currentStock <= 0) { throw new BusinessException("商品已售罄"); } // 步骤3:扣减Redis库存(原子操作) boolean decrSuccess = redisStock.decrementAndGet() >= 0; if (!decrSuccess) { // 若扣减后为负,回补Redis库存(避免超卖) redisStock.incrementAndGet(); throw new BusinessException("商品已售罄"); } // 步骤4:扣减数据库库存(最终一致性,可异步) int updateCount = productMapper.decreaseStock(productId, 1); if (updateCount == 0) { // 数据库扣减失败,回补Redis库存 redisStock.incrementAndGet(); throw new BusinessException("下单失败"); } // 步骤5:创建订单(唯一索引防重复下单) Order order = new Order(); order.setUserId(userId); order.setProductId(productId); try { orderMapper.insert(order); } catch (DuplicateKeyException e) { // 重复下单,回补Redis和数据库库存 redisStock.incrementAndGet(); productMapper.increaseStock(productId, 1); // 库存回补SQL throw new BusinessException("您已下单,请勿重复购买"); } return convert(order, productMapper.selectById(productId)); } catch (InterruptedException e) { Thread.currentThread().interrupt(); throw new BusinessException("下单异常,请重试"); } finally { // 步骤6:释放锁(确保锁一定释放) if (lock.isHeldByCurrentThread()) { lock.unlock(); } } }}高并发秒杀(如 QPS=10 万)场景下,Redis 分布式锁的锁竞争会成为瓶颈,需用 “Redis 预扣减 + 消息队列异步同步” 实现 “无锁高并发”:
@Servicepublic class SeckillOrderService { @Autowired private RedissonClient redissonClient; @Autowired private KafkaTemplate<String, String> kafkaTemplate; @Autowired private OrderMapper orderMapper; /** * 秒杀下单:Redis预扣减+MQ异步同步 */ public OrderDTO seckillOrder(Long userId, Long productId) { // 步骤1:Redis原子扣减库存(无锁,高并发) RAtomicLong redisStock = redissonClient.getAtomicLong("stock:product:" + productId); long remainingStock = redisStock.decrementAndGet(); if (remainingStock < 0) { // 库存不足,回补Redis(避免超卖) redisStock.incrementAndGet(); throw new BusinessException("手慢了,商品已售罄"); } // 步骤2:生成订单(仅存必要信息,快速返回) Order order = new Order(); order.setUserId(userId); order.setProductId(productId); order.setOrderStatus(0); // 待支付 String orderNo = generateOrderNo(); // 生成唯一订单号 order.setOrderNo(orderNo); orderMapper.insertSelective(order); // 仅插入必要字段,提升速度 // 步骤3:发送MQ消息(异步同步库存到数据库+处理支付) SeckillMqMsg msg = new SeckillMqMsg(); msg.setOrderNo(orderNo); msg.setProductId(productId); msg.setUserId(userId); kafkaTemplate.send("seckill-order-topic", JSON.toJSONString(msg)); // 步骤4:快速返回订单信息(用户无需等待库存同步完成) OrderDTO orderDTO = new OrderDTO(); orderDTO.setOrderNo(orderNo); orderDTO.setStatus("待支付"); orderDTO.setMsg("下单成功,请在15分钟内支付"); return orderDTO; } // 生成唯一订单号(时间戳+随机数+用户ID后4位) private String generateOrderNo() { return new SimpleDateFormat("yyyyMMddHHmmss").format(new Date()) + RandomUtils.nextInt(1000, 9999) + Thread.currentThread().getId() % 10000; }}@Componentpublic class SeckillOrderConsumer { @Autowired private ProductMapper productMapper; @Autowired private OrderMapper orderMapper; @Autowired private RedissonClient redissonClient; @KafkaListener(topics = "seckill-order-topic") public void processSeckillOrder(String msgStr) { SeckillMqMsg msg = JSON.parseObject(msgStr, SeckillMqMsg.class); String orderNo = msg.getOrderNo(); Long productId = msg.getProductId(); try { // 步骤1:异步扣减数据库库存(最终一致性) int updateCount = productMapper.decreaseStock(productId, 1); if (updateCount == 0) { // 数据库库存不足(极端情况,Redis预扣减有误),回补Redis库存 RAtomicLong redisStock = redissonClient.getAtomicLong("stock:product:" + productId); redisStock.incrementAndGet(); // 更新订单状态为“下单失败” orderMapper.updateStatusByOrderNo(orderNo, 3); // 3=下单失败 log.error("异步扣减库存失败,订单号:{},商品ID:{}", orderNo, productId); return; } // 步骤2:监听支付状态(如15分钟未支付,回补库存) listenPaymentStatus(orderNo, productId); } catch (Exception e) { log.error("处理秒杀订单异常,订单号:{}", orderNo, e); // 异常时回补Redis库存 RAtomicLong redisStock = redissonClient.getAtomicLong("stock:product:" + productId); redisStock.incrementAndGet(); orderMapper.updateStatusByOrderNo(orderNo, 3); } } // 监听支付状态:15分钟未支付,回补库存 private void listenPaymentStatus(String orderNo, Long productId) { // 用定时任务或延迟队列监听支付状态(如RabbitMQ延迟队列) // 若15分钟未支付: // 1. 回补Redis库存:redisStock.incrementAndGet() // 2. 回补数据库库存:productMapper.increaseStock(productId, 1) // 3. 更新订单状态:orderMapper.updateStatusByOrderNo(orderNo, 2) // 2=已取消 }}无论选择哪种方案,都需添加 “兜底措施”,应对极端情况(如 Redis 宕机、MQ 队列堆积):
定时(如每 5 分钟)对比 Redis 库存与数据库库存,发现不一致时修正:
@Scheduled(fixedRate = 300000) // 每5分钟执行一次public void checkStockConsistency() { // 1. 查询所有秒杀商品 List<Product> seckillProducts = productMapper.listSeckillProducts(); for (Product product : seckillProducts) { Long productId = product.getId(); int dbStock = product.getStock(); // 2. 查询Redis库存 RAtomicLong redisStock = redissonClient.getAtomicLong("stock:product:" + productId); long redisStockVal = redisStock.get(); // 3. 对比并修正(以数据库为准,或根据业务规则) if (dbStock != redisStockVal) { log.warn("库存不一致,商品ID:{},数据库库存:{},Redis库存:{}", productId, dbStock, redisStockVal); // 修正Redis库存为数据库库存 redisStock.set(dbStock); } }}在订单创建接口添加 “总订单量≤初始备货量” 的校验,即使前面的方案失效,仍能防止超卖:
// 下单前校验总订单量int totalOrderCount = orderMapper.countByProductId(productId);int initialStock = productMapper.selectInitialStock(productId); // 初始备货量if (totalOrderCount >= initialStock) { throw new BusinessException("商品已售罄");}用户下单后未支付(如 15 分钟超时),必须回补库存,避免 “占库存不付款” 导致的超卖:
业务规模 | 并发量级 | 推荐方案组合 | 核心优势 |
|---|---|---|---|
单体应用(中小电商) | ≤500QPS | 数据库乐观锁 + 唯一索引 | 无额外依赖,易落地 |
单体应用(促销活动) | 500-1000QPS | 数据库乐观锁 + Redis 缓存库存 | 兼顾性能与一致性 |
分布式应用(多服务) | 1000-10000QPS | Redis 分布式锁 + 数据库同步 | 分布式强一致,支持中高并发 |
高并发秒杀(大促) | ≥10000QPS | Redis 预扣减 + MQ 异步同步 + 库存对账 | 无锁高并发,响应时间短 |
超卖解决的本质是 “在性能与一致性之间找平衡”:
无论选择哪种方案,都需记住:没有 “银弹”,只有 “适合业务的方案”—— 结合自身并发量级、一致性要求、技术栈选择方案,同时添加 “库存对账、超时回补” 等兜底措施,才能彻底杜绝超卖,保障业务稳定。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。