5.3 如何防止超卖和少卖
库存一直以来面临的最大挑战就是怎么防止超卖。当前出现超卖问题,不仅仅是一个技术问题,为什么这么说,因为超卖本质上是下单减库存和付款减库存的一致性导致的。技术上通过分布式事务就能解决问题,但因为性能太差,所以要取得一个平衡,既不能大面积出现超卖,又不能太影响性能。
目前即使是在双11高峰也基本不会再出现超卖问题,那库存经过单元化改造后,会不会新增或者放大超卖和少卖风险呢?我们仔细分析一下:
1)卖家在编辑库存时出现单元库存和实际可售的总库存数不一致
按照当前的库存单元化逻辑,我们有总库存行和单元库存行2个概念,单元库存行是实际的商品的可售库存数量,也是买家实际扣减的库存数,总库存行用来记录当前这个商品还剩多少库存数量,怎么保证这两个数据一致性呢?
必然是只能以一个数据为主数据,另外一个数据依赖这个数据做计算来获得,否则只能用事务来保证他们的准确性,用事务显然是比较重的一个选择。以单元库存行为主数据,这个数据是实际用户下单在扣减的数据,最实时,总库存行就以单元库存行来计算获得。
怎么获得这个数据?有多种方式,一是查询单元库存行,另一个是监听单元库存DB通过消息来获得。这两个方式都可以做优化,可以做适当的合并,并不需要每一次变更都要计算一次。当库存小于一定数量时,例如小于10件时,需要精确知道可售库存,那么就可以转变成只有单元库存行发生变化,总库存行就实时计算,以保证总库存行的正确性。
最后一个问题,当卖家在编辑库存时,应该编辑中心单元库存行,因为总库存行本身就是根据单元库存行计算得来的,他不是源头数据。卖家编辑库存直接编辑中心单元库存行,增加可以直接加库存,然后再分配到各个单元库存,如果是减少库存,则类似调拨的逻辑。
2)库存在单元间调拨时以及库存回收时出现不一致
这是容易想到的一个可能出现不一致的问题,但是按照我们前面设计的逻辑,是需要进行跨单元和跨库进行操作的,所以需要分布式事务进行保证,即一个单元进行出库,一个单元进行入库,要保证这2个动作要么都完成要么都失败。这是一个比较重的操作,但是没办法避免,我们通过消息事务来保障,尽量减少性能影响,好在这个调拨操作并不是常态,只有在非常浅的库存时才会发生,而且是库存在单元分配不均匀时才会频繁发生,根据我们双11测试,这个跨单元调拨的频次和调拨的性能都可以接受,总量可控并没有影响整个库存的性能。
前面讲的是从逻辑上保障数据的一致性,我们还可以通过对账来核对最终数据的一致性,前面也介绍了单元调拨或者库存回收时需要继续入库和出库单据以及实际的扣减单据,这些单据可以用来做时间的库存对账,来保障可售数据与卖家设置的一致。
5.4 单元封闭下怎么保障最终一致性
前面介绍的方案基于一个假设:总的可售库存和每个单元的单元库存总数是一致的,也就是防止出现超卖和少卖的发生。这个假设的前提是每个单元的网络都是通畅的,数据通信都没问题,但是这个场景显然是理想的场景,我们要做多机房容灾,本身就是为了应对这个场景,很显然我们要考虑单元机房之间数据不能同步的问题。
我们要解决2个基本问题:
1)假设单元机房之间的网络断开,或者某个机房损坏,其他好的机房仍然能交易扣减完成。
这个问题就是解决基本的单元库存的封闭问题,也是我们做库存单元化的一个最重要的初衷之一,所以必须要能实现这一点。
按照前面的整体设计思路,总库存数已经拆分到各个单元库存,各个单元扣减理论上扣减本单元已经分配的梳理就可以,最重要的是要看在单元完成扣减过程中,有哪些需要强依赖其他单元的数据?目前看来有2个场景:
一个是用户看到的总库存行数据。由于总行数据是各个单元库存行汇总的数据,如果单元网络不通,这个数据将会不准确,那么用户看到是有库存,但是实际上已经没有了,这种情况下只影响了部分用户体验,但每个单元的库存实际上可以卖完,不会影响商品的实际成交。
另一个是当单元库存不够时需要去调拨。这个情况是出现单元间库存分配不均匀的场景,一个单元卖完了,另外一个单元没卖完,但是却没人买,最终商品的成交会部分受损,对用户体验也会有部分损失,但至少能保证好的机房的库存能够顺利卖完。
这类情况可以看出,总体已经分配到单元的库存能够顺利卖完,用户体验和总销量会有部分受损。
2)当机房之间的网络恢复后,能保证最终的数据一致性,即最终单元的库存数之和等于卖家设置的可售库存总数。
回答这个问题,要看当机房之间的网络隔绝时,发生的交易扣减库存,有影响到哪些数据,这些数据的变更又对后面产生哪些影响。其实实际影响的数据就是总库存行数量,而这个数据不需要记录过程,不管建了多少次,我们只需要知道最新的当前值就可以,所以一旦网络恢复,我们重新计算当前最新的总库存数据就是准确的,所以总库存行数准确性没有受到断网的影响。
另外断网时的调拨影响也是当时的某次用户下单,他也不会对后续的总量产生后续影响。所以单元库存在单元之间单元网络中断时仍然可以进行正常的交易减库存,网络恢复时也能保证最终的数据一致性。
06
效果怎么样?
6.1 降低了交易延时
按照双11的数据表现来看,交易到库存的调用,单元化写和非单元化写RT,客户端耗时下降超过半数,RT提升比较明显。
6.2 单元容灾的案例效果
在某一年的双11,由于对中心机房的DB有些误操作,导致中心单元的库存DB受到影响,导致交易无法下单,出现了一定的下跌,但是减库存并没有跌零,这是由于库存做了单元化,而单元机房的库存DB没有受到影响,所以单元机房交易减库存是正常进行的。如果库存没有进行单元化,那么这次中心单元库存DB有问题,交易肯定会跌零,即使交易已经实现了单元化。这是一个非常体现库存实现单元化后,能做到交易封闭在单元的一个很好的案例。
6.3 热卖品降低单库的瓶颈
像直播或者秒杀时的热卖单元原来都是落在一个库中,很难进行优化,进行库存单元化后,我们把一个商品的库存拆分了多份,这样即使是同一个商品扣库存也可以做到由多个DB来支撑,所以相当于把扣减分散到多个单元了,这样热卖品对DB的挑战就会降低很多。相应的DB的峰值流量也会下降很多,以前的库存单元机房的DB只有读的请求,那么单元化后,单元机房的DB也可以利用起来,所以DB机器的峰值下降,平均利用率会提升,对降低DB的成本也有好处。
07
获得的经验哪些
可以被借鉴和继承
库存单元化这个方案,如果抽象一下其实有一定的通用场景,就是一个共享资源被多个地方分配使用,然后可以再动态调配的解决方案,至少有几个方案是有通用性的。
- 资源的初始预测分配方案。在库存这个场景中,我们利用流量比例来分配库存的初始比例,但是可以通过实际每个机房的实际库存消耗量来做后续库存的预测,当前我们已经做了,单个卖家某些商品的销量预测,提前提醒卖家来补库存,来提升商品的在架率。类似的我们也可以提前预测单元机房的库存量,提前做一些调拨,避免在卖家实际减库存不够时,做实时的调拨。
- 资源的调拨方案。当某个地方的资源不够时,可以触发一个调拨逻辑,实现从其他地方进行资源的代扣,同时还可以把资源进行回收。这个方案如果和前一个方案进行整合,就实现了资源的动态调配,从而保持每个地方的资源保持在合理水位上。
- 一致性数据校验方案。在上面的减库存实现中,其实并不是简单减一下库存那么,还需要生成减库存的记录也就是单据,用这个单据来进行最终的库存对账,否则无法知道库存被谁扣,被谁代扣了。所以基于库存的单据进行对账来保障最终数据的一致性也是非常通用的一个解决方案。
08
总结
库存单元化有很多挑战,但是经过仔细设计、小心求证,整个项目从设计、测试、灰度验证到最终的双11上线整体比较顺利。虽然整体方案比较简单,但实现细节比较复杂,但都是一次性的投入,对系统的长期运维并没有带来复杂度,所以投入性价比也比较高。
库存单元化挑战了业界难度很大、对交易进行了完全封闭的方案,在业界也是具有非常创新性的价值,希望能对大家有一些参考。