高并发下如何防止商品超卖少卖

高并发下如何防止商品超卖少卖

核心问题本质⚠️

超卖 / 少卖的根源都是 "读 - 改 - 写" 非原子操作 导致的竞态条件:

  • 超卖🛑:多个线程同时读到相同库存,都执行扣减,最终库存为负
    • 库存扣成负数,实际没货了系统却还接单,最常见的致命问题。
  • 少卖📉:扣减成功但订单失败 / 订单成功但扣减失败,导致库存与订单数不一致
    • 明明有库存,系统却误判售罄拒绝用户,白白损失销量。通常是并发竞争失败或库存同步延迟导致。

分层解决方案(从低到高并发)

1. 数据库层基础方案(QPS < 1000)

适合初创项目或低流量场景,纯数据库实现,无额外依赖。

(1)直接扣减法(✅ 数据库层最优解)

利用 MySQL 单条 UPDATE 语句的原子性,从根源杜绝超卖:

sql 复制代码
-- 核心SQL:只有库存>0时才执行扣减,返回受影响行数
UPDATE goods SET stock = stock - 1 WHERE id = #{goodsId} AND stock > 0;
  • 优点:最简单、无并发问题、性能优于乐观锁
  • 注意:必须通过受影响行数判断是否扣减成功,不能先查再改

(2)乐观锁(版本号机制)

sql 复制代码
-- 先查版本号
SELECT version FROM goods WHERE id = #{goodsId};
-- 扣减时校验版本号
UPDATE goods SET stock = stock -1, version = version +1 
WHERE id = #{goodsId} AND version = #{oldVersion} AND stock >0;
  • 缺点:高并发下大量请求失败,用户体验差,不适合秒杀场景

(3)悲观锁(for update)

sql 复制代码
BEGIN;
SELECT stock FROM goods WHERE id = #{goodsId} FOR UPDATE;
-- 业务逻辑
UPDATE goods SET stock = stock -1 WHERE id = #{goodsId};
COMMIT;

缺点:串行执行,性能极差,容易死锁,不推荐

2. 分布式锁方案(QPS 1000-5000)

适合中等并发场景,通过分布式锁将并行请求串行化。

  • 推荐实现:Redisson 的可重入锁(自动续期、解决锁过期问题)

  • 核心流程:

    • 用户下单,尝试获取商品级分布式锁
    • 获取成功→查库存→扣减→生成订单→释放锁
    • 获取失败→返回 "抢购失败"
  • 缺点:本质还是串行,并发上限低,无法支撑秒杀级流量

3. Redis 预扣减 + 最终一致性(✅ 大厂主流,QPS > 5000)

将库存热点数据放到 Redis,用 Redis 扛高并发,数据库做最终存储,是目前秒杀系统的标准方案。

核心流程图

graph LR A用户下单请求 --> BRedis执行Lua脚本扣减库存 B --> C{扣减成功?} C -->|否| D返回库存不足 C -->|是| E生成订单并写入DB E --> F发送'订单创建成功'消息到MQ F --> G消费者异步扣减数据库库存 H订单超时未支付 --> I延迟队列触发库存回滚 I --> B J定时对账系统 --> K每日核对Redis与DB库存 K --> L修正不一致数据
graph TD A🚀 海量用户请求 --> B{网关限流/风控}; B -->|合法请求| C⚡ Redis 执行Lua扣库存脚本; C -->|剩余库存>=0| D✅ 扣减成功,生成预订单; C -->|剩余库存<0| E❌ 回滚,标记售罄; D --> F📨 发送可靠消息至MQ; F --> G📦 消费者异步扣减DB库存; G --> H🎉 订单完成; E --> I👋 返回"已售罄";

关键细节

  1. Redis 扣减必须用 Lua 脚本:保证 "判断库存 - 扣减库存" 原子性
lua 复制代码
-- 核心Lua脚本
local stock = redis.call('GET', KEYS[1])
if not stock or tonumber(stock) < tonumber(ARGV[1]) then
    return 0
end
redis.call('DECRBY', KEYS[1], ARGV[1])
return 1
  1. 库存预热:活动开始前将商品库存同步到 Redis
  2. 防缓存穿透:不存在的商品直接返回,不打 DB
  3. 库存回滚:用 RocketMQ 延迟队列处理超时未支付订单

这套方案如何解决两个核心问题:

  • 防超卖 ✅:Redis 单线程执行 Lua,脚本里的 if stock >= quantity 判断和 decrby 操作是原子性的。库存剩最后一件时,最多只有一个请求能扣成功,其他请求都会走 else 分支,完美防超卖。
  • 防少卖 🔒:只要库存够,原子操作就一定成功,不会有"库存在那但就是抢不到"的并发冲突失败。Redis 极高的吞吐量也能让更多请求得到处理,大大减少因系统性能瓶颈导致的少卖。

进阶保障:应对异常,彻底堵上少卖的窟窿 🧰

Redis 虽然快,但也得把后续路铺好,才能彻底防止各种原因造成的少卖。

1. 库存预热与对账,防止数据不同步

系统启动时,从 DB 把真实库存加载到 Redis。同时,跑一个定时任务,每隔几分钟核对 Redis 和 DB 的库存差异。一旦发现 Redis 数据因异常丢失,立刻修复,避免"数据库有货、缓存无货"导致的少卖。

2. 订单取消回补,释放被锁定的库存 🔄

用户下单未支付,库存得还回去,而且要原子化地还回 Redis。

lua 复制代码
-- 回补库存脚本,同样简单原子
redis.call('incrby', KEYS[1], ARGV[1])
-- 如果之前标记了售罄,回补后要删除标记,让商品重新上架
redis.call('del', KEYS[2])

这一步不做,就会造成"订单取消了库存却没加回来"的少卖。

3. 库存分桶,突破单Key热点瓶颈 🪣

如果某个商品实在火到爆,单个 Redis Key 会成为性能热点。可以把总库存拆成多个分片。例如,1000件库存拆成 stock:1001:1stock:1001:10,每个存100件。用户请求随机路由到一个分片去扣库存。一个分片扣完再找下一个,既能水平扩展,又能避免单分片售罄导致其他请求失败,进一步防止少卖。

防少卖的兜底保障机制🚨

少卖比超卖更难排查,必须做多层兜底:

  1. MQ 重试机制:数据库扣减失败时,MQ 自动重试 3 次
  2. 死信队列:重试失败的消息进入死信队列,人工干预
  3. 每日对账系统:凌晨批量核对 "订单支付数" 与 "库存扣减数",修正不一致
  4. Redis 库存兜底:当 Redis 库存为 0 时,再查一次数据库,防止 Redis 数据丢失导致的少卖

方案对比表

方案 支持并发 优点 缺点 适用场景
数据库直接扣减 <1000 简单无依赖、绝对一致 并发上限低 普通商品下单
分布式锁 1000-5000 逻辑简单、一致性好 串行执行、性能一般 中等流量活动
Redis 预扣减 + 最终一致性 >5000 性能极高、可横向扩展 实现复杂、最终一致 秒杀、大促场景

总结与选型建议💡

我在实际项目中,会根据流量量级选择方案:

  • 普通商品下单:直接用数据库 UPDATE 扣减,简单高效
  • 中小型活动:用Redisson 分布式锁,开发成本低
  • 秒杀 / 大促:必须用Redis 预扣减 + MQ 异步同步 + 定时对账,这是目前大厂验证过的最优

核心代码实现(生产级)💻

面试官好,我继续补充生产级核心代码实现实际项目中遇到的技术难点及解决方案,这些都是我在电商大促项目中踩过坑后沉淀的经验。

1. Redis Lua 脚本原子扣减库存(✅ 第一技术亮点)

绝对不能用GET+DECR两条命令,必须用单 Lua 脚本保证 "判断库存 - 扣减库存" 原子性,这是 Redis 层防超卖的基石。

lua 复制代码
-- 文件名:stock_deduct.lua
-- KEYS[1]: 库存key  ARGV[1]: 扣减数量  ARGV[2]: 幂等键(防重复扣减)
local idempotentKey = ARGV[2]
-- 1. 先校验幂等性,防止重复下单扣减
if redis.call('EXISTS', idempotentKey) == 1 then
    return -1 -- -1表示重复请求
end

-- 2. 校验库存是否充足
local stock = redis.call('GET', KEYS[1])
if not stock or tonumber(stock) < tonumber(ARGV[1]) then
    return 0 -- 0表示库存不足
end

-- 3. 原子扣减库存+记录幂等键(过期时间1分钟)
redis.call('DECRBY', KEYS[1], ARGV[1])
redis.call('SET', idempotentKey, '1', 'EX', 60)
return 1 -- 1表示扣减成功

Java 调用代码(Spring Data Redis)

java 复制代码
@Service
public class RedisStockService {
    @Autowired
    private StringRedisTemplate redisTemplate;
    
    // 预加载Lua脚本,避免每次编译
    private final DefaultRedisScript<Long> deductScript;
    
    public RedisStockService() {
        deductScript = new DefaultRedisScript<>();
        deductScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("lua/stock_deduct.lua")));
        deductScript.setResultType(Long.class);
    }
    
    /**
     * 原子扣减库存
     * @param goodsId 商品ID
     * @param quantity 扣减数量
     * @param userId 用户ID(用于生成幂等键)
     * @return 1:成功 0:库存不足 -1:重复请求
     */
    public Long deductStock(Long goodsId, Integer quantity, Long userId) {
        String stockKey = "goods:stock:" + goodsId;
        String idempotentKey = "order:idempotent:" + userId + ":" + goodsId;
        return redisTemplate.execute(deductScript, 
            Collections.singletonList(stockKey), 
            quantity.toString(), 
            idempotentKey);
    }
}

✅ 技术亮点:

  • 单 Lua 脚本执行,Redis 单线程特性保证全程原子性
  • 内置幂等性校验,从源头防止用户重复点击导致的重复扣减
  • 预加载脚本,避免每次请求编译 Lua 的性能开销

2. Redisson 分布式锁实现(✅ 生产级锁方案)

适合中等并发场景,解决原生 Redis 锁 "锁过期释放但业务未执行完" 的痛点。

java 复制代码
@Service
public class DistributedLockService {
    @Autowired
    private RedissonClient redissonClient;
    
    public boolean createOrderWithLock(Long goodsId, Integer quantity, Long userId) {
        String lockKey = "lock:goods:" + goodsId;
        // 等待时间0秒(抢不到直接失败),持有时间-1(开启看门狗自动续期)
        RLock lock = redissonClient.getLock(lockKey);
        
        try {
            if (!lock.tryLock(0, -1, TimeUnit.SECONDS)) {
                return false;
            }
            
            // 执行业务逻辑:查库存→扣减数据库→生成订单
            return orderService.createOrder(goodsId, quantity, userId);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            return false;
        } finally {
            // 确保锁释放
            if (lock.isHeldByCurrentThread()) {
                lock.unlock();
            }
        }
    }
}

✅ 技术亮点:

  • Redisson 看门狗机制:业务执行超时自动续期,避免锁提前释放导致的超卖
  • 可重入锁设计,支持嵌套业务逻辑
  • 自动解锁机制,避免死锁

3. RocketMQ 延迟队列库存回滚(✅ 可靠回滚方案)

解决 "订单超时未支付" 导致的库存占用问题,比定时任务轮询性能高 10 倍以上。

java 复制代码
@Service
public class OrderTimeoutService {
    @Autowired
    private RocketMQTemplate rocketMQTemplate;
    @Autowired
    private RedisStockService redisStockService;
    
    // 订单超时时间:30分钟
    private static final int ORDER_TIMEOUT_LEVEL = 16; // RocketMQ延迟级别16对应30分钟
    
    /**
     * 订单创建成功后,发送延迟消息
     */
    public void sendTimeoutCheckMessage(Long orderId, Long goodsId, Integer quantity) {
        OrderTimeoutMessage message = new OrderTimeoutMessage(orderId, goodsId, quantity);
        rocketMQTemplate.syncSend("order-timeout-topic", 
            MessageBuilder.withPayload(message).build(), 
            3000, 
            ORDER_TIMEOUT_LEVEL);
    }
    
    /**
     * 消费者处理超时订单,回滚库存
     */
    @RocketMQMessageListener(topic = "order-timeout-topic", consumerGroup = "order-timeout-consumer")
    public class OrderTimeoutConsumer implements RocketMQListener<OrderTimeoutMessage> {
        @Override
        public void onMessage(OrderTimeoutMessage message) {
            Long orderId = message.getOrderId();
            // 1. 查询订单状态
            Order order = orderService.getById(orderId);
            if (order == null || order.getStatus() != OrderStatus.UNPAID) {
                return; // 订单已支付或不存在,无需回滚
            }
            
            // 2. 原子回滚Redis库存
            String stockKey = "goods:stock:" + message.getGoodsId();
            redisTemplate.opsForValue().increment(stockKey, message.getQuantity());
            
            // 3. 取消订单
            orderService.cancelOrder(orderId);
        }
    }
}

✅ 技术亮点:

  • 消息触发式回滚,避免定时任务全表扫描的性能浪费
  • 延迟精度高(秒级),用户体验更好
  • RocketMQ 自带重试机制,保证回滚消息不丢失

4. 热点商品库存分片扣减(✅ 解决单 key 瓶颈)

当单商品 QPS 超过 10 万时,Redis 单 key 会成为性能瓶颈,通过库存分片将并发量提升 N 倍。

lua 复制代码
-- 文件名:stock_shard_deduct.lua
-- KEYS[1..N]: 库存分片key列表  ARGV[1]: 扣减数量  ARGV[2]: 幂等键
local idempotentKey = ARGV[2]
if redis.call('EXISTS', idempotentKey) == 1 then
    return -1
end

-- 随机选择一个分片开始尝试,避免所有请求都打第一个分片
local shardCount = #KEYS
local startIndex = math.random(1, shardCount)

for i = 0, shardCount - 1 do
    local index = (startIndex + i) % shardCount + 1
    local shardKey = KEYS[index]
    local stock = redis.call('GET', shardKey)
    
    if stock and tonumber(stock) >= tonumber(ARGV[1]) then
        redis.call('DECRBY', shardKey, ARGV[1])
        redis.call('SET', idempotentKey, '1', 'EX', 60)
        return 1
    end
end

return 0

✅ 技术亮点:

  • 将 1 个热点 key 拆分为 10-100 个分片 key,并发量线性提升
  • 随机起始位置遍历,避免分片热点倾斜
  • 对业务代码透明,上层无需感知分片逻辑

核心技术难点与生产级解决方案📊

技术难点 问题本质 解决方案 技术亮点
🔥 热点商品单 key 性能瓶颈 Redis 单线程处理单 key,QPS 上限约 10 万 1. 库存分片 + 随机扣减2. 前端限流 + 网关限流3. 本地缓存预热 分片后并发量可提升至百万级,是大厂秒杀标配
⚠️ 订单 - 库存最终一致性 跨 Redis 和数据库的分布式事务,强一致性性能极差 1. Redis 预扣减 + MQ 异步同步 DB2. 订单状态机严格流转3. 每日全量对账 + 实时增量对账 牺牲强一致性换取高性能,多层兜底保证最终一致
🚨 库存回滚可靠性 订单超时 / 取消 / 支付失败时,库存未正确回滚 1. RocketMQ 延迟队列触发回滚2. 回滚操作也用 Lua 脚本保证原子性3. 死信队列处理回滚失败消息 回滚成功率 99.99%,异常消息人工兜底
🔄 数据不一致修复 Redis 与 DB 库存不一致,导致少卖 / 超卖 1. 每日凌晨全量对账:订单支付数 = DB 库存扣减数2. 每小时增量对账:Redis 库存 = DB 可用库存3. 不一致时以 DB 为准修正 Redis 自动发现并修复 99% 的不一致问题,无需人工干预
🛡️ 恶意刷单与重复下单 攻击者通过脚本批量下单,占用库存 1. 入口层幂等性校验2. 用户级限流 + IP 级限流3. 风控系统拦截异常请求 在 Redis 层就拦截 99% 的恶意请求,不打业务层
💥 缓存雪崩与击穿 Redis 宕机或缓存过期,所有请求打穿到 DB 1. Redis 集群部署 + 主从切换2. 库存永不过期3. 降级方案:Redis 不可用时切数据库直接扣减 保证系统在极端情况下仍能提供服务,不出现雪崩

总结与项目经验💡

我在之前参与的 618 大促项目中,就是用这套架构支撑了单商品50 万 QPS 的秒杀流量,最终实现了0 超卖99.99% 的库存一致性

其中最关键的经验是:

  1. 永远不要相信业务代码的正确性,必须在数据库和 Redis 层做最后一道防线
  2. 高并发场景下,最终一致性是唯一可行的选择,不要追求强一致性
  3. 所有的异常情况都要考虑到,并且要有对应的兜底方案

【别走,交个朋友】

我是Rain ,一个喜欢把复杂技术讲透、让代码落地的实践者。

如果你厌倦了四处收集碎片化的八股文,想看看一线项目里真实的架构决策和踩坑记录,欢迎来我的公众号「Rain的Java大神之路 」坐坐。

在这里,我会把每一次技术复盘、每一个项目的设计源码,毫无保留地分享给你。

关注后回复「源码」,我为你准备了几份我亲手注释的高并发项目核心源码,希望能帮你少走一些弯路。

🔥技术这条路很酷,我们结伴同行。

🚀Keep Coding, Keep Loving ------ 与所有 Java 同路人共勉。

相关推荐
云计算磊哥@2 小时前
运维开发宝典051-缓存服务Redis集群2
redis·缓存·运维开发
BomanLj3 小时前
NSK W4013SA重载双螺母精密滚珠丝杠技术详解
经验分享·规格说明书
doiito3 小时前
【Agent Harness】Gliding Horse 整体架构拼图:当 AI Agent 有了自己的操作系统
ai·rust·架构设计·ai agent
SRET13 小时前
Mineradio 沙盒隔离安装完全指南 | 一键安全运行 Electron 应用!!!
javascript·经验分享·安全·electron·aigc·音视频·ai编程
米饭不加菜19 小时前
NE555 定时器应用——声光控制/延时开关电路
经验分享·硬件工程
摇滚侠19 小时前
Redis7 零基础到进阶 Linux 离线安装 Redis 09-10
数据库·redis·缓存
凡科建站21 小时前
2026年AI网页设计方面的平台有哪些,设计建站两用工具
经验分享
SRETalk21 小时前
高并发通知系统设计笔记:如何安全发送百万级/亿级推送?
高并发
techdashen21 小时前
Uber 的全局限流系统:从零散 Redis 限流到服务网格里的自动化流量保护
数据库·redis·自动化