写点什么

【干货】常见库存设计方案 - 各种方案对比总有一个适合你

作者:Java你猿哥
  • 2023-03-17
    湖南
  • 本文字数:3368 字

    阅读完需:约 11 分钟

一:背景

某个票务系统比如 12306 占座,演出等, 流量最高的业务场景是在查询座位图和锁座环节,新的票务系统在优化后用了新的扣位占座系统,同时锁座扣位环节用新库存服务支撑,锁座 &下单环节分别做预占、扣减库存操作,查询座位图由静态座位图加上实时座位图,静态座位图来自基础数据,实时座位图(预占+已占)来自新库存服务,票务库存与电商库存的区别在于电商库存只要控制加减避免超卖,而票务库存需要精确到座位,关注座位不重卖和少卖



二:功能

  • 查询库存:座位图查询库存

  • 预占库存:锁座时预占库存

  • 扣减库存:出票时扣减库存

  • 释放库存:超时未支付释放 &退款时释放库存

  • 预留库存:保留座位,预留,或者后边再卖

三:业务流程


预占库存时机

常规电商都是在下单环节预占库存,支付成功后扣减库存,但票务在线选座有个前置环节是选座,所以预占库存可以前置到选座,而不是在下单环节,在支付成功后进行扣减库存操作。

恶意预占库存 如果有用户在开始恶意预占大量库存但不下单,导致票导致后续有大量票没有卖出?预占并不是实际扣减,后台系统会自动释放预占超过 15 分钟的库存,重新放出来售卖,这种在现有电商系统已经很常见了,但用户还是可以重新恶意预占,这种只能通过风控和反作弊行为来限制,具体方式有很多,用户限制,ip 限制,手机 PIN 限制等。

预占失败导致查询库存量变大

在热门场次会出现用户抢座,而抢座失败的用户会高频刷新座位图信息重新选座,导致查询库存的请求量瞬时增加 3-5 倍。

四: 那么最终系统设计要遵循什么原则???

  • 容忍动态座位图短暂不一致,接受最终一致性,必须保障高可用;

五 :具体设计

  • 请求量最大的是查询库存,避免查库操作

  • 预占库存需要考虑多并发场景,防止重卖,可以用数据库唯一索引防止重卖

  • 定时释放超时的预占库存,防止少卖

5.1 方案一

本方案完全基于同步操作

5.1.1 预占库存


  • 预占库存不能完全靠 mysql 索引来防止重卖,需要用 redis 先做一层防重,这样可以最大保证数据库的请求量等于实际座位数;

  • 在 redis 出问题时导致流量全部击穿到 mysql,此时需要在 mysql 操作上加入流控熔断,宁可部分预占失败,也要保障服务可用;

  • 先插入 mysql 后写入 redis,防止写 redis 成功但插入 mysql 失败导致少卖;

5.1.2 扣减库存


  • 支付成功后扣减库存,修改库存状态为已占用;

5.1.3 查询库存


  • 查询库存时只查询 redis 里面的数据,不允许穿透到数据库出现数据;

5.1.4 超时释放库存


  • 定时任务扫描状态为预占状态且超过 15 分钟的库存记录;

  • reids 操作失败则释放库存失败;

5.1.5 问题

  • redis 和 mysql 出现不可用情况怎么办?

mysql 不可用的情况是不能容忍的,会完全阻塞业务流程,所以数据库的击穿都需要有流控熔断防范措施;

redis 不可用的情况确定可能出现 2 种业务场景,redis 动态座位图数据少了导致部分重卖失败, redis 动态座位图多了导致少卖所以 redis 的数据准确性至关重要。

  • 重卖情况如何保障 redis 数据的最终一致性?

只有 1 和 2 的业务场景下 redis 不可用时才会出现重卖,重卖底层有数据库唯一索引做保障,一旦出现重卖数据库会抛出索引重复异常(DuplicateKeyException),只要捕捉到异常再将库存补到 redis 就可以避免下次重卖,数据出现一次重复后就可达到最终一致性,如果没有异常出现但缓存数据一直不一致,也不影响业务,表示该场次没有用户选此座位。

  • 少卖情况下如何保障 redis 数据的最终一致性

少卖会直接带来损失,如何保障不出现少卖至关重要,只有 4 的业务场景才会出现少卖,只要保障定时 task 能够重试就可以保障少卖的情况。

  • 如何处理场次售罄?

当场次所有座位都预占或扣减,场次状态需要变成售罄,在售罄状态也可以变回售卖状态,此逻辑正常由场次服务负责,但库存可以在库存出现变化时异步周知场次服务。

  • 关于查询库存的性能问题?

当前采用 redis 的 set 结构来做库存结构缓存,set 的的 SMEMBERS 操作是一个 O(N)的操作,在性能上还需要验证,秒杀最大的流量在于查动态座位图,每个场次的座位数在[200,1000]之间,SMEMBERS 的性能问题会带来很大隐患,所以暂时废弃使用 SMEMBERS 查询动态座位图的方案。

5.2 方案二(异步操作)

本方案是基于异步操作设计 基于方案一的 SMEMBERS 操作性能问题,考虑到异步操作缓存库存来优化查询性能。

5.2.1 预占库存


  • 相对于方案一,预占完成后引入 MQ 来异步记录已售座位图,避免多线程下修改缓存的同步问题;

5.2.2 扣减库存


  • 类似方案一

5.2.3 查询库存


  • 直接 redis 缓存查出已售座位图数量;

  • 缓存动态座位图可能出现少卖情况,所以这里需要触发动态座位图定时更新机制,通过发送 MQ 异步更新。

5.2.4 取消库存


5.2.5 超时释放库存


  • 相对于方案一,引入 MQ 来异步释放预占位图;

5.2.6 异步更新动态库存


  • MQ 通过 taskId 来区分不同的 partition(不同任务分组);

5.2.7 异步比对缓存


  • 依赖查询库存触发定时比对动作,从 redis 拿到库存与数据库做比对,如果出现少卖情况发送 mq 删除 redis 中的数据。

5.2.8 问题

  • redis 和 mysql、MQ 出现不可用情况怎么办?



  • 异步更新动态座位图,用户看到的实时座位图会有多久的延迟?

每次异步更新座位图需要做 setNX → get →set 3 次缓存操作,如果每次按照 50ms 来计算,一个普通任务有 200 个座位,在秒杀情况下,最后一个用户看到完整实时座位图的耗时是 200*50ms=1s,单个场次秒杀最后一个用户抢座失败点击刷新座位图只要超过 1 秒就能看到准实时座位图,所以可以通过交互来一些优化,避免用户因为座位图更新不及时多次锁座失败的场景。

  • 上述 4 和 5 的情况下会出现少卖,如何避免?

可以通过在查询座位图的逻辑,每隔 N 分钟去校验缓存数据和数据库数据是否一致,只校验少卖的数据,出现缓存中有而数据库没有的座位(少卖),可以发送到 MQ 移除缓存数据,来释放座位,为什么在查询座位图逻辑重触发?因为没人查询座位图就不会出现少卖的情况。

  • MQ 如何保证 add 和 delete 操作的顺序性?

用户下单预占后,取消订单,预占库存和释放库存间隔较短,add 和 delete 操作通过 mafka 异步同步到动态座位图缓存,无法保证操作顺序性,会有两种情况:1、先 add 再 delete,正确,无影响;2、先 delete 再 add,错误,会导致少卖,因 add 操作后就无法释放。这种情况通过定时更新机制来做。

  • 满座如何做?

在创建对应任务时写入库存总量,每次出票时去修改库存量,当库存为 0 时主动发送 MQ 通知到管理系统,提供查询库存余量的接口。

5.3 方案三

本方案基于 异步+MQ 方案二是通过步骤 7 异步比对来达到缓存和数据库最终一致,从而防止少卖,但整个流程过于复杂,库存的各个步骤之间耦合很严重,不利于系统维护,方案二中会出现少卖情况都是因为预占库存没有释放,而已售库存不会导致少卖,所以是不是可以把缓存分为预占库存和已售库存,缓存的预占库存可以定时失效,从而保证数据定时刷新达到最终一致性。

  • redis 存入 2 个 key,value 分别是已售座位和预占座位信息

  • 缓存预占座位信息设置 N 分钟的过期时间。

5.3.1 预占库存


  • 相对于方案二,插入数据库前先从缓存获取已售座位,判断座位是否已售,减少数据库的压力;

  • 发送 MQ 异步修改缓存中的预占库存;

5.3.2 扣减库存


  • 同步修改缓存的已售库存,修改已售库存失败需要上游做轮训保障;

5.3.3 查询库存


  • 查询缓存中的已售库存+预占库存;

5.3.4 取消库存


  • 发送 MQ 异步更新缓存中的预占库存

5.3.5 超时释放库存


5.3.6 MQ 异步更新预占库存


  • 同方案二,预占库存设置了 N 分钟超时时间,每次更新做一次 N 分钟续签;

5.3.7 问题

  • 预占缓存失效了怎么办?

1、如果是热门任务预占库存的频率会很高,而 MQ 异步更新预占缓存会做续签操作,可以避免预售场次缓存失效导致大量因动态座位图显示不准确锁座失败的情况;

2、在低峰区,如果用户 A 预占了场次,N 分钟没操作,同时 N 分钟内也无其他用户预占库存导致释放了库存,此时 B 预占相同座位会出现预占失败的情况,这种情况刷新座位后该座位就会变成预占状态,所以低峰期会出现小概率的锁座失败。

  • 预占缓存 N 设置几分钟合适?

1、正常预占座位有效时长是 15 分钟,比如 12306 的扣位时效是 15 分钟,如果 N 设置成 15 分钟最合理,但要考虑开始抢票前前 15 分钟会出现少卖 case 或恶意预占的情况导致场次真实少卖,所以建议 N 设置越小越好,但设置太小就会导致虚假重卖(用户锁座失败)的情况,伤害用户体验,需要在 2 者之间权衡,可以根据具体场景摸索设置。

  • 如何保障缓存已售座位和数据库最终一致?

1、在退票和扣减库存操作时,保证 redis 的操作是同步的,操作 redis 失败就返回扣减、退票失败,由上游系统做重试保证数据最终一致性。

  • redis 和 mysql 出现不可用情况怎么办?



用户头像

Java你猿哥

关注

一只在编程路上渐行渐远的程序猿 2023-03-09 加入

关注我,了解更多Java、架构、Spring等知识

评论

发布
暂无评论
【干货】常见库存设计方案-各种方案对比总有一个适合你_Java_Java你猿哥_InfoQ写作社区