天天看點

基于 MySQL + Tablestore 分層存儲架構的大規模訂單系統實踐-訂單搜尋篇

背景

在大規模訂單系統中,存在以下常見需求:

  • 查詢某店鋪過去一段時間成交額
  • 查詢某品牌商品在過去一周内的成交額
  • 查詢在某店鋪購物的客戶清單
  • ……

是以,開發者對于資料庫在非主鍵查詢、多列的自由組合查詢等複雜查詢需求上會有比較高的要求。傳統的訂單系統會使用 Elasticsearch 或者 Solr 來實作這一需求,但伴随而來的是更高的系統複雜度和更加昂貴的系統維護成本。

Tablestore 的多元索引,能夠支援此類資料檢索工作,且具有操作簡單、維護成本低等特點,可以将開發者從索引建立、資料同步、叢集維護等工作中解放出來。本文将簡要介紹多元索引,展示如何在 Tablestore 執行個體上建立多元索引,并通過JAVA代碼展示利用多元索引實作搜尋需求。

多元索引簡介

Tablestore 的多元索引,底層使用自研索引引擎,基于反向索引和列式存儲,可以支援非主鍵列查詢、全文檢索、字首查詢、模糊查詢、多字段自由組合查詢、嵌套查詢、地理位置查詢和統計聚合(max、min、count、sum、avg、distinct_count、group_by)等複雜查詢功能。不同于 MySQL 等傳統資料庫的索引使用方式,多元索引無最左比對原則限制,使用時非常靈活。一般情況下一張表隻需要建立一個多元索引即可。

其架構如圖。資料在 Tablestore 的基礎表中寫入,基礎表中的增量資料會通過異步的方式被拉入多元索引。由于這個異步操作,多元索引中的資料相比于基礎表資料存在一定延遲,這個延遲在幾秒到十幾秒的量級。由圖可以看出,基于主鍵列的讀取會由基礎表進行支援;而多元索引會承擔相對更加複雜的非主鍵列查詢、全文檢索、組合查詢、聚合查詢等查詢功能。架構實作了不同流量的分離,部分實作了讀寫分離。

基于 MySQL + Tablestore 分層存儲架構的大規模訂單系統實踐-訂單搜尋篇

更詳細的多元索引介紹可以參考:

多元索引建立

索引建立

進入

Tablestore控制台首頁

。點選建立的 Tablestore 執行個體。

基于 MySQL + Tablestore 分層存儲架構的大規模訂單系統實踐-訂單搜尋篇

點選訂單表 order_contract 對應的索引管理,進入索引管理界面。

基于 MySQL + Tablestore 分層存儲架構的大規模訂單系統實踐-訂單搜尋篇

點選建立多元索引。

基于 MySQL + Tablestore 分層存儲架構的大規模訂單系統實踐-訂單搜尋篇

輸入索引名稱。選擇手動錄入索引字段。這裡,選擇訂單 id(oId)、商品品牌(p_brand)、商品名稱(p_name)、客戶名稱(c_name)、賣家名稱(s_name)、商品單價(p_price)、支付時間(pay_time)、客戶 id(c_id)、賣家 id(s_id)、交易金額(total_price)作為索引字段。點選确定完成索引建立。

基于 MySQL + Tablestore 分層存儲架構的大規模訂單系統實踐-訂單搜尋篇

可以在索引管理頁看到索引相關記錄。

基于 MySQL + Tablestore 分層存儲架構的大規模訂單系統實踐-訂單搜尋篇

索引同步

多元索引建立後,需要同步存量資料,同步過程中,同步狀态顯示為存量;資料同步結束後,同步狀态顯示為增量。此時可以在行數統計處看到記錄總數。

索引查詢

點選搜尋。

基于 MySQL + Tablestore 分層存儲架構的大規模訂單系統實踐-訂單搜尋篇

添加查詢字段,選擇精确查詢,輸入需要查詢的值。

基于 MySQL + Tablestore 分層存儲架構的大規模訂單系統實踐-訂單搜尋篇

搜尋結果如下。

基于 MySQL + Tablestore 分層存儲架構的大規模訂單系統實踐-訂單搜尋篇

JAVA 查詢

訂單表 order_contract 中記錄數約為一億二百萬條。

多元索引建立後,可以直接通過 SDK 讀取多元索引中的資料。pom 引入 SDK 。

 <dependency>
     <groupId>com.aliyun.openservices</groupId>
     <artifactId>tablestore</artifactId>
     <version>5.10.3</version>
 </dependency>           

精确查詢 

搜尋購買過某品牌的使用者。傳入需要搜尋的品牌,通過多元索引 order_contract_index 以及品牌字段 p_brand 進行搜尋。

 public List<String> getUserByBrand(String brand) {

        // 組裝請求參數
        SearchQuery searchQuery = new SearchQuery();
        searchQuery.setGetTotalCount(true);

        BoolQuery boolQuery = new BoolQuery();

        TermQuery applierNameQuery = new TermQuery();
        applierNameQuery.setFieldName("p_brand");
        applierNameQuery.setTerm(ColumnValue.fromString(brand));

        boolQuery.setMustQueries(Arrays.asList(
                applierNameQuery
        ));

        searchQuery.setQuery(boolQuery);

        SearchRequest searchRequest = new SearchRequest("order_contract", "order_contract_index", searchQuery);
        SearchRequest.ColumnsToGet columnsToGet = new SearchRequest.ColumnsToGet();
        columnsToGet.setReturnAll(true);
        searchRequest.setColumnsToGet(columnsToGet);

        // 進行搜尋
        SearchResponse response = syncClient.search(searchRequest);

        // 解析傳回資料
        List<String> userList = new ArrayList<>();
        if (response != null && !CollectionUtils.isEmpty(response.getRows())) {
            List<Row> item = response.getRows();
            for (Row r : item) {
                userList.add(r.getColumn("c_id").get(0).getValue().asString());
            }
        }

        return userList;
    }           

範圍查詢

搜尋在某店鋪購買的商品單價在 500 元到 600 元之間的使用者。

    public List<String> searchByBrandAndKey(String brand, Double high, Double low) {

        // 組裝請求參數
        SearchQuery searchQuery = new SearchQuery();
        searchQuery.setGetTotalCount(true);

        BoolQuery boolQuery = new BoolQuery();

        TermQuery applierNameQuery = new TermQuery();
        applierNameQuery.setFieldName("p_brand");
        applierNameQuery.setTerm(ColumnValue.fromString(brand));

        RangeQuery rangeQuery = new RangeQuery();
        rangeQuery.setFieldName("p_price");
        rangeQuery.setFrom(ColumnValue.fromDouble(low), true);
        rangeQuery.setTo(ColumnValue.fromDouble(high),true);
        
        boolQuery.setMustQueries(Arrays.asList(
                applierNameQuery,
                rangeQuery
        ));

        searchQuery.setQuery(boolQuery);

        SearchRequest searchRequest = new SearchRequest("order_contract", "order_contract_index", searchQuery);
        SearchRequest.ColumnsToGet columnsToGet = new SearchRequest.ColumnsToGet();
        columnsToGet.setReturnAll(true);
        searchRequest.setColumnsToGet(columnsToGet);

        // 進行搜尋
        SearchResponse response = syncClient.search(searchRequest);

        // 解析傳回資料
        List<String> userList = new ArrayList<>();
        if (response != null && !CollectionUtils.isEmpty(response.getRows())) {
            List<Row> item = response.getRows();
            for (Row r : item) {
                userList.add(r.getColumn("c_id").get(0).getValue().asString());
            }
        }

        return userList;
    }           

通配符查詢

搜尋購買過包含關鍵字的商品的客戶。

    public List<String> searchByKeyInProductName(String key) {
        // 組裝請求參數
        SearchQuery searchQuery = new SearchQuery();
        searchQuery.setGetTotalCount(true);

        BoolQuery boolQuery = new BoolQuery();

        WildcardQuery wildcardQuery = new WildcardQuery();
        wildcardQuery.setFieldName("p_name");
        wildcardQuery.setValue("*" + key + "*");

        boolQuery.setMustQueries(Arrays.asList(
                wildcardQuery
        ));

        searchQuery.setQuery(boolQuery);

        SearchRequest searchRequest = new SearchRequest("order_contract", "order_contract_index", searchQuery);
        SearchRequest.ColumnsToGet columnsToGet = new SearchRequest.ColumnsToGet();
        columnsToGet.setReturnAll(true);
        searchRequest.setColumnsToGet(columnsToGet);

        // 進行搜尋
        SearchResponse response = syncClient.search(searchRequest);

        // 解析傳回資料
        List<String> userList = new ArrayList<>();
        if (response != null && !CollectionUtils.isEmpty(response.getRows())) {
            List<Row> item = response.getRows();
            for (Row r : item) {
                userList.add(r.getColumn("c_id").get(0).getValue().asString());
            }
        }

        return userList;

    }           

更多查詢

除了上文提到的查詢方式外,多元索引還支援許多豐富的查詢方式,例如模糊查詢、地理位置查詢、多條件組合查詢、嵌套查詢等等。同時還支援統計聚合、排序、并發導出資料等功能,更多關于多元索引的介紹可參考官網

多元索引

與 MySQL 索引比對

多元索引在複雜的組合檢索、聚合檢索場景下,比 MySQL 更具有優勢。

  • 多元索引不需要遵守最左比對原則,可以一張索引支援所有需求。而 MySQL 需要針對不同需求建立多個索引,索引資料占用空間大,難以維護。
  • 多元索引支援非主鍵列的條件查詢、任意列的自由組合查詢、And ,Or,Not等關系查詢、全文檢索、地理位置查詢、字首查詢、模糊查詢、嵌套結構查詢、Null值查詢、統計聚合(min、max、sum、avg、count、distinct_count和group_by)。功能層面遠強于 MySQL 索引。

下面給出幾個大規模訂單場景下的需求以及實作樣例并對比性能。

基于訂單金額、狀态等組合檢索

需求:搜尋 2021 年 6 月 30 日零點以來成交額在 2000 元以上,且商品品牌中包含特定關鍵字的訂單,按商品單價倒序排列取前 1000。

對應 SQL如下,執行時間分鐘級。MySQL 中建立有p_price,total_price,pay_time 的聯合索引。符合篩選條件的記錄數約為 16W 條。

select * from order_contract 
where total_price > 2000 and pay_time > 1624982400000000
and p_brand like "%牌22%" order by p_price desc limit 1000           

JAVA 中通路 Tablestore 代碼如下,執行時間秒級。

 SearchRequest searchRequest = SearchRequest.newBuilder()
                .tableName("order_contract")
                .indexName("order_contract_index")
                .searchQuery(
                        SearchQuery.newBuilder()
                                .query(QueryBuilders.bool().must(QueryBuilders.range("total_price").greaterThan(2000))
                                .must(QueryBuilders.wildcard("p_brand","*牌22*"))
                                .must(QueryBuilders.range("pay_time").greaterThan(1624982400000000L)))
                                .sort(new Sort(Arrays.asList(new FieldSort("p_price", SortOrder.DESC))))
                                .limit(1000)
                                .build())
                .build();

        SearchResponse response = syncClient.search(searchRequest);           

報表分析、營運推廣

需求:統計 2021 年 6 月 30 日零點以來,下單金額最高的 100 個客戶。涉及記錄數大于 1200W 條。

對應 SQL如下,執行時間約兩分半。MySQL 建有 pay_time, c_id, total_price 的聯合索引。

SELECT c_id ,sum(total_price) as a FROM order_contract where pay_time >= '2021-06-30 00:00:00'
group by c_id 
order by a desc limit 100           

JAVA 中通路 Tablestore 代碼如下,執行時間約為15秒。

 SearchRequest searchRequest = SearchRequest.newBuilder()
            .tableName("order_contract")
            .indexName("order_contract_index")
            .addColumnsToGet("c_id","total_price")
            .searchQuery(
                    SearchQuery.newBuilder()
                            .query(QueryBuilders.range("pay_time").greaterThan(1624982400000000L))
                            .addGroupBy(GroupByBuilders.groupByField("c_id","c_id")
                                    .addGroupBySorter(GroupBySorter.subAggSortInDesc("sumPrice"))
                                    .addSubAggregation(AggregationBuilders.sum("sumPrice", "total_price"))
                            .size(100))
                            .build())
            .build();

    // 進行搜尋
    SearchResponse response = syncClient.search(searchRequest);           

總結

Tablestore 的多元索引功能對類似海量訂單場景下的搜尋功能提供了較好的支援。使用多元索引,開發者可以以更小的開發成本、更低的運維成本,實作訂單搜尋這樣的需求。

本文對 Tablestore 多元索引做了簡要介紹,并展示了如何建立索引,以及如何在JAVA程式中利用建立的索引進行搜尋。

附錄

代碼 git 位址:

https://github.com/aliyun/tablestore-examples http://gitlab.alibaba-inc.com/lihongnan.lhn/OTStest/branches