Java架构直通车——订单扣库存问题

场景描述

无论是日常工作中,还是面试问题中,并发扣库存都是一个很常见的场景,正好业务里有这样的场景,可以对这类问题做一下总结。

举例说明两个比较典型的扣库存场景:

  1. 产品1:线上招募人员的产品,招募是有人数限制的,每招募成功1人,扣减库存1,直到库存为0,自动停止招募。
  2. 产品2:用户秒杀产品,用户在同一个时间点,同时抢一件有库存的商品。

一般来说,一次扣库存,可以分解为以下3个动作:

  1. 查询最新库存(query)
  2. 检查库存是否足够(内存计算)
  3. 更新库存(update)

由扣库存产生的问题

  1. 单用户重复提交未做幂等
    用户同时提交了2次扣库存操作,因未做幂等,导致一次购买扣减2次库存。
  2. 多用户并发场景下库存超卖
    2个用户抢1个库存,查询到的库存都是1,检查库存足够,然后都去扣减库存,库存变成-1,发生了超卖。

对于问题1:重复提交的幂等性问题,该问题比较容易解决:
1.前端做防重机制防止用户二次提交请求。
2.后端做幂等处理,用户请求带业务token,进行校验,重复token直接返回。

这里主要讨论超卖的问题,我们从各个方面一一探讨解决方案。
假设最原始的,不采用任何解决方案的代码如下:

@Transactional(propagation=Propagation.REQUIRED)
@Override
public void decreaseItemSpecStock(String specId,Integer buysCounts){
	//1.查询库存
	int stock=10;//假设查询数据库后,其值为10.
	//2.判断库存,是否能够扣除
	if(stock<buysCounts){
		//提示用户库存不够
	}
	//3.扣库存
	...
}

扣库存问题不推荐的方案:

  1. 使用synchronized关键字
    虽然synchronized关键字在单体架构上能够很好的工作,但是如果使用集群架构,就不能产生想要的效果了。
    其次,方法上加锁对性能影响较大,导致单体架构性能低下。

  2. 锁数据库或者数据库表(添加全局锁或者行锁)
    这种方法也不推荐,首先性能会降低,其次如果出现事故不好处理。

  3. 悲观锁,将扣库存操作(step1->3)变成只能串行,缺点是同一时间只能有一个用户来操作库存,导致并发量不高(无论是通过synchronized关键字、数据库锁比如select for update、分布式锁等各种方式加锁,本质都是一样的)

扣库存问题解决方案推荐:

  1. 直接扣库存,不预检查库存
    update stock_table set stock = stock - 1 where stock-1 >= 0 and id = xxx,缺点是不通用,比如业务上要求库存除了有reduce还有add操作。
    大部分简单业务场景下,方法1完全够用了,甚至一些对并发并不是特别高、业务容忍少量超卖场景下,直接扣库存,无需检查库存是否stock-1 >= 0。
    我们这里代码使用了@Transactional,所以如果数据库执行失败,可以进行回滚。
@Transactional(propagation=Propagation.REQUIRED)
@Override
public void decreaseItemSpecStock(String specId,Integer buysCounts){
	//直接扣库存
	int result=itemsMapperCustom.decreaseStock(specId,busCounts);
	if(result!=1){
		throw new RuntimeException("订单创建失败,库存不足");
	}
}
  1. 乐观锁CAS方案
    相比较直接扣库存,库存增加版本字段version,在更新库存时比较版本号例如update stock_table set version = old_version + 1,stock = stock - 1 where version = query_version and id = xxx,只有版本号没有变化,才能更新库存成功,如果版本号发生变化,则更新库存失败并进行重试。

  2. 分布式锁:zookeeper redis
    库存放到redis等缓存中,在redis中进行库存的查询、扣减,利用内存数据库的特性提高读QPS。对DB进行水平扩展(分库分表方案)来提升读写QPS等等。
    //todo 其细节以后再添加,这里简单看下代码就能理解:

@Transactional(propagation=Propagation.REQUIRED)
@Override
public void decreaseItemSpecStock(String specId,Integer buysCounts){
	lockUtil().getLock(); //--分布式加锁
	//1.查询库存
	int stock=10;//假设查询数据库后,其值为10.
	//2.判断库存,是否能够扣除
	if(stock<buysCounts){
		//提示用户库存不够
	}
	//3.扣库存
	...
	lockUtil().unLock();//--分布式解锁
}

如何验证

最简单粗暴的方法就是构造大流量压测:

1.第一个幂等问题,对单用户请求大流量压测,基本都能发现问题。
2.第二个多用户并发问题,多个用户的请求大流量压测,也能发现问题。

扩展下:
1个商品有多个库存,怎么处理?

发布了385 篇原创文章 · 获赞 326 · 访问量 16万+

猜你喜欢

转载自blog.csdn.net/No_Game_No_Life_/article/details/104135051