假设你在开发订单系统时遇到高并发下库存扣减出错,如何解决?由浅入深分析

假设你在开发订单系统时遇到高并发下库存扣减出错,如何解决?由浅入深分析

作者:Java开发者 · 8年经验

标签:并发控制|库存扣减|乐观锁|悲观锁|事务一致性


一、背景介绍

在一次做电商订单系统的开发中,我们遇到了一个典型的问题:高并发下库存扣减不准确,出现超卖现象

场景如下:

  • 每次秒杀或大促活动开始,瞬间并发量暴增;
  • 商品库存是有限的;
  • 订单系统在处理扣减库存时,由于并发竞争,导致库存被减成负数 ,甚至同一商品被超卖上千件

这是一个典型的并发控制问题。本文将结合我在项目中的实际经验,从问题分析、方案演进到最终实现,逐步讲解如何在高并发环境下保证库存扣减的正确性与系统的高性能


二、问题分析

1. 现有逻辑(有问题的代码)

scss 复制代码
public void createOrder(Long productId) {
    Product product = productDao.selectById(productId);
    if (product.getStock() > 0) {
        // 扣减库存
        product.setStock(product.getStock() - 1);
        productDao.update(product);
        // 创建订单
        orderDao.insert(new Order(productId));
    }
}

2. 问题根源

  • 多个线程读取到的库存仍是"可用"的;
  • 并发下多个线程同时进入扣减逻辑;
  • 更新库存时没有加锁,导致并发写覆盖
  • 最终导致库存被多次扣减,超卖。

三、解决思路

为了应对高并发,我们尝试以下几种方案:

✅ 方案一:数据库悲观锁

利用数据库的行级锁机制,通过 SELECT ... FOR UPDATE 锁定库存记录。

实现方式:
java 复制代码
@Transactional
public void createOrderWithPessimisticLock(Long productId) {
    // 查询并锁定库存行(悲观锁)
    Product product = productDao.selectForUpdate(productId);

    if (product.getStock() > 0) {
        product.setStock(product.getStock() - 1);
        productDao.update(product);
        orderDao.insert(new Order(productId));
    } else {
        throw new RuntimeException("库存不足");
    }
}
优点:
  • 简单直接,事务内锁定库存,保证并发安全。
缺点:
  • 并发量大时,锁竞争严重,容易造成数据库性能瓶颈
  • 容易产生死锁或阻塞。

✅ 方案二:乐观锁 + 版本号机制(推荐)

利用乐观锁机制,在更新库存时带上版本号或原库存值,只有版本一致才执行扣减。

数据结构:

数据库表中增加 version 字段:

sql 复制代码
ALTER TABLE product ADD COLUMN version INT DEFAULT 0;
实现方式:
ini 复制代码
@Transactional
public void createOrderWithOptimisticLock(Long productId) {
    boolean success = false;

    for (int i = 0; i < 3 && !success; i++) {
        // 1. 查询商品库存信息(包括版本号)
        Product product = productDao.selectById(productId);

        if (product.getStock() <= 0) {
            throw new RuntimeException("库存不足");
        }

        // 2. 构造更新语句,更新时带上 version 条件
        int updateCount = productDao.updateStockWithVersion(
                productId,
                product.getStock() - 1,
                product.getVersion()
        );

        // 3. 判断是否更新成功
        if (updateCount == 1) {
            orderDao.insert(new Order(productId));
            success = true;
        }
    }

    if (!success) {
        throw new RuntimeException("库存扣减失败,请重试");
    }
}
DAO 层 SQL(MyBatis 示例):
ini 复制代码
<update id="updateStockWithVersion">
    UPDATE product
    SET stock = #{stock},
        version = version + 1
    WHERE id = #{id}
    AND version = #{version}
</update>
优点:
  • 无需显式加锁,性能较好;
  • 在高并发场景下也能保证数据一致性;
  • 可结合 重试机制 提高成功率。
缺点:
  • 编码复杂度略高;
  • 大量失败重试会浪费 CPU 资源。

✅ 方案三:Redis 分布式锁 + 原子扣减(适合高并发秒杀)

适用于库存保存在 Redis 中,利用 Redis 的原子操作进行扣减。

java 复制代码
public void createOrderWithRedis(Long productId) {
    Long stock = redisTemplate.opsForValue().decrement("product_stock:" + productId);

    if (stock != null && stock >= 0) {
        // 创建订单入库
        orderDao.insert(new Order(productId));
    } else {
        throw new RuntimeException("库存不足");
    }
}
优点:
  • 扣减操作是原子的,性能极高;
  • 抗并发能力强,适合秒杀系统。
缺点:
  • 需要保障 Redis 与数据库的一致性;
  • 系统复杂度变高,需引入异步队列(如 Kafka)处理后续订单落库。

四、最终方案选型建议

根据业务场景,我们选择:

  • 普通高并发业务 :使用乐观锁 + 数据库事务
  • 极端高并发 (如秒杀):使用Redis原子扣减 + 异步落库
  • 低并发但要求强一致性 :使用悲观锁

五、总结

库存扣减是订单系统中最关键的一环。正确处理并发问题,避免超卖,是每个后端开发者必须掌握的技能。

核心原则是:

  • 控制并发访问资源;
  • 保证操作原子性;
  • 在性能和一致性之间做权衡。

一句话总结:

乐观锁保证正确性,悲观锁保证唯一性,Redis 保证高性能,最终一致性靠架构设计。


附:项目中常见的事务一致性处理建议

  • 使用 @Transactional 保证本地事务;
  • 使用消息队列保障异步一致性;
  • Redis 缓存更新必须有回写策略;
  • 分布式事务可引入 Seata 或 TCC 模式。
相关推荐
RainbowSea4 分钟前
补充:问题:CORS ,前后端访问跨域问题
java·spring boot·spring
RainbowSea6 分钟前
15. MySQL 多版本并发控制
java·sql·mysql
倔强的石头10614 分钟前
飞算JavaAI:重构软件开发范式的智能引擎
java·数据库·重构
前端小巷子15 分钟前
跨域问题解决方案:JSONP
前端·javascript·面试
Q_9709563930 分钟前
java+vue+SpringBoo足球社区管理系统(程序+数据库+报告+部署教程+答辩指导)
java·开发语言·数据库
要开心吖ZSH35 分钟前
微服务架构的演进:迈向云原生
java·微服务·云原生
程序员爱钓鱼39 分钟前
Go语言中的反射机制 — 元编程技巧与注意事项
前端·后端·go
为了更好的明天而战1 小时前
Java 中的 ArrayList 和 LinkedList 区别详解(源码级理解)
java·开发语言
JosieBook1 小时前
【Java编程动手学】Java中的数组与集合
java·开发语言·python
N_NAN_N2 小时前
类图+案例+代码详解:软件设计模式----单例模式
java·单例模式·设计模式