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

本文涉及的产品
RDS MySQL Serverless 基础系列,0.5-2RCU 50GB
云数据库 RDS MySQL,集群系列 2核4GB
推荐场景:
搭建个人博客
表格存储 Tablestore,50G 2个月
简介: 背景在大规模订单系统中,存在以下常见需求:查询某店铺过去一段时间成交额查询某品牌商品在过去一周内的成交额查询在某店铺购物的客户列表……因此,开发者对于数据库在非主键查询、多列的自由组合查询等复杂查询需求上会有比较高的要求。传统的订单系统会使用 Elasticsearch 或者 Solr 来实现这一需求,但伴随而来的是更高的系统复杂度和更加昂贵的系统维护成本。Tablestore 的多元索引,能够支

背景

在大规模订单系统中,存在以下常见需求:

  • 查询某店铺过去一段时间成交额
  • 查询某品牌商品在过去一周内的成交额
  • 查询在某店铺购物的客户列表
  • ……

因此,开发者对于数据库在非主键查询、多列的自由组合查询等复杂查询需求上会有比较高的要求。传统的订单系统会使用 Elasticsearch 或者 Solr 来实现这一需求,但伴随而来的是更高的系统复杂度和更加昂贵的系统维护成本。

Tablestore 的多元索引,能够支持此类数据检索工作,且具有操作简单、维护成本低等特点,可以将开发者从索引建立、数据同步、集群维护等工作中解放出来。本文将简要介绍多元索引,展示如何在 Tablestore 实例上创建多元索引,并通过JAVA代码展示利用多元索引实现搜索需求。

多元索引简介

Tablestore 的多元索引,底层使用自研索引引擎,基于倒排索引和列式存储,可以支持非主键列查询、全文检索、前缀查询、模糊查询、多字段自由组合查询、嵌套查询、地理位置查询和统计聚合(max、min、count、sum、avg、distinct_count、group_by)等复杂查询功能。不同于 MySQL 等传统数据库的索引使用方式,多元索引无最左匹配原则限制,使用时非常灵活。一般情况下一张表只需要创建一个多元索引即可。

其架构如图。数据在 Tablestore 的基础表中写入,基础表中的增量数据会通过异步的方式被拉入多元索引。由于这个异步操作,多元索引中的数据相比于基础表数据存在一定延迟,这个延迟在几秒到十几秒的量级。由图可以看出,基于主键列的读取会由基础表进行支持;而多元索引会承担相对更加复杂的非主键列查询、全文检索、组合查询、聚合查询等查询功能。架构实现了不同流量的分离,部分实现了读写分离。

 

更详细的多元索引介绍可以参考:多元索引简介

多元索引创建

索引创建

进入Tablestore控制台首页。点击创建的 Tablestore 实例。

点击订单表 order_contract 对应的索引管理,进入索引管理界面。

点击创建多元索引

输入索引名称。选择手动录入索引字段。这里,选择订单 id(oId)、商品品牌(p_brand)、商品名称(p_name)、客户名称(c_name)、卖家名称(s_name)、商品单价(p_price)、支付时间(pay_time)、客户 id(c_id)、卖家 id(s_id)、交易金额(total_price)作为索引字段。点击确定完成索引创建。

可以在索引管理页看到索引相关记录。

索引同步

多元索引创建后,需要同步存量数据,同步过程中,同步状态显示为存量;数据同步结束后,同步状态显示为增量。此时可以在行数统计处看到记录总数。

索引查询

点击搜索。

添加查询字段,选择精确查询,输入需要查询的

搜索结果如下。

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

相关实践学习
如何快速连接云数据库RDS MySQL
本场景介绍如何通过阿里云数据管理服务DMS快速连接云数据库RDS MySQL,然后进行数据表的CRUD操作。
全面了解阿里云能为你做什么
阿里云在全球各地部署高效节能的绿色数据中心,利用清洁计算为万物互联的新世界提供源源不断的能源动力,目前开服的区域包括中国(华北、华东、华南、香港)、新加坡、美国(美东、美西)、欧洲、中东、澳大利亚、日本。目前阿里云的产品涵盖弹性计算、数据库、存储与CDN、分析与搜索、云通信、网络、管理与监控、应用服务、互联网中间件、移动服务、视频服务等。通过本课程,来了解阿里云能够为你的业务带来哪些帮助 &nbsp; &nbsp; 相关的阿里云产品:云服务器ECS 云服务器 ECS(Elastic Compute Service)是一种弹性可伸缩的计算服务,助您降低 IT 成本,提升运维效率,使您更专注于核心业务创新。产品详情: https://www.aliyun.com/product/ecs
目录
相关文章
|
5月前
|
存储 关系型数据库 MySQL
Linux 安装 mysql 及配置存储位置
Linux 安装 mysql 及配置存储位置
176 3
|
4月前
|
存储 关系型数据库 MySQL
MySQL——数据库备份上传到阿里云OSS存储
MySQL——数据库备份上传到阿里云OSS存储
198 0
|
4月前
|
存储 关系型数据库 MySQL
mysql 使用变量存储中间结果的写法
mysql 使用变量存储中间结果的写法
|
6天前
|
搜索推荐 API 定位技术
一文看懂Elasticsearch的技术架构:高效、精准的搜索神器
Elasticsearch 是一个基于 Lucene 的开源搜索引擎,以其强大的全文本搜索功能和快速的倒排索引技术著称。它不仅支持数字、文本、地理位置等多类型数据,还提供了可调相关度分数、高级查询 DSL 等功能。Elasticsearch 的核心技术流程包括数据导入、解析、索引化、查询处理、得分计算及结果返回,确保高效处理大规模数据并提供准确的搜索结果。通过 RESTful API、Logstash 和 Filebeat 等工具,Elasticsearch 可以从多种数据源中导入和解析数据,支持复杂的查询需求。
30 0
|
2月前
|
存储 关系型数据库 MySQL
PACS系统 中 dicom 文件在mysql 8.0 数据库中的 存储和读取(pydicom 库使用)
PACS系统 中 dicom 文件在mysql 8.0 数据库中的 存储和读取(pydicom 库使用)
45 2
|
2月前
|
存储 SQL 关系型数据库
MySQL 存储函数及调用
MySQL 存储函数及调用
204 3
|
2月前
|
存储 关系型数据库 MySQL
MySQL 如何存储地理信息
MySQL 如何存储地理信息
194 1
|
3月前
|
存储 关系型数据库 MySQL
深入解析MySQL数据存储机制:从表结构到物理存储
深入解析MySQL数据存储机制:从表结构到物理存储
354 1
|
2月前
|
存储 关系型数据库 MySQL
Key_Value 形式 存储_5级省市城乡划分代码 (mysql 8.0 实例)
本文介绍了如何使用MySQL8.0数据库中的Key_Value形式存储全国统计用区划代码和城乡划分代码(5级),包括导入数据、通过数学函数提取省市区信息,以及查询5级行政区划的详细数据。
39 0
|
5月前
|
关系型数据库 MySQL 存储