背景
爲了支持供應鏈幾百個倉庫,單倉日均近百次(倉庫大小不同,數據差別也會比較大,這裏取均值用於評估)的出入庫操作,也就是日均會產生幾十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數據讀寫
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的樣子,這麼低的訪問量,允許我們用多種方式來支持,但又不是完全沒有訪問,那麼我們就不能完全不做處理,直接返回空,或者錯誤信息; - 冷熱數據分開,可以單獨配置、擴容
冷熱數據分開,可以單獨配置、擴容,冷數據因爲訪問量不大,相對來說配置要求也要低一些,而它最大的成本優勢在於存儲,我們可以多個業務場景共用一個實例,這樣將成本優勢最大化,像我們除了出入庫有明細數據,交接單,運單等也都有類似的明細數據,大家完全可以共用一個實例。
受篇幅限制,關於冷數據處理的實施方案在另一篇中詳細說明:供應鏈冷數據實施方案
相關連接:
[1]: HBase企業級功能之存儲分層:HBase冷熱分離
[2]: 分佈式數據庫中間件、產品——sharding-jdbc、mycat、drds
[3]: 供應鏈冷數據實施方案