供應鏈冷熱數據處理實踐

背景

爲了支持供應鏈幾百個倉庫,單倉日均近百次(倉庫大小不同,數據差別也會比較大,這裏取均值用於評估)的出入庫操作,也就是日均會產生幾十K的單據數據,所以這一塊的數據量並不大;但爲了更精細化的記錄出入庫數據,出入庫的明細也是必須要記錄的,通過前期對正在運行的業務摸排,按單車日均百萬的零配件(包括整包,裝箱的情況)出入庫量,助力車日均數百萬的換電量(熱點地區的電動車一天需要換兩到三次電池,出入庫都會記錄,所以實際產生的數據量是更換電池次數*2),再加上其他場景的出入庫操作,日均產生的明細數據接近千萬量級,這個數據量是很大的,如何存儲和使用這些數據,便是一開始我們面對的問題。

方案選型

因爲主單數據日均只有幾十K,單表就可以了;而明細會比較多,日均接近千萬,單表肯定是不行的,接下來就是看用哪種拆分方式;

  • 分庫分表:優點是擴展性更好,缺點是需要考慮分佈式事務問題
  • 不分庫只分表:優點是不需要考慮分佈式事務問題,缺點是擴展性不如分庫分表,但後期可以改造成分庫分表
  • 只分庫不分表:不適用,不考慮

日均千萬的數據量,一天一張表完全沒問題,這麼看不做分庫,按天分表是最爲合適的一個方案,每天一張表,一年就是365張,單表2-3G的存儲空間,一年1T左右,如果都放到DB裏面,這個開銷也是不小的,另一方面,單據有很明顯的冷熱屬性,用戶頻繁操作的基本是近一個月內的數據,歷史數據其實是很少訪問的,那麼把歷史數據做冷數據處理就可以了,公司對於冷數據的處理方案有三種;

  • OSS:只做數據備份
  • HIVE:可做數據分析,離線查詢
  • HBASE:大數據,支持實時查詢

毫無疑問,冷數據用HBASE

實施

我們數據庫用的是PG,除了一下細節上和MySQL不同,使用上大部分並沒什麼區別,因爲不需要分庫,只需要申請一個數據庫實例就可以了,明細是按天分表,找DBA要了個function,批量建表,很快就完成了,建好後是這樣的
單據明細分表數據
順便說明下,主單和明細的關係,一個出入庫會有多條明細,也就是1:n的關係,主單數據對外以單號透出,單號生成是有規則的(SN+yyyyMMdd+自定義雪花算法),主單支持列表分頁查詢,明細沒有直接透出,只能在查看單據詳情的時候以單號作爲查詢條件查詢,這一點很重要,因爲這個場景限制,使得我們分表和後續的冷熱數據處理不必面臨什麼挑戰,通過對單號日期數據的截取解析,可以很容易定位到要查詢的數據是在PG還是HBASE,如果在PG,是在哪一張表
數據路由
PG數據的讀寫用的是sharding-jdbc,因爲這方面介紹的比較多,我之前也有過介紹(詳見: https://blog.csdn.net/jornada_/article/details/82947677),這裏就不做贅述,接下來詳細說一下HBASE數據的讀寫

HBASE數據讀寫

hbase數據讀寫方式
IMS是是我們的單據服務,方案主要包含讀寫兩塊,寫方案最初計劃是把90天以上的歷史數據通過JOB洗到HBASE,實際實施過程中,因爲大數據那邊提供了一個基於binlog的準實時同步,我們最終選用了這種方式,
讀的方案是在sharding-jdbc的數據路由之前手動做一個PG OR HBASE的路由,具體代碼如下

public List<OperateOrderInventoryVO> queryInventory(InventoryQueryReq req) {
    String orderNo = req.getOrderNo();
    if (StringUtils.isEmpty(orderNo)) {
        throw new ServiceRuntimeException(Protos.createBadRequest("orderNo"));
    }

    // date check
    Date orderDate = DateUtil.formatDate(orderNo.substring(3, 11), "yyyyMMdd");
    if (DateUtil.calIntervalDay(System.currentTimeMillis(), orderDate.getTime()) > CommonConstant.DEFAULT_DATA_TIME_OPERATE_ORDER_INVENTORY_LIST) {
        // get from HBASE
        Table table = hbaseConnection.getTable(TableName.valueOf(HBASE_TABLE_NAME));
        PrefixFilter filter = new PrefixFilter(req.getOrderNo().getBytes());
        Scan scan = new Scan();
        scan.setFilter(filter);
        ResultScanner scanner = table.getScanner(scan);

        List<OperateOrderInventoryVO> data = new ArrayList<>();
        org.apache.hadoop.hbase.client.Result result = null;
        while ((result = scanner.next()) != null) {
            Cell[] cellList = result.rawCells();
            for (Cell cell : cellList) {
                data.add(JSON.parseObject(new String(CellUtil.cloneValue(cell)), OperateOrderInventoryVO.class));
            }
        }

        // filter
        if (!CollectionUtils.isEmpty(data)) {
            if (!StringUtils.isEmpty(req.getInventorySpu())) {
                data = data.stream().filter(i -> (req.getInventorySpu().equals(i.getInventorySpu())
                        || req.getInventorySpu().equals(i.getInventorySku()))).collect(Collectors.toList());
            }
            if (!StringUtils.isEmpty(req.getInventorySku())) {
                data = data.stream().filter(i -> (req.getInventorySku().equals(i.getInventorySku())
                        || req.getInventorySku().equals(i.getInventorySpu()))).collect(Collectors.toList());
            }
            if (!StringUtils.isEmpty(req.getPackageSn())) {
                data = data.stream().filter(i -> req.getPackageSn().equals(i.getPackageSn())).collect(Collectors.toList());
            }
        }
        return data;
    }

    // get from PG
    OperateOrderInventoryList query = new OperateOrderInventoryList();
    query.setRefSn(orderNo);
    query.setIsDeleted(DeleteEnum.EXISTED.value());
    query.setInventorySpu(req.getInventorySpu());
    query.setInventorySku(req.getInventorySku());
    query.setPackageSn(req.getPackageSn());
    List<OperateOrderInventoryList> inventoryList = inventoryListMapper.query(query);
    List<OperateOrderInventoryVO> inventoryVOList = new ArrayList<>();
    if (!CollectionUtils.isEmpty(inventoryList)) {
        inventoryList.forEach(i -> {
            OperateOrderInventoryVO v = new OperateOrderInventoryVO();
            BeanUtils.copyProperties(i, v);
            v.setOrderNo(i.getRefSn());
            inventoryVOList.add(v);
        });
    }
    return inventoryVOList;
}

生產驗證

項目已經生產運行一段時間了,基本達到了預期

總結

上面說了很多如何去實施,那麼最後聊一下爲什麼要這麼做,我們這麼做的原因主要是三點

  1. 數據量大了之後,存儲的成本是一個不得不考慮的問題
    通過對比阿里雲PG和HBSE(冷數據)的單位存儲成本,前者大概是後者的10倍,數據到達一定量的時候,節省的成本還是非常可觀的;
  2. 歷史數據量巨大,但訪問量很小,可又不是完全沒有訪問
    歷史數據的訪問量很小,每天也只有幾K的樣子,這麼低的訪問量,允許我們用多種方式來支持,但又不是完全沒有訪問,那麼我們就不能完全不做處理,直接返回空,或者錯誤信息;
  3. 冷熱數據分開,可以單獨配置、擴容
    冷熱數據分開,可以單獨配置、擴容,冷數據因爲訪問量不大,相對來說配置要求也要低一些,而它最大的成本優勢在於存儲,我們可以多個業務場景共用一個實例,這樣將成本優勢最大化,像我們除了出入庫有明細數據,交接單,運單等也都有類似的明細數據,大家完全可以共用一個實例。

受篇幅限制,關於冷數據處理的實施方案在另一篇中詳細說明:供應鏈冷數據實施方案

相關連接:
[1]: HBase企業級功能之存儲分層:HBase冷熱分離
[2]: 分佈式數據庫中間件、產品——sharding-jdbc、mycat、drds
[3]: 供應鏈冷數據實施方案

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章