基于 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

上一篇:基于 MySQL + Tablestore 分层存储架构的大规模订单系统实践-架构篇


下一篇:vue中在父组件点击按钮触发子组件的事件