【超卖问题,高并发情况下,如何扣减库存】

目录

  • 扣减库存需要注意的点
  • 方案一: 纯mysql扣减实现
    • 原理
    • 实现
    • 优点
    • 缺点
    • MYSQL架构升级
    • 读写分离
    • 再次升级
    • 代码实现:
  • 方案二:缓存实现扣减
  • 方案三:数据库+缓存
    • 顺序写的架构
    • 扣减流程
  • 总结
    • 扣减库存的操作节点
      • 下单减库存
      • 付款减库存
      • 预扣减库存
      • 防范恶意用户
      • 小结

高并发场景下,商品展示页上面的信息,除了库存的其他信息属于静态数据,静态数据是可以缓存的。动态数据只有库存。
电商项目对并发数据处理要求较高。

扣减库存需要注意的点

  • 剩余库存要大于等于当前需要扣减的库存,不允许超卖
  • 对于同一个商品,用户并发扣减时,要保证并发的一致性
  • 保证可用性和性能,性能至少是秒级
  • 一次扣减包含多个商品
  • 扣减多个商品时,一个不成功则全部不成功,需要回滚
  • 下单时必须产生了扣减,退款时才能归还,归还的数量必须加回去,不能丢失
  • 下单时的一次扣减,可以多次归还
  • 归还时需要保证幂等

方案一: 纯mysql扣减实现

扣减业务完全依赖MYSQL数据库来实现,不依赖中间件或缓存。

原理

  • 基于数据库乐观锁方式保证并发扣减的强一致性
  • 基于数据库的事务实现批量扣减失败进行回滚

实现

【超卖问题,高并发情况下,如何扣减库存】_第1张图片

  • 流程图
    【超卖问题,高并发情况下,如何扣减库存】_第2张图片
    一次完整的流程就是先进行数据校验,做接口开发的时候,要保持一个不信任原则,一切数据都不要相信,都要做校验判断,其次还可以进行库存扣减的前置校验,如果库存只有8个,用户要买10个,此时的数据校验中,可以拦截,减少对数据库的写操作。纯读不会加锁,性能较高。
  • 关键sql
update xxx set 库存 = 库存-10 where skuid = 'xxx' and 库存>= 10 

用户每次扣减的时候,需要传入一个uuid作为流水号,全局唯一:

  • 当用户退单时,传回此编号,用来标识属于历史上的哪次扣减
  • 进行幂等控制,用户调用扣款接口时,出现超时,不知道成功了没,可以通过此编号进行重试或反查,重试时可通过此标识防重

优点

逻辑简单,开发部署成本低。

缺点

无法支持高并发,单机数据库并发1000,2000压力就非常大了,如果AB两个用户同时购买同一个商品,校验通过,后续购买时,只会有一个人成功,导致另外一个人失败,数据库也就多了一次查询,降低性能

MYSQL架构升级

根据场景分析,读库操作一般是浏览商品时产生,扣减库存是在购买时产生,用户购买请求的业务价值大,要保障写操作。

读写分离

【超卖问题,高并发情况下,如何扣减库存】_第3张图片
根据二八原则,80%为读流量,主库压力降低了80%,但采用读写分离会导致读取的数据不准确,不过库存本身就在变,短暂差异,业务上可以允许,最终的扣减会保证数据的准确性。

再次升级

初次升级支持并发并不太高,我们可以引入缓存

【超卖问题,高并发情况下,如何扣减库存】_第4张图片加缓存reids,高并发,单机redis每秒支持并发可在3,4W

代码实现:

version做控制之类的,其实用不上,我们只需要
update where id and 库存>0.
下单失败了,给你返回执行的行数就是0。
if==0
return 下单失败
else
下单成功

方案二:缓存实现扣减

【超卖问题,高并发情况下,如何扣减库存】_第5张图片

  • 和前面的扣减库存其实一样,这里依赖redis,不依赖数据库。
  • redis的hash结构不支持多个key批量操作,我们可采用redis+lua脚本来实现批量扣减单线程。

升级成纯redis实现扣减也会有问题

  • redis挂了,如果还没执行到redis扣减挂了,直接返回前端失败; 如果执行到redis扣减后,挂了,接口返回的失败,redis扣减成功了,但是没有触发异步更新逻辑,数据库不会扣减,数据库是准确的,这个时候需要一个对账程序,通过对比redis和数据库库存是否一致,并结合扣减日志表,发现扣减失败了,将数据库库存比redis多的库存加回到redis中。
  • redis扣减完成,异步刷新数据库失败了,redis此时是准的,数据库库存是多的,结合扣减日志,将数据库比redis多的库存数据在数据库中进行扣减。

方案三:数据库+缓存

在磁盘写数据时,向文件末尾不断追加写入的性能远大于随机修改。对于传统机械硬盘来说,每次随机更新都需要磁头寻址,向文件末尾追加数据,只需要寻址一次。
对固态硬盘来说,虽然避免了磁头移动,但依然存在寻址过程。对文件的随机更新和数据库表更新比较类似,都存在加锁带来的性能消耗。

顺序写的架构

【超卖问题,高并发情况下,如何扣减库存】_第6张图片

与纯缓存架构的区别是,写入任务数据库不是异步,而是在扣减的时候同步写入,用的是顺序写,不是update做数据库数量的更改,所以性能更好。
insert任务数据库,只记录操作,不进行真实扣减。

扣减流程

【超卖问题,高并发情况下,如何扣减库存】_第7张图片

insert是顺序写,将update异步化,所以可以很大提高并发,这样会用到数据库事务来进行redis中的数据修改,所以不会出问题,不会出现少卖的问题。

总结

可以用方案1,但是后期业务量上来了,可以考虑后面用方案2,方案3。
大部分电商目前是基于方案2。

扣减库存的操作节点

扣减库存的节点分为

  • 下单减库存
  • 付款减库存
  • 预扣库存

下单减库存

用户下单了,未必会付款

付款减库存

用户明明购买成功了,却不能付款。

预扣减库存

用户下单后,为用户预留库存,占用数量就是购买的数量,例如预留10分钟,超过10分钟释放用户库存,其他用户继续下单。
用户下单预扣减库存后,在付款时检查是否存在有效的预留库存,如果存在则扣减库存并付款。如果不存在则再次尝试预扣减库存,如果库存不足,则不付款。如果预扣减成功则真正扣减库存并付款。

防范恶意用户

  • 经常下单不付款的用户打标签,这些用户特殊处理,不扣减库存等
  • 秒杀期间,设置同一个人的最大购买数
  • 不付款重复下单的用户进行限制,如果存在未付款的订单,并且是同一商品,提示用户先付款再提交订单

小结

大部分秒杀系统会采用下单减库存的方式。

  • 扣减库存时在程序中判断库存是否为负,如果变为负数,回滚事务不再扣减库存
  • 数据库设置库存字段为无符号整数,从数据库层面保证无法出现负数。

你可能感兴趣的:(面试,intellij-idea,mysql,数据库)