天天看點

基于TableStore的海量電商訂單中繼資料管理一、背景二、方案演進三、基于表格存儲實作的訂單場景Demo

一、背景

訂單系統存在于各行各業,如電商訂單、銀行流水、營運商話費賬單等,是一個非常廣泛、通用的系統。對于這類系統,在過去十幾年發展中已經形成了經典的做法。但是随着網際網路的發展,以及各企業對資料的重視,需要存儲和持久化的訂單量越來越大。資料的重視程度與資料規模的膨脹帶來了新的挑戰,原有的系統是否還能繼續滿足需求成了焦點?

需求場景

某電商平台A,需要進行持久化所有平台産生的訂單資料。同時,基于所有的訂單資料,系統又需要向外提供面向多種角色:消費者、店家、平台三類人群的多元化的查詢服務。消費者可以查詢自己的曆史訂單,商家可以統計熱銷産品,平台也可以分析使用者行為、平台交易規模等。主要查詢方式涵蓋訂單的多元度檢索,以及訂單資料的分析、統計等,例如:

面向消費者:【A消費者】*【近1年】*【産品名含'電腦'字段】訂單查詢;

面向店家:【B店家】*【近1個月】*【每個産品】銷售量排名;

......

技術點

在訂單場景中,技術上通常需要考慮的技術點,主要包含如下幾個方面:

  • 查詢能力:需要具備豐富的查詢類型,如多元度、範圍、模糊查詢等,同時具備排序、統計等功能;
  • 資料量:存儲海量資料的同時,滿足強一緻、高可用、低成本等要求;
  • 服務性能:應對高并發請求高并發的同時,保證低延遲;

二、方案演進

應對訂單場景,電商通常會采用MySQL傳統方案。借助關系型資料庫強大的查詢能力,使用者可直接通過SQL語句實作訂單資料的多元度查詢、資料統計等。所謂資料膨脹,分為橫向、縱向兩種,橫向即不斷疊代引入的新字段次元,縱向即總的存儲資料量。在面對這兩種訂單資料膨脹上,單MySql方案逐漸變得吃力。 SQL + NoSQL的組合方案(以下稱:組合方案)便應運而生,借助兩個資料庫各自的優勢分别解決不同場景各自的需求。但組合方案同樣也帶來了新的問題,組合方案犧牲空間成本,同時也增加了開發工作量與運維複雜度。在保證資料一緻性上産生額外開銷。

下面讓我們看一下如下幾個正常方案:

正常方案

1、MySql分庫分表方案

MySql自身擁有強大的資料查詢、分析功能,基于MyQql建立訂單系統,可以應對訂單資料多元查詢、統計場景。伴随着訂單資料量的增加,使用者會采取分庫、分表方案應對,通過這種僞分布式方案,解決資料膨脹帶來的問題。但資料一旦達到瓶頸,便需要重新建立更大規模的分庫+資料的全量遷移,麻煩就會不斷出現。資料疊代、膨脹帶來的困擾,是MySql方案難于逾越的。僅僅依靠MySql的傳統訂單方案短闆凸顯。

1、資料縱向(資料規模)膨脹:采用分庫分表方案,MySql在部署時需要預估分庫規模,資料量一旦達到上限後,重新部署并做資料全量遷移;

2、資料橫向(字段次元)膨脹:schema需預定義,疊代新增新字段變更複雜。而次元到達一定量後影響資料庫性能;

2、MySql+HBase方案

引入雙資料的方案應運而生,通過實時資料、曆史資料分存的方案,可以一定程度解決資料量膨脹問題。該方案将資料歸類成兩部分存儲:實時資料、曆史資料。同時通過資料同步服務,将過期資料同步至曆史資料。

1、實時訂單資料(例如:近3個月的訂單):将實時訂單存入MySql資料庫。實時訂單的總量膨脹的速度得到了限制,同時保證了實時資料的多元查詢、分析能力;

2、曆史訂單資料(例如:3個月以前的訂單):将曆史訂單資料存入HBase,借助于HBase這一分布式NoSql資料庫,有效應對了訂單資料膨脹困擾。也保證了曆史訂單資料的持久化;

但是,該方案犧牲了曆史訂單資料對使用者、商家、平台的使用價值,假設了曆史資料的需求頻率極低。但是一旦有需求,便需要全表掃描,查詢速度慢、IO成本很高。而維護資料同步又帶來了資料一緻性、同步運維成本飙升等難題;

3、MySql+Elasticsearch方案

組合方案還有MySql+Elasticsearch,該方案同樣是将資料分兩部分存儲,可以一定程度解決訂單索引次元增長問題。使用者自己維護資料同步服務,保證兩部分資料的一緻性;

1、全量資料:将全量的訂單資料存入MySql資料庫,訂單ID之外的資料整體存為一個字段。該全量資料作為持久化存儲,也用于非索引字段的反查;

2、查詢資料:僅将需要檢索的字段存入Elasticsearch(基于Lucene分布式索引資料庫),借助于Elasticsearch的索引能力,提供可以應付次元膨脹的訂單資料,然後必要時反查MySql擷取訂單完整資訊;

該方案應付了資料次元膨脹帶來的困擾,但是随着訂單量的不斷膨脹,MySql擴充性差的問題再次暴露出來。同時資料同步至Elasticsearch的方案,開發、運維成本很高,方案選擇也存在弊端。

能力分析 MySql HBase Elasticsearch TableStore
存儲方式 行存儲 列存儲 索引存儲 列存儲+索引存儲
擴充性 單機、擴充性差 水準擴充 (自動)水準擴充
一緻性 強一緻性 強一緻性、時序一緻性
檢索 較弱的支援 不支援 支援
資料量 ~ 1T,~億行 ~10 PB,~萬億行 ~1 PB,~千億行

TableStore方案

如果使用表格存儲(TableStore)研發的多元索引(SearchIndex)方案,則可以完美地解決以上問題。TableStore具有即開即用,按量收費等特點。多元索引随時建立,是海量電商訂單中繼資料管理的優質方案。

TableStore作為阿裡雲提供的一款全托管、分布式NoSql型資料存儲服務,具有【海量資料存儲】、【熱點資料自動分片】、【海量資料多元檢索】等功能,天然地解決了訂單資料大爆炸這一挑戰;

同時,SearchIndex功能在保證使用者資料高可用的基礎上,提供了資料多元度搜尋、統計等能力。針對多種場景建立多種索引,實作多種模式的檢索。使用者可以僅在需要的時候建立、開通索引。由TableStore來保證資料同步的一緻性,這極大的降低了使用者的方案設計、服務運維、代碼開發等工作量。

對表格存儲(TableStore)感興趣的使用者,歡迎加入【表格存儲公開交流群】,群号:11789671。

基于TableStore的海量電商訂單中繼資料管理一、背景二、方案演進三、基于表格存儲實作的訂單場景Demo

附:Demo代碼

三、基于表格存儲實作的訂單場景Demo

業務描述:

每成功完成一筆交易,就會生成一筆交易資料。交易資料包含了交易中的必要元素,如:交易時間、交易的雙方、交易的産品、數量、價格等,這裡選擇最基本元素舉例,僅将必要字段履歷索引,格式如下:

訂單持久化資料

表名:"order_table"

列名 索引類型 類型 索引字段
order_id(主鍵列) KEYWORD String 均勻散列的字元串
time_stamp LONG long 交易時間戳
consumer_id 消費者
seller_id 商家unique編号
product_id 産品unique編号
product_name 産品名
product_type 産品類型
product_price DOUBLE double 産品單價
product_count
total_pay
description

建立訂單表

使用者僅需維護一個資料庫,按如下方式建立:使用者可以通過控制台建立、管理Table,也可通過SDK

List<PrimaryKeySchema> primaryKey = Arrays.asList(
        new PrimaryKeySchema("order_id", PrimaryKeyType.STRING)
);

TableMeta tableMeta = new TableMeta(tableName);
tableMeta.addPrimaryKeyColumns(primaryKey);
CreateTableRequest request = new CreateTableRequest(tableMeta, new TableOptions(-1, 1));
CreateTableResponse createTableResponse = otsClient.createTable(request);           

建立索引

使用者根據自身需求,在需要的時候随時建立索引。TableStore自動做全量、增量的索引資料同步:使用者可以通過控制台建立、管理SearchIndex,也可通過SDK按如下方式建立(索引暫不支援update)

CreateSearchIndexRequest createSearchIndexRequest = new CreateSearchIndexRequest();
createSearchIndexRequest.setTableName("tableName");
createSearchIndexRequest.setIndexName("indexName");

IndexSchema indexSchema = new IndexSchema();
indexSchema.setIndexSetting(new IndexSetting(1));//必寫
indexSchema.setFieldSchemas(Arrays.asList(
        new FieldSchema("product_id", FieldType.KEYWORD).setIndex(true).setEnableSortAndAgg(true).setStore(true),
        new FieldSchema("product_name", FieldType.TEXT).setIndex(true),//TEXT不能設定docValues
        new FieldSchema("product_type", FieldType.KEYWORD).setIndex(true).setEnableSortAndAgg(true).setStore(true),
        new FieldSchema("product_count", FieldType.DOUBLE).setIndex(true).setEnableSortAndAgg(true).setStore(true),
        new FieldSchema("consumer_id", FieldType.KEYWORD).setIndex(true).setEnableSortAndAgg(true).setStore(true),
        new FieldSchema("seller_id", FieldType.KEYWORD).setIndex(true).setEnableSortAndAgg(true).setStore(true),
        new FieldSchema("total_pay", FieldType.DOUBLE).setIndex(true).setEnableSortAndAgg(true).setStore(true),
        new FieldSchema("time_stamp", FieldType.LONG).setIndex(true).setEnableSortAndAgg(true).setStore(true)
));
createSearchIndexRequest.setIndexSchema(indexSchema);

CreateSearchIndexResponse createSearchIndexResponse = otsClient.createSearchIndex(createSearchIndexRequest);           

資料讀取

資料讀取分為兩類:

1、基于原生表格存儲的主鍵列擷取:getRow, getRange, batchGetRow等;

2、基于新SearchIndex功能Query:search;

主鍵讀取

GetRowRequest getRowRequest = new GetRowRequest();

PrimaryKey pk = new PrimaryKey(new PrimaryKeyColumn[]{
        new PrimaryKeyColumn("order_id", PrimaryKeyValue.fromString("fa960b5af"))
});

SingleRowQueryCriteria singleRowQueryCriteria = new SingleRowQueryCriteria("order_table", pk);
singleRowQueryCriteria.setMaxVersions(1);
getRowRequest.setRowQueryCriteria(singleRowQueryCriteria);

GetRowResponse rowResponse = o​tsClient.getRow(getRowRequest);           

Search讀取

新增的search接口,通過設定QueryRequest實作不同query,不同aggregation,不同sort的功能

SearchQuery searchQuery = new SearchQuery();

//設定查詢條件,使用者發揮
searchQuery.setQuery(Query anyQuery);

//做分頁
searchQuery.setLimit(10);
searchQuery.setOffSet(0);

SearchRequest searchRequest = new SearchRequest("tableName", "indexName", searchQuery);

SearchRequest.ColumnsToGet columnsToGet = new SearchRequest.ColumnsToGet();
columnsToGet.setColumns(columnsToShow);//List<String> columnsToShow
searchRequest.setColumnsToGet(columnsToGet);

SearchResponse resp = otsClient.search(searchRequest);           

傳回結構

SearchResponse extends Response {
    private long totalCount;//query比對成功資料總數
    private List<Row> rows;//query比對資料清單(1)
    private boolean isAllSuccess;
}           

場景Demo

search功能主要分為三種:(多元度)查詢,排序,聚合,使用上通過三種功能的組合來實作;

場景1:多元度查詢

【"consumer_001"使用者】【上個月】購買【産品名含某"牙膏"字段】的訂單記錄

使用:BoolQuery, TermQuery, RangeQuery, MatchPhraseQuery

BoolQuery boolQuery = new BoolQuery();

TermQuery termQuery = new TermQuery();
termQuery.setFieldName("consumer_id");
termQuery.setTerm(ColumnValue.fromString("consumer_001"));

RangeQuery rangeQuery = new RangeQuery();
rangeQuery.setFieldName("time_stamp");
rangeQuery.greaterThanOrEqual(ColumnValue.fromLong(fromTime));
rangeQuery.lessThanOrEqual(ColumnValue.fromLong(toTime));

MatchPhraseQuery matchPhraseQuery = new MatchPhraseQuery();
matchPhraseQuery.setFieldName("product_name");
matchPhraseQuery.setText("牙膏");


boolQuery.setMustQueries(Arrays.asList(
        termQuery, rangeQuery, matchPhraseQuery
));

SearchQuery searchQuery = new SearchQuery();
searchQuery.setQuery(boolQuery);
searchQuery.setLimit(10);

//僅建構Query
SearchRequest searchRequest = new SearchRequest("tableName", "indexName", searchQuery);           

場景2:查詢,排序

整個平台【上個月】【單訂單支付金額】排行榜Top10

使用:RangeQuery, FieldSort

RangeQuery rangeQuery = new RangeQuery();
rangeQuery.setFieldName("time_stamp");
rangeQuery.greaterThanOrEqual(ColumnValue.fromLong(fromTime));
rangeQuery.lessThanOrEqual(ColumnValue.fromLong(toTime));

//排序因子
FieldSort fieldSort = new FieldSort("total_pay");
fieldSort.setOrder(SortOrder.DESC);

SearchQuery searchQuery = new SearchQuery();
searchQuery.setQuery(rangeQuery);
searchQuery.setSort(new Sort(Arrays.asList(fieldSort)));
searchQuery.setLimit(10);

//建構Query+Sort
SearchRequest searchRequest = new SearchRequest("tableName", "indexName", searchQuery);