MySQL乐观锁解决库存超卖问题

本文通过实例展示了如何在高并发场景下,利用MySQL的乐观锁机制实现商品库存的线程安全减少,介绍了Goods和Order类的设计,以及OrderDao和GoodsDao接口的操作。通过单元测试模拟并发情况,确保库存更新的原子性和一致性。

在通过多线程来解决高并发的问题上,线程安全往往是最先需要考虑的问题,其次才是性能。库存超卖问题是有很多种技术解决方案的,比如悲观锁,分布式锁,乐观锁,队列串行化,Redis原子操作等。本篇通过MySQL乐观锁来演示基本实现。

一、Goods和Order

@Data
public class Goods {
    private int id;
    private String name;
    private int stock;
    private int version;
}
@Data
public class Order {
    private int id;
    private int uid;
    private int gid;
}

二、OrderDao和GoodsDao

@Mapper
public interface OrderDao {

    /**
     * 插入订单
     * 注意: 由于order是sql中的关键字,所以表名需要加上反引号
     * @param order
     * @return int
     */
    @Insert("INSERT INTO `order` (uid, gid) VALUES (#{uid}, #{gid})")
    @Options(useGeneratedKeys = true, keyProperty = "id")
    int insertOrder(Order order);
}
@Mapper
public interface GoodsDao {

    /**
     * 查询商品库存
     * @param id 商品id
     * @return
     */
    @Select("SELECT * FROM goods WHERE id = #{id}")
    Goods getStock(@Param("id") int id);

    /**
     * 乐观锁方案扣减库存
     * @param id 商品id
     * @param version 版本号
     * @return
     */
    @Update("UPDATE goods SET stock = stock - 1, version = version + 1 WHERE id = #{id} AND stock > 0 AND version = #{version}")
    int decreaseStockForVersion(@Param("id") int id, @Param("version") int version);
}

三、GoodsService

@Service
@Slf4j
public class GoodsService {

    @Autowired
    private GoodsDao goodsDao;
    @Autowired
    private OrderDao orderDao;

    /**
     * 扣减库存
     * @param gid 商品id
     * @param uid 用户id
     * @return SUCCESS 1 FAILURE 0
     */
    public int sellGoods(int gid, int uid) {
        int retryCount = 0;
        int update = 0;
        // 获取库存
        Goods goods = goodsDao.getStock(gid);
        if (goods.getStock() > 0) {
            // 乐观锁更新库存
            // 更新失败,说明其他线程已经修改过数据,本次扣减库存失败,可以重试一定次数或者返回
            // 最多重试3次
            while(retryCount < 3 && update == 0){
                update = this.reduceStock(gid);
                retryCount++;
            }
            if(update == 0){
                log.error("库存不足");
                return 0;
            }
            // 库存扣减成功,生成订单
            Order order = new Order();
            order.setUid(uid);
            order.setGid(gid);
            int result = orderDao.insertOrder(order);
            return result;
        }
        // 失败返回
        return 0;
    }


    /**
     * 减库存
     *
     * 由于默认的事务隔离级别是可重复读,会导致在同一个事务中查询3次goodsDao.getStock()
     * 得到的数据始终是相同的,所以需要提取reduceStock方法。每次循环都启动新的事务尝试扣减库存操作。
     */
    @Transactional(rollbackFor = Exception.class)
    public  int  reduceStock(int gid){
        int result = 0;
        //1、查询商品库存
        Goods goods = goodsDao.getStock(gid);
        //2、判断库存是否充足
        if(goods.getStock() >= 0){
            //3、减库存
            // 乐观锁更新库存
            result = goodsDao.decreaseStockForVersion(gid, goods.getVersion());
        }
        return result;
    }
}

四、单元测试GoodsServiceTest

@SpringBootTest
class GoodsServiceTest {

    @Autowired
    GoodsService goodsService;

    @Test
    void seckill() throws InterruptedException {

        // 库存初始化为10,这里通过CountDownLatch和线程池模拟100个并发
        int threadTotal = 100;

        ExecutorService executorService = Executors.newCachedThreadPool();

        final CountDownLatch countDownLatch = new CountDownLatch(threadTotal);
        for (int i = 0; i < threadTotal ; i++) {
            int uid = i;
            executorService.execute(() -> {
                try {
                    goodsService.sellGoods(1, uid);
                } catch (Exception e) {
                    e.printStackTrace();
                }
                countDownLatch.countDown();
            });
        }

        countDownLatch.await();
        executorService.shutdown();

    }
}

五、测试结果

库存由10减到了0,并且生产了10条订单记录。
在这里插入图片描述
在这里插入图片描述

参考资料
更新库存数量 - 乐观锁
通过乐观锁解决库存超卖的问题
mysql 乐观锁_使用MySQL乐观锁解决超卖问题
商品超买超卖问题分析及实战

### 使用乐观锁MySQL中实现库存管理 为了有效防止库存并确保库存数量不低于零,可以采用基于版本号的乐观锁机制。这种方式通过引入一个`version`字段来跟踪记录的变化次数,从而避免并发更新带来的问题。 #### 数据库表结构设计 假设有一个名为 `inventory` 的表用于存储商品库存信息: | Column Name | Data Type | Description | |--| | id | INT | 商品ID | | stock | INT | 库存数量 | | version | INT | 版本号 | 其中 `stock` 字段表示当前可用的商品数量,而 `version` 则用来标记每次对该条目所做的更改。 #### 更新逻辑实现 当执行库存扣减操作时,程序会读取最新的库存数据及其对应的版本号,并尝试将其更新为新的值。如果在此期间有其他事务也试图修改同一项,则该次更新将会失败,此时应用层应捕获异常并决定是否重试。 以下是具体SQL语句示例以及相应的Java伪代码片段展示如何安全地处理库存变更: ```sql UPDATE inventory SET stock = stock - ?, version = version + 1 WHERE id = ? AND version = ? ``` 此查询仅会在满足条件的情况下生效——即只有当传入的旧版号与数据库中的实际值相匹配时才会成功更新。否则返回影响行数为0,意味着发生了冲突需重新获取最新状态再做判断。 ```java public boolean decreaseStock(int productId, int quantity) { String sql = "UPDATE inventory SET stock = stock - ?, version = version + 1 WHERE id = ? AND version = ?"; try (Connection conn = dataSource.getConnection(); PreparedStatement pstmt = conn.prepareStatement(sql)) { // 获取现有库存详情 Inventory currentInventory = getCurrentInventory(productId); if(currentInventory.getStock() >= quantity){ pstmt.setInt(1, quantity); // 减少的数量 pstmt.setInt(2, productId); // 商品ID pstmt.setInt(3, currentInventory.getVersion()); // 当前版本 int affectedRows = pstmt.executeUpdate(); return affectedRows == 1; }else{ throw new InsufficientStockException("Insufficient stock available"); } } catch (SQLException e) { logger.error(e.getMessage()); throw new RuntimeException(e); } } ``` 上述方法能够有效地阻止多个客户端同时对相同资源进行非法改动的可能性,同时也保障了最终一致性原则下的业务需求得到满足[^4]。
评论 1
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值