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

本文涉及的产品
云数据库 RDS MySQL,集群系列 2核4GB
推荐场景:
搭建个人博客
RDS MySQL Serverless 基础系列,0.5-2RCU 50GB
云数据库 RDS MySQL,高可用系列 2核4GB
简介: 背景在大规模订单系统中,存在以下常见需求:查询某店铺过去一段时间成交额查询某品牌商品在过去一周内的成交额查询在某店铺购物的客户列表……因此,开发者对于数据库在非主键查询、多列的自由组合查询等复杂查询需求上会有比较高的要求。传统的订单系统会使用 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

相关实践学习
如何在云端创建MySQL数据库
开始实验后,系统会自动创建一台自建MySQL的 源数据库 ECS 实例和一台 目标数据库 RDS。
全面了解阿里云能为你做什么
阿里云在全球各地部署高效节能的绿色数据中心,利用清洁计算为万物互联的新世界提供源源不断的能源动力,目前开服的区域包括中国(华北、华东、华南、香港)、新加坡、美国(美东、美西)、欧洲、中东、澳大利亚、日本。目前阿里云的产品涵盖弹性计算、数据库、存储与CDN、分析与搜索、云通信、网络、管理与监控、应用服务、互联网中间件、移动服务、视频服务等。通过本课程,来了解阿里云能够为你的业务带来哪些帮助 &nbsp; &nbsp; 相关的阿里云产品:云服务器ECS 云服务器 ECS(Elastic Compute Service)是一种弹性可伸缩的计算服务,助您降低 IT 成本,提升运维效率,使您更专注于核心业务创新。产品详情: https://www.aliyun.com/product/ecs
目录
相关文章
|
14天前
|
Ubuntu Linux
查看Linux系统架构的命令,查看linux系统是哪种架构:AMD、ARM、x86、x86_64、pcc 或 查看Ubuntu的版本号
查看Linux系统架构的命令,查看linux系统是哪种架构:AMD、ARM、x86、x86_64、pcc 或 查看Ubuntu的版本号
126 3
|
15天前
|
设计模式 存储 前端开发
揭秘.NET架构设计模式:如何构建坚不可摧的系统?掌握这些,让你的项目无懈可击!
【8月更文挑战第28天】在软件开发中,设计模式是解决常见问题的经典方案,助力构建可维护、可扩展的系统。本文探讨了.NET中三种关键架构设计模式:MVC、依赖注入与仓储模式,并提供了示例代码。MVC通过模型、视图和控制器分离关注点;依赖注入则通过外部管理组件依赖提升复用性和可测性;仓储模式则统一数据访问接口,分离数据逻辑与业务逻辑。掌握这些模式有助于开发者优化系统架构,提升软件质量。
30 5
|
11天前
|
微服务 API Java
微服务架构大揭秘!Play Framework如何助力构建松耦合系统?一场技术革命即将上演!
【8月更文挑战第31天】互联网技术飞速发展,微服务架构成为企业级应用主流。微服务将单一应用拆分成多个小服务,通过轻量级通信机制交互。高性能Java Web框架Play Framework具备轻量级、易扩展特性,适合构建微服务。本文探讨使用Play Framework构建松耦合微服务系统的方法。Play采用响应式编程模型,支持模块化开发,提供丰富生态系统,便于快速构建功能完善的微服务。
22 0
|
14天前
|
消息中间件 Java RocketMQ
微服务架构师的福音:深度解析Spring Cloud RocketMQ,打造高可靠消息驱动系统的不二之选!
【8月更文挑战第29天】Spring Cloud RocketMQ结合了Spring Cloud生态与RocketMQ消息中间件的优势,简化了RocketMQ在微服务中的集成,使开发者能更专注业务逻辑。通过配置依赖和连接信息,可轻松搭建消息生产和消费流程,支持消息过滤、转换及分布式事务等功能,确保微服务间解耦的同时,提升了系统的稳定性和效率。掌握其应用,有助于构建复杂分布式系统。
31 0
|
15天前
|
消息中间件 缓存 Java
如何优化大型Java后端系统的性能:从代码到架构
当面对大型Java后端系统时,性能优化不仅仅是简单地提高代码效率或硬件资源的投入,而是涉及到多层次的技术策略。本篇文章将从代码层面的优化到系统架构的调整,详细探讨如何通过多种方式来提升Java后端系统的性能。通过对常见问题的深入分析和实际案例的分享,我们将探索有效的性能优化策略,帮助开发者构建更高效、更可靠的后端系统。
|
16天前
|
缓存 架构师 数据库
缓存系统稳定性 - 架构师峰会演讲实录
缓存系统稳定性 - 架构师峰会演讲实录
|
17天前
|
存储 API 数据库
Django后端架构开发:构建在线云媒资系统思路解析
Django后端架构开发:构建在线云媒资系统思路解析
30 0
|
21天前
|
SQL 关系型数据库 MySQL
【揭秘】MySQL binlog日志与GTID:如何让数据库备份恢复变得轻松简单?
【8月更文挑战第22天】MySQL的binlog日志记录数据变更,用于恢复、复制和点恢复;GTID为每笔事务分配唯一ID,简化复制和恢复流程。开启binlog和GTID后,可通过`mysqldump`进行逻辑备份,包含binlog位置信息,或用`xtrabackup`做物理备份。恢复时,使用`mysql`命令执行备份文件,或通过`innobackupex`恢复物理备份。GTID模式下的主从复制配置更简便。
91 2
|
16天前
|
弹性计算 关系型数据库 数据库
手把手带你从自建 MySQL 迁移到云数据库,一步就能脱胎换骨
阿里云瑶池数据库来开课啦!自建数据库迁移至云数据库 RDS原来只要一步操作就能搞定!点击阅读原文完成实验就可获得一本日历哦~
|
19天前
|
关系型数据库 MySQL 数据库
RDS MySQL灾备服务协同解决方案构建问题之数据库备份数据的云上云下迁移如何解决
RDS MySQL灾备服务协同解决方案构建问题之数据库备份数据的云上云下迁移如何解决