Product Inventory and Concurrency
近来遇到关于库存与并发的问题。由于一直接触的系统都是没有考虑过商品库存的并发,加上解决过的并发问题,也只是简单直接地采用锁表的方式。所以导致踩坑。
1 问题1,商品基础数据与库存数量,设计在同一个表。
商品基础数据,包括库存数量,主要用于查询。但库存数量,还要解决经常变化,且可能出现并发的情况。如果简单使用锁,即使只锁一行数据,也会导致正在进行下单(涉及扣减库存)的商品不能被浏览(因为锁住,不能查询)。
为了减轻这个情况,下单时,检查库存数量是否足够购买时,不锁数据,等到保存订单数据,真正扣减库存时才加锁。本想着通过减少锁数据的时间,减少商品数据不能查询的情况。但是系统采用Java开发,使用了Spring + Hibernate框架。而Hibernate在事务内使用了一级缓存,即事务内未提交时,查询到的业务数据都放到一级缓存。事务内查询时,会先查询一级缓存,若命中,则不再查询数据库。就导致了检查库存时已获取了商品数据,扣减库存时(从一级缓存获取)不能获取到最新库存(特别是两个客户同时下单同一个商品的情况),最后在并发情况下扣减库存,就出现库存扣少1了的问题。
解决方案很简单,把商品基础数据与库存数据分开两个表存放。库存数据在扣减时,不影响商品浏览。
2 问题2,库存数量,需减少锁定时间。
由于客户浏览商品,或者添加商品到购物车,都需要查询库存数据。如果使用悲观锁,即锁表或锁数据后不能查询,会导致客户不能浏览。参考了以下文章,决定使用乐观锁,即不使用数据库锁。
- 超卖问题,高并发情况下,如何扣减库存
https://blog.csdn.net/u010347967/article/details/123788355
目前系统规模比较小,且没有涉及分布式,于是决定在扣减库存时直接更新数据的方式。即使用update语句扣减库存时,用where条件判断是否足够扣减,并返回是否扣减成功。
由于使用MySQL,update语句不能返回指定数据(但是,sql server可以使用update...output,PostgreSQL可用update...returning)。加上Hibernate不能同时执行update和select两个语句,最后采用存储过程。参考以下网址:
- mysql update column then select updated value
https://stackoverflow.com/questions/24691576/mysql-update-column-then-select-updated-value
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 //