---
title: "库存服务优化思路"
description: "细粒度单位的库存服务设计，你经历过吗？"
date: 2020-02-28T14:12:54Z
canonical: https://xiaobox.github.io/p/2020-02-28-ku-cun-fu-wu-you-hua-si-lu/
author: 小盒子
categories: ["数据库"]
tags: ["Java", "Redis", "缓存", "性能优化", "算法", "架构"]
source: https://mp.weixin.qq.com/s/jvCER8goIDmwLTZX78C18w
license: CC BY-NC-SA 4.0
license_url: https://creativecommons.org/licenses/by-nc-sa/4.0/
---

# 库存服务优化思路

![Image](https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2020-02-28-ku-cun-fu-wu-you-hua-si-lu/001-0505c464.jpg)

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

-   库存可以以数量为单元，假如有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 的。**

代码实现如下：

```cs
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;
}

```

![Image](https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2020-02-28-ku-cun-fu-wu-you-hua-si-lu/002-80380443.gif)

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

-   这里你可能会关心Reids存储的数据量，由于车辆的开放预定周期不会太久，比如半年或一年，那么分到每个门店和车型其实预定的人并不那么多，即使在未来的预定周期内所有车都被预定了数据量也不很大。

-   对于过期的数据，比如一个月以前或一周以前的不再参与库存计算的Redis数据，用定时任务清掉就可以了，或者也可以再行持久化备份一份。

-   这里讲的是单车型的，如果要算多车型的，就起多线程并行计算。

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

需要注意的是，上面的方案我并没有在生产环境实施过，只是一种单纯的设计，如有不严谨和不对的地方，欢迎指正![Image](https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2020-02-28-ku-cun-fu-wu-you-hua-si-lu/003-35076111.png)。如你需要解决相似的问题，请谨慎参考！![Image](https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2020-02-28-ku-cun-fu-wu-you-hua-si-lu/004-80504576.png)

End

![Image](https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2020-02-28-ku-cun-fu-wu-you-hua-si-lu/005-67882503.png)

![Image](https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2020-02-28-ku-cun-fu-wu-you-hua-si-lu/006-57ddc455.jpg)

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

