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

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

作者: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 模式。
相关推荐
拧之4 小时前
✅XXL-JOB的基本使用
java
老朋友此林5 小时前
MongoDB GEO 项目场景 ms-scope 实战
java·数据库·spring boot·mongodb
追逐时光者6 小时前
一个基于 ASP.NET Core 的开源、模块化、多租户应用框架和内容管理系统
后端·.net
豆沙沙包?6 小时前
2025年--Lc165--H637.二叉树的层平均值(二叉树的层序遍历)--Java版
java·开发语言
迎風吹頭髮7 小时前
UNIX下C语言编程与实践40-UNIX 全局跳转:setjmp 与 longjmp 函数的使用与注意事项
java·c语言·unix
春风霓裳7 小时前
maven-setting配置
java·maven
小蒜学长7 小时前
springboot二手儿童绘本交易系统设计与实现(代码+数据库+LW)
java·开发语言·spring boot·后端
xqlily7 小时前
Go语言:高效简洁的现代编程语言
开发语言·后端·golang
数据知道7 小时前
Go语言:数据压缩与解压详解
服务器·开发语言·网络·后端·golang·go语言
席万里7 小时前
什么是GO语言里面的GMP调度模型?
开发语言·后端·golang