哈啰供应链冷热数据处理实践
- 背景
- 方案选型
- 实施
- HBASE数据读写
- 生产验证
- 总结
背景
为了支持供应链几百个仓库,单仓日均近百次(仓库大小不同,数据差别也会比较大,这里取均值用于评估)的出入库操作,也就是日均会产生几十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,因为这方面介绍的比较多,我之前也有过介绍(详见: ),这里就不做赘述,接下来详细说一下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;
}
生产验证
项目已经生产运行一段时间了,基本达到了预期
总结
上面说了很多如何去实施,那么最后聊一下为什么要这么做,我们这么做的原因主要是三点
- 数据量大了之后,存储的成本是一个不得不考虑的问题
通过对比阿里云PG和HBSE(冷数据)的单位存储成本,前者大概是后者的10倍,数据到达一定量的时候,节省的成本还是非常可观的; - 历史数据量巨大,但访问量很小,可又不是完全没有访问
历史数据的访问量很小,每天也只有几K的样子,这么低的访问量,允许我们用多种方式来支持,但又不是完全没有访问,那么我们就不能完全不做处理,直接返回空,或者错误信息; - 冷热数据分开,可以单独配置、扩容
冷热数据分开,可以单独配置、扩容,冷数据因为访问量不大,相对来说配置要求也要低一些,而它最大的成本优势在于存储,我们可以多个业务场景共用一个实例,这样将成本优势最大化,像我们除了出入库有明细数据,交接单,运单等也都有类似的明细数据,大家完全可以共用一个实例。