彻底解决超卖问题:从单体到分布式的全场景技术方案

彻底解决超卖问题:从单体到分布式的全场景技术方案

在电商秒杀、限时促销、限量商品发售等场景中,"超卖" 是最致命的业务故障之一 ------ 明明只备货 1000 件商品,最终却卖出 1200 件,不仅导致 "无货可发" 的用户投诉,还可能引发平台信誉危机与经济损失。超卖的本质是 "并发场景下库存读写不同步",但不同业务规模(单体 / 分布式)、不同并发量级(千级 / 万级 / 十万级)的解决方案差异极大,盲目套用 "分布式锁" 可能导致性能瓶颈,仅用 "数据库乐观锁" 又扛不住高并发。

本文结合电商实战经验,梳理超卖问题的完整解决思路,从基础的数据库控制到高并发的 Redis 预扣减,覆盖全场景方案,帮你按需选择最优解。

一、先明确:什么是超卖?为什么会发生?

1. 超卖的定义

超卖是指 "商品实际售出数量超过初始备货量",最终导致库存为负的异常情况。典型场景:

  • 某商品初始库存 1000 件,秒杀活动中,1200 个用户成功下单,库存最终变为 - 200;
  • 用户下单后未支付,库存未及时回补,后续用户继续下单,导致总订单量超备货量。

2. 超卖的核心原因:并发下的 "库存读写冲突"

超卖的本质是 "多线程 / 多服务同时读写库存,未做同步控制",具体可分为 3 类场景:

冲突场景 技术原因 典型案例
单体应用并发读写 多线程同时读取库存(如读取到 1000),同时扣减(均扣为 999),最终多扣 1 次 单体电商秒杀,1000 并发下单导致超卖 10 件
分布式应用数据不一致 多服务实例操作同一数据库,未做分布式同步,各实例独立扣减库存 3 个服务实例同时处理订单,库存从 1000 扣至 997,实际应扣 3 次,却扣成 997(无超卖)?不,若读取时均为 1000,扣减后均为 999,最终库存 999,超卖 2 次
异步处理库存延迟 库存扣减用异步队列,队列堆积导致 "下单成功但库存未及时扣减",后续用户继续下单 秒杀峰值时,库存扣减 MQ 队列堆积 5 分钟,期间用户持续下单,导致超卖

关键结论 :超卖的核心是 "读库存 " 与 "扣库存" 两个操作非原子性 ------ 若能让 "读 + 扣" 成为不可分割的原子操作,就能从根本上避免超卖。

二、基础方案:单体应用超卖解决(并发≤1000QPS)

单体应用并发较低(如日常促销,非秒杀),无需复杂中间件,仅通过数据库控制即可解决超卖,优先选择 "低侵入、易落地" 的方案。

方案 1:数据库唯一索引 ------ 防止重复创建订单(间接防超卖)

原理

通过 "订单表 + 商品 ID + 用户 ID" 的唯一索引,确保 "同一用户对同一商品只能创建 1 个有效订单",同时结合 "库存非负校验",间接防止超卖:

  1. 下单时,先校验商品库存是否 > 0;
  1. 再尝试创建订单,唯一索引保证不会重复下单;
  1. 最后扣减库存(若库存不足,创建订单失败)。
实现步骤
  1. 订单表设计(唯一索引)
sql 复制代码
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 '订单表';
  1. 商品表设计(库存字段)
sql 复制代码
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 '商品表';
  1. 下单逻辑(Java 代码)
scss 复制代码
@Service
@Transactional
public 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>
适用场景与优缺点
  • 适用场景:单体应用、日常促销(并发≤500QPS)、需限制 "同一用户重复下单" 的场景;
  • 优点:实现简单,无额外依赖,同时解决 "重复下单" 与 "超卖";
  • 缺点:并发高时(如 1000QPS),唯一索引冲突频繁,用户体验差;扣库存与创建订单非原子操作,极端情况下仍可能超卖(如步骤 2 成功后,步骤 3 执行前,其他线程已扣完库存)。

方案 2:数据库悲观锁 ------ 强一致性,适合低并发

原理

通过数据库的 "行锁"(SELECT ... FOR UPDATE),锁定商品库存行,确保 "读库存" 与 "扣库存" 的原子性:

  1. 下单时,用悲观锁锁定商品行,其他线程需等待锁释放;
  1. 读取锁定后的库存,判断是否 > 0;
  1. 若有库存,扣减库存并创建订单;若无库存,释放锁并返回失败。
实现步骤(核心代码)
  1. ProductMapper 加悲观锁查询
sql 复制代码
// ProductMapper.java
Product 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>
  1. 下单逻辑(悲观锁版)
scss 复制代码
@Service
@Transactional
public 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);
    }
}
适用场景与优缺点
  • 适用场景:单体应用、低并发(≤300QPS)、对数据一致性要求极高的场景(如奢侈品秒杀);
  • 优点:强一致性,100% 避免超卖,无需担心并发冲突;
  • 缺点:悲观锁会阻塞其他线程,并发高时导致 "订单创建排队",响应时间变长(如 1000QPS 下,响应时间从 100ms 升至 500ms)。

方案 3:数据库乐观锁 ------ 无锁阻塞,适合中低并发

原理

乐观锁不主动锁定数据,而是通过 "版本号" 或 "库存字段" 判断扣减前库存是否被修改:

  1. 下单时,读取商品库存与版本号(如库存 1000,版本 1);
  1. 扣减库存时,用版本号做条件(WHERE version = 1),确保期间无其他线程修改;
  1. 若更新成功(影响行数 = 1),说明扣减有效;若失败(影响行数 = 0),说明库存已被修改,重试或返回失败。
实现步骤(版本号机制)
  1. 商品表版本号字段(已在方案 1 中定义,version字段);
  1. 扣减库存 SQL(带版本号校验)
xml 复制代码
<!-- ProductMapper.xml 乐观锁扣减库存 -->
<update id="decreaseStockWithVersion">
    UPDATE product 
    SET stock = stock - #{count},
        version = version + 1 -- 版本号自增
    WHERE id = #{productId} 
      AND stock > 0 
      AND version = #{version}; -- 关键:版本号匹配才更新
</update>
  1. 下单逻辑(乐观锁版,带重试)
scss 复制代码
@Service
@Transactional
public 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("下单人数过多,请稍后重试");
    }
}
适用场景与优缺点
  • 适用场景:单体应用、中低并发(≤1000QPS)、对响应时间敏感的场景(如日常电商促销);
  • 优点:无锁阻塞,并发性能优于悲观锁;实现简单,仅需数据库字段;
  • 缺点:重试机制可能导致部分用户下单失败(需友好提示);高并发下重试次数增加,响应时间变长。

三、进阶方案:分布式应用超卖解决(并发≤10000QPS)

分布式应用(多服务实例、多数据库分库分表)或中高并发(如秒杀 QPS=5000)场景,数据库方案已无法支撑,需引入 Redis、消息队列等中间件,通过 "缓存预扣减 + 异步同步" 提升性能,同时保证不超卖。

方案 4:Redis 分布式锁 ------ 分布式强一致性

原理

通过 Redis 分布式锁,确保 "多服务实例对同一商品的库存扣减" 互斥,实现分布式环境下的原子操作:

  1. 下单时,用商品 ID 生成分布式锁(如lock:product:123);
  1. 加锁成功后,查询 Redis 缓存库存(或数据库库存);
  1. 扣减库存(Redis + 数据库),释放锁;
  1. 加锁失败,重试或返回 "下单繁忙"。
实现步骤(基于 Redisson 分布式锁)
  1. 引入 Redisson 依赖(Maven)
xml 复制代码
<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson-spring-boot-starter</artifactId>
    <version>3.23.3</version>
</dependency>
  1. Redis 缓存库存预热(秒杀前执行)
java 复制代码
@Service
public 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());
    }
}
  1. 分布式锁下单逻辑
scss 复制代码
@Service
@Transactional
public 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();
            }
        }
    }
}
适用场景与优缺点
  • 适用场景:分布式应用、中高并发(≤10000QPS)、对一致性要求高的场景(如电商秒杀);
  • 优点:分布式环境下强一致,支持较高并发;Redis 库存查询减少数据库压力;
  • 缺点:依赖 Redis 集群(需保证 Redis 高可用);锁粒度为商品 ID,热门商品可能出现 "锁竞争"(可通过商品分片优化)。

方案 5:Redis 预扣减 + 消息队列异步同步 ------ 高并发秒杀(QPS≥10000)

原理

高并发秒杀(如 QPS=10 万)场景下,Redis 分布式锁的锁竞争会成为瓶颈,需用 "Redis 预扣减 + 消息队列异步同步" 实现 "无锁高并发":

  1. 秒杀前,将库存加载到 Redis(预扣减库存);
  1. 下单时,直接扣减 Redis 库存(原子操作,无锁),生成订单并发送到消息队列;
  1. 消息队列消费者异步同步库存到数据库,处理订单支付状态;
  1. 若用户未支付,异步回补 Redis 与数据库库存。
实现步骤(秒杀场景)
  1. Redis 预扣减下单(核心代码)
scss 复制代码
@Service
public 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;
    }
}
  1. MQ 消费者异步同步库存(Kafka 消费者)
java 复制代码
@Component
public 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=已取消
    }
}
适用场景与优缺点
  • 适用场景:高并发秒杀(QPS≥10000)、对响应时间要求极高的场景(如 "双十一" 秒杀);
  • 优点:无锁设计,支持极高并发;异步同步降低响应时间(下单响应时间 < 100ms);
  • 缺点:Redis 与数据库存在短暂不一致(需最终一致性);需处理 "未支付回补库存" 的复杂逻辑;依赖 MQ 与 Redis 的高可用。

四、终极保障:超卖防护的 3 个兜底措施

无论选择哪种方案,都需添加 "兜底措施",应对极端情况(如 Redis 宕机、MQ 队列堆积):

1. 库存对账机制

定时(如每 5 分钟)对比 Redis 库存与数据库库存,发现不一致时修正:

ini 复制代码
@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);
        }
    }
}

2. 库存上限限制

在订单创建接口添加 "总订单量≤初始备货量" 的校验,即使前面的方案失效,仍能防止超卖:

arduino 复制代码
// 下单前校验总订单量
int totalOrderCount = orderMapper.countByProductId(productId);
int initialStock = productMapper.selectInitialStock(productId); // 初始备货量
if (totalOrderCount >= initialStock) {
    throw new BusinessException("商品已售罄");
}

3. 支付超时回补

用户下单后未支付(如 15 分钟超时),必须回补库存,避免 "占库存不付款" 导致的超卖:

  • 用延迟队列(如 RabbitMQ 延迟交换机)监听订单支付状态;
  • 超时未支付,执行 "库存回补"(Redis + 数据库)与 "订单取消"。

五、方案选型速查表

业务规模 并发量级 推荐方案组合 核心优势
单体应用(中小电商) ≤500QPS 数据库乐观锁 + 唯一索引 无额外依赖,易落地
单体应用(促销活动) 500-1000QPS 数据库乐观锁 + Redis 缓存库存 兼顾性能与一致性
分布式应用(多服务) 1000-10000QPS Redis 分布式锁 + 数据库同步 分布式强一致,支持中高并发
高并发秒杀(大促) ≥10000QPS Redis 预扣减 + MQ 异步同步 + 库存对账 无锁高并发,响应时间短

六、避坑指南:超卖解决的 5 个常见错误

  1. 错误 1:仅用 Redis incr/decr,不做库存非负校验
    • 问题:redisStock.decrementAndGet()可能导致库存为负(如库存 1,两个线程同时扣减,变为 - 1);
    • 解决:扣减后判断是否≥0,否则回补(redisStock.incrementAndGet())。
  1. 错误 2:分布式锁未设置过期时间
    • 问题:服务宕机导致锁未释放,其他线程无法下单;
    • 解决:用 Redisson 的tryLock(等待时间, 过期时间, 单位),自动释放锁。
  1. 错误 3:忽略 "重复下单" 问题
    • 问题:仅防超卖,未防同一用户重复下单,导致 "一个用户买多件" 超卖;
    • 解决:订单表加 "user_id+product_id" 唯一索引,或 Redis 记录用户下单次数(user:order:count:userId:productId)。
  1. 错误 4:异步同步库存无重试机制
    • 问题:MQ 消费失败导致数据库库存未扣减,Redis 与数据库不一致;
    • 解决:MQ 消费者添加重试机制(如重试 3 次,失败后存入死信队列人工处理)。
  1. 错误 5:秒杀前未预热库存到 Redis
    • 问题:秒杀开始后,大量请求查询数据库,导致数据库宕机;
    • 解决:秒杀前 10 分钟,将库存从数据库加载到 Redis,秒杀时优先查 Redis。

总结:超卖解决的核心逻辑

超卖解决的本质是 "在性能与一致性之间找平衡":

  • 低并发场景:优先用数据库方案(乐观锁 / 悲观锁),简单可靠;
  • 中高并发场景:用 Redis 分布式锁,兼顾一致性与性能;
  • 高并发秒杀场景:用 Redis 预扣减 + MQ 异步同步,牺牲短暂一致性换极致性能。

无论选择哪种方案,都需记住:没有 "银弹",只有 "适合业务的方案" ------ 结合自身并发量级、一致性要求、技术栈选择方案,同时添加 "库存对账、超时回补" 等兜底措施,才能彻底杜绝超卖,保障业务稳定。

相关推荐
8***29311 小时前
能懂!基于Springboot的用户增删查改(三层设计模式)
spring boot·后端·设计模式
IT_陈寒1 小时前
Python高手都在用的5个隐藏技巧,让你的代码效率提升50%
前端·人工智能·后端
拾忆,想起2 小时前
Dubbo动态配置实时生效全攻略:零停机实现配置热更新
分布式·微服务·性能优化·架构·dubbo
Qiuner2 小时前
Spring Boot 机制二:配置属性绑定 Binder 源码解析(ConfigurationProperties 全链路)
java·spring boot·后端·spring·binder
Victor3562 小时前
Redis(151)Redis的内存使用如何监控?
后端
Victor3562 小时前
Redis(150)Redis的性能瓶颈如何排查?
后端
豆浆whisky3 小时前
Go并发模式选择指南:找到最适合你项目的并发方案|Go语言进阶(19)
开发语言·后端·golang
Y***h18710 小时前
第二章 Spring中的Bean
java·后端·spring
稚辉君.MCA_P8_Java11 小时前
DeepSeek 插入排序
linux·后端·算法·架构·排序算法