近来遇到关于库存与并发的问题。由于一直接触的系统都是没有考虑过商品库存的并发,加上解决过的并发问题,也只是简单直接地采用锁表的方式。所以导致踩坑。

1 问题1,商品基础数据与库存数量,设计在同一个表。

商品基础数据,包括库存数量,主要用于查询。但库存数量,还要解决经常变化,且可能出现并发的情况。如果简单使用锁,即使只锁一行数据,也会导致正在进行下单(涉及扣减库存)的商品不能被浏览(因为锁住,不能查询)。

为了减轻这个情况,下单时,检查库存数量是否足够购买时,不锁数据,等到保存订单数据,真正扣减库存时才加锁。本想着通过减少锁数据的时间,减少商品数据不能查询的情况。但是系统采用Java开发,使用了Spring + Hibernate框架。而Hibernate在事务内使用了一级缓存,即事务内未提交时,查询到的业务数据都放到一级缓存。事务内查询时,会先查询一级缓存,若命中,则不再查询数据库。就导致了检查库存时已获取了商品数据,扣减库存时(从一级缓存获取)不能获取到最新库存(特别是两个客户同时下单同一个商品的情况),最后在并发情况下扣减库存,就出现库存扣少1了的问题。

解决方案很简单,把商品基础数据与库存数据分开两个表存放。库存数据在扣减时,不影响商品浏览。

2 问题2,库存数量,需减少锁定时间。

由于客户浏览商品,或者添加商品到购物车,都需要查询库存数据。如果使用悲观锁,即锁表或锁数据后不能查询,会导致客户不能浏览。参考了以下文章,决定使用乐观锁,即不使用数据库锁。

目前系统规模比较小,且没有涉及分布式,于是决定在扣减库存时直接更新数据的方式。即使用update语句扣减库存时,用where条件判断是否足够扣减,并返回是否扣减成功。

由于使用MySQL,update语句不能返回指定数据(但是,sql server可以使用update...output,PostgreSQL可用update...returning)。加上Hibernate不能同时执行update和select两个语句,最后采用存储过程。参考以下网址:

3 解决方案

总的来说,使用乐观锁(即没有使用数据库的锁),并利用MySQL存储过程实现扣减库存后返回结果。

1)库存表

create table `product_stock` (
    `productId` bigint not null comment '商品ID',
    `instock` int not null default '0' comment '库存数量',
    `createTime` datetime(3) default null comment '创建时间',
    `updateTime` datetime(3) default null comment '更新时间',
    primary key ( productId )
) engine=InnoDB default charset=utf8mb4 collate=utf8mb4_0900_ai_ci comment='商品库存';

2)扣减库存的存储过程

利用存储过程的out参数,返回扣减结果。当outUpdateQty返回的值大于零,扣减成功,否则失败。扣减成功,outStockAfter的值才是正确。

delimiter //
create procedure `product_reduce_instock`(
    in inProductId bigint, /*传入参数:商品ID*/
    in inReduceQty int, /*传入参数:扣减数量*/
    out outUpdateQty int, /*传出参数:实际扣减数量*/
    out outStockAfter int /*传出参数:更新后库存数量*/
)
begin
    -- 初始化返回的值
    set @updateQty=0;
    set @stockAfter=0;
    
    -- 执行扣减库存
    update product_stock 
    set instock = (@stockAfter := instock - (@updateQty := inReduceQty)), updateTime = now() 
    where productId = inProductId and instock >= inReduceQty;
    
    -- 传出参数赋值,即返回扣减结果
    set outUpdateStock=@updateQty;
    set outStockAfter=@updateQty;
end //

标签: MySQL, 库存

添加新评论