无论是日常工作中,还是面试问题中,并发扣库存都是一个很常见的场景,正好业务里有这样的场景,可以对这类问题做一下总结。
一、场景描述
负责的项目里面有2个扣库存的场景:
1.产品1:线上招募人员的产品,招募是有人数限制的,每招募成功1人,扣减库存1,直到库存为0,自动停止招募。
2.产品2:用户秒杀产品,用户在同一个时间点,同时抢一件有库存的商品。
一次扣库存,可以分解为以下3个动作:
step1:查询最新库存(query
)
step2:检查库存是否足够(内存计算)
......
step3:更新库存(update
)
二、问题
1.单用户重复提交未做幂等
用户同时提交了2次扣库存操作,因未做幂等,导致一次购买扣减2次库存。
2.多用户并发场景下库存超卖
2个用户抢1个库存,查询到的库存都是1,检查库存足够,然后都去扣减库存,库存变成-1,发生了超卖。
三、常见解法
对于第一个问题:
1.前端做防重机制防止用户二次提交请求。
2.后端做幂等处理,用户请求带业务token,进行校验,重复token直接返回。
对于第二个问题:
1.直接扣库存,不预检查库存update stock_table set stock = stock - 1 where stock-1 >= 0 and id = xxx
,缺点是不通用,比如业务上要求库存除了有reduce
还有add
操作。
2.悲观锁,将扣库存操作(step1->3
)变成只能串行,缺点是同一时间只能有一个用户来操作库存,导致并发量不高(无论是通过synchronized
关键字、数据库锁比如select for update
、分布式锁等各种方式加锁,本质都是一样的)
3.乐观锁CAS方案:相比较方法1,库存增加版本字段version
,在更新库存时比较版本号例如update stock_table set version = old_version + 1,stock = stock - 1 where version = query_version and id = xxx
,只有版本号没有变化,才能更新库存成功,如果版本号发生变化,则更新库存失败并进行重试。
还有一些优化比如
1.库存放到redis
等缓存中,在redis
中进行库存的查询、扣减,利用内存数据库的特性提高读QPS。
我当前负责的一个应用正是把库存放在了内存数据库中来提高读QPS,效果还是很显著的
2.对DB进行水平扩展(分库分表方案)来提升读写QPS等等
根据经验:
大部分简单业务场景下,方法1完全够用了,甚至一些对并发并不是特别高、业务容忍少量超卖场景下,直接扣库存,无需检查库存是否stock-1 >= 0
,但是要注意如果扣减库存后,发现业务失败,可能需要做恢复库存操作。
四、如何验证
最简单粗暴的方法就是构造大流量压测:
1.第一个幂等问题,对单用户请求大流量压测,基本都能发现问题。
2.第二个多用户并发问题,多个用户的请求大流量压测,也能发现问题。
扩展下:
1个商品有多个库存,怎么处理?