库存服务优化思路

数据库NoSQL数据库关系型数据库

picture.image

很多业务系统构架中都有库存服务

  • 库存可以以数量为单元,假如有5台手机,那么库存就是 5 ,卖掉一台,库存就减 1 变成 4 。这种以数量为单位进行的库存计算相对比较简单,在架构设计中无论你优化DB中的SQL,还是存储结构,或者引入Redis做缓存都比较好设计和实现。

  • 有些库存不是以数量为单位的,比如酒店房间,这是可以重复利用的,是以天为单位的,行话叫“间夜”,理论上如果某间房间一年365天都可用,那么一年就有365个库存,一天就一个。A客人1月1号占了,B客人1月1号就不能占。当然,还有钟点房,时间粒度更细,这个先不讨论,后面会涉及。

  • 还有些业务的库存是以更细粒度为单位的,比如小时。租车业务就是以小时为单位进行库存占用的,假设我们只有一辆车,A客户8:00-10:00租用,B客户可以 11:00-15:00租用。

以下的库存优化思路是针对租车这种细粒度的占用单位的

笔者几年前参与过租车业务中库存系统的改造升级,当时的库存服务主要的问题有两个

  • 没有形成独立的服务,与其他服务耦合在一个“全家桶”式的系统中

  • 与库存相关的业务提供的RPC服务响应较慢,而又由于库存是核心服务,所以库存一慢,各相关子系统都会拖慢

对于第一个问题,相对比较好解决,我们将库存服务独立出来,包括数据库。 问题在于第二个 ,其实慢的原因也很明显,就是对于库存的计算完全依赖数据库,利用SQL与java代码进行计算,当然重头戏在DB,当时采用的数据库是SQL Server,幸亏是SQL Server,如果是My SQL的话可能性能撑不到我们对它进行改造的时候。随着数据量的增加,整个库存服务在完全依赖DB的情况下,服务的查询速度较慢,是秒的量级。

当时的改造先进行了一轮谨慎的尝试,即对业务进行整体重新梳理、对现存SQL,java代码进行全面优化。优化的结果并不理想,虽然这个过程让业务更清晰,也发现了些遗留问题和BUG,但是并没有解决慢的根本问题。

在第一轮的基础上想过用缓存进行处理的方案,但由于单位粒度太细,缓存的设计方案复杂又不好落地,并没有采用。在QPS和TPS趋于平稳的情况下,库存系统的问题并没有被逐渐放大,反而似乎没有到大家忍受不了的地步。于是就不了了之了。

而实际上,这件事真的就结束了吗?我想不一定

由于当时公司的系统越来越多,并且旧系统也会升级改造,随着一些业务的新增或升级,如果有突发的流量,比如做个营销活动,系统能不能撑得住?即使撑得住,用户体验会不会好?我不知道,但我知道如果一个核心服务慢了,整体的体验都不会好。

其实对于我个人来讲,那次的改造不彻底也是我的一个心结,首先当时没有明确的标准和目标,没有特别清晰的量化下来,比如库存服务优化到300-500ms。其实当时我很清楚它慢,就是没有很好的解决了那个问题,心有不甘。

于是今天想起来,提出一个方案,看能不能解决它!

整个库存服务的重点就是如何准确的计算出来它的值,如果算的不对,可能导致有车租不出去(算少了),或没车租出去了(算多了)。而计算这个值的公式特别简单:

    **可预定车辆数 = 现有运营车辆数 - 被占用车辆数** 

1 我们将现有运营车辆数以 门店_车型_amount 为 key ,车辆数为 value 存在Redis中。 当现有运营车辆数变化时,在修改DB的同时同步Redis,当然无论是DB还是Redis都是要做高可用架构设计的,以防止单点故障的发生。

2 被占用车辆原先是以数据库表的方式存储的记录,之前由于时间粒度太细,所以用SQL计算,所以慢,这里我们 采用贪心算法的思路 ,具体是这样的:

首先按门店+车型 一对一的将占车记录取出来。如A门店C1车型,A门店C2车型。将每组每条占车记录的开始和结束时间转为Unix时间戳,最小单位是秒,如:1546315932。


再将开始和结束时间组成一个闭区间如[1546315932,1546316000],再把每组的这些闭区间组成一个二维数组。int[][]intervals。

例如:

[[1546315932,1546316000],[1546315942,1546317000],[1546315932,1546315943],[1546315901,1546315907]]

最后每一个门店+车型都对应一个占车二维数组。  



将这些数组以 门店\_车型\_occupy 为 key ,二维数组为 value 序列化到redis中

当要计算某门店某车型的库存时,先将上面的以门店_车型_occupy为key的值取出,然后利用贪心算法计算出时间段重合的占用数量,再用门店_车型_amount为key的总量减去占用数量就得到了可用库存数。如果时间段不重合就更新Redis二维数组添加新的元素。Redis在操作期间是需要LOCK的。

贪心算法

贪心算法,特别适合解决 Interval Scheduling(区间调度问题), 比如算出多个区间中最多有几个互不相交的区间;或给定一个区间的集合,找到需要移除区间的最小数量,使剩余区间互不重叠。 如果我们请求库存的参数中的时间与现有占车记录的有重叠,那么返回的 需要移除区间的最小数量就是 >=1 的。

代码实现如下:

  
public int eraseOverlapIntervals(int[][] intervals) {  
 if (intervals.length == 0) {  
 return 0;  
 }  
 Arrays.sort(intervals,new Comparator<int[]>(){  
 @Override  
 public int compare(int[] o1,int[] o2){  
 return o1[1]-o2[1];  
 }  
     });  
 int cnt = 1;  
 int end = intervals[0][1];  
 for (int i = 1; i < intervals.length; i++) {  
 if (intervals[i][0] < end) {  
 continue;  
 }  
 end = intervals[i][1];  
 cnt++;  
 }  
 return intervals.length - cnt;  
}

picture.image

由于贪心算法是线性时间复杂度,又是在内存中计算,所以效率会比数据库高很多。

  • 这里你可能会关心Reids存储的数据量,由于车辆的开放预定周期不会太久,比如半年或一年,那么分到每个门店和车型其实预定的人并不那么多,即使在未来的预定周期内所有车都被预定了数据量也不很大。
  • 对于过期的数据,比如一个月以前或一周以前的不再参与库存计算的Redis数据,用定时任务清掉就可以了,或者也可以再行持久化备份一份。
  • 这里讲的是单车型的,如果要算多车型的,就起多线程并行计算。

以上就是利用了缓存和贪心算法进行的缓存优化,在此基础上还应该注意:不要对原服务(依赖数据库版本)进行删除,可做为系统服务降级时使用。Redis也要做好高可用和数据库降级处理,如果Redis挂了,还可以用DB顶一下,如果数据没有了,就将整个服务降级到老版本。

需要注意的是,上面的方案我并没有在生产环境实施过,只是一种单纯的设计,如有不严谨和不对的地方,欢迎指正picture.image

End

picture.image

picture.image

关注公众号 获取更多精彩内容

0
0
0
0
关于作者

文章

0

获赞

0

收藏

0

相关资源
字节跳动 NoSQL 的实践与探索
随着 NoSQL 的蓬勃发展越来越多的数据存储在了 NoSQL 系统中,并且 NoSQL 和 RDBMS 的界限越来越模糊,各种不同的专用 NoSQL 系统不停涌现,各具特色,形态不一。本次主要分享字节跳动内部和火山引擎 NoSQL 的实践,希望能够给大家一定的启发。
相关产品
评论
未登录
看完啦,登录分享一下感受吧~
暂无评论