供应链冷热数据处理实践

背景

为了支持供应链几百个仓库,单仓日均近百次(仓库大小不同,数据差别也会比较大,这里取均值用于评估)的出入库操作,也就是日均会产生几十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]: 供应链冷数据实施方案

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