万字长文之分库分表里如何优化分页查询?【后端面试题 | 中间件 | 数据库 | MySQL | 分库分表 | 分页查询】

分库分表的一般做法

一般会使用三种算法:

  1. 哈希分库分表:根据分库分表键算出一个哈希值,根据这个哈希值选择一个数据库。最常见的就是数字类型的字段作为分库分表键,然后取余。比如在订单表里,可以按照买家的ID除以8的余数进行分表
  2. 范围分库分表:将某个数据按照范围大小进行分段。比如说根据ID,[0,1000)在一张表,[1000,2000)在另外一张表。最常见的应该是按照日期进行分库分表,比如每个月一张表
  3. 中间表:引入一个中间表来记录数据所在的目标表。一般是记录主键到目标表的映射关系。
    在这里插入图片描述
    这三者并不互斥,也就是说可以考虑使用哈希分库分表,同时引入一个中间表;也可以先进行范围分库分表,再引入一个中间表。

分库分表中间件的形态

  1. SDK形态:通过依赖的形式引入代码里,比如Java的依赖ShardingSphere
  2. Proxy形态:独立部署的分库分表中间件,对于所有的业务方来说,就像一个普通的数据库,业务方的查询发送过去后,就会执行分库分表,发起实际的查询,再把查询结果返回给业务方。ShardingSphere也支持这种形态。
    在这里插入图片描述
  3. Sidecar形态:提供了一个分库分表的Sidecar,但是现在并没有非常成熟的产品

Sidecar是一种分库分表中间件的形态。它是一个理论上的概念,指的是一个独立的组件,为应用程序提供分库分表的功能。在这种形态下,Sidecar作为应用程序的伴随服务运行,类似于服务网格中的Sidecar容器,它与应用程序实例部署在一起,但作为独立的进程运行。
在这里插入图片描述

其中,SDK形态的性能最好,但是和语言强耦合。
Proxy形态性能最差,因为所有的数据库查询都发送给它了,很容易成功性能瓶颈。尤其单机部署Proxy的话,还面临着单节点故障的问题。优点是跟编程语言无关,部署一个Proxy之后可以给使用不同编程语言的业务使用。同时,业务方可以轻易地从单库单表切换到分库分表。
在这里插入图片描述
Sidecar 目前还没有成熟的产品,但是从架构上来说它的性能应该介于 SDK 和 Proxy 之间,并且也没有单体故障、集群管理等烦恼。

面试准备

还需要弄清楚几个问题:

  • 公司是如何解决分库分表中的分页问题的?
  • 有没有因为排序或分页而引起的性能问题?最终怎么解决的

还可以去看看公司的监控数据,注意下分页查询的响应时间。并且在业务高峰期或是频繁执行分页的时候,看看内存和CPU的使用率。这些数据可以作为分页查询比较引起性能问题的证据

面试策略上来说,最好把分页查询优化作为你性能优化的一个举措,可以进一步和前面的查询优化、数据库参数优化相结合,这样方案会更完善,能力会更全面。

如果面试官问到了数据库性能优化和数据库分页查询,你都可以尝试把话题引导到分页查询上。

基本思路

可以尝试介绍一下是如何优化数据库性能的,比如SQL本身优化、数据库优化,然后罗列出准备的SQL案例,说明你在SQL优化方面做过哪些事情,比如优化过分库分表的查询,其中最典型的就是优化分页查询。
假设之前是全局查询,现在采用禁用跨页查询的方案来优化

最开始我在公司监控慢查询的时候,发现有一个分页查询非常慢。这个分页查询是按照更新时间降序来排序的。后来我发现那个分页查询用的是全局查询法,因为这个接口原本是提供给 Web 端用的,而 Web 端要支持跨页查询,所以只能使用全局查询法。当查询的页数靠后的时候,响应时间就非常长。 后来我们公司搞出 App 之后,类似的场景直接复用了这个接口。但是事实上在 App 上是没有跨页需求的。所以我就直接写了一个新接口,这个接口要求分页的时候带上上一页的最后一条数据的更新时间。也就是我用这个更新时间构造了一个查询条件,只查询早于这个时间的数据。那么分页查询的时候 OFFSET 就永远被我控制在 0 了,查询的时间就非常稳定了。

最后你可以加一个总结。

分页查询在分库分表里面是一个很难处理的问题,要么查询可能有性能问题,比如说这里使用的全局查询法,要么就是要求业务折中,比如说我优化后禁用了跨页,以及要求数据平均分布的平均分页法,当然还有各方面都不错,但是实现比较复杂的二次查询法、中间表法。

当面试官追问你其中细节的时候,你就可以这样来引导。

全局查询

理论上说,分页查询要在全局有序的情况下进行,但是在分库分表以后,要做到全局有序就很难了。假如说我们的数据库order_tab是以buyer_id % 2来进行分表的,如果你要执行一个语句

SELECT * FROM order_tab ORDER BY id LIMIT 4 OFFSET 2

实际执行查询的时候,就要考虑各种数据的分布情况。

  • 符合条件的数据全部在某个表里面。在这就是order_tab_0上有全部数据,或是order_tab_1上有全部数据。
    在这里插入图片描述
  • 偏移量中前面两条全部在一张表,但是符合条件的数据在另外一张表
    在这里插入图片描述
  • 偏移量和数据在两张表都有
    在这里插入图片描述
    在分库分表中,一个SELECT语句生成的目标语句是这样的:
SELECT * FROM order_tab ORDER BY id LIMIT 6 OFFSET 0
SELECT * FROM order_tab ORDER BY id LIMIT 6 OFFSET 0

注意看LIMIT部分,被修改成了0,6。通俗的说,如果一个分页语句是 LIMIT x OFFSET y 的形式,那么最终生成的目标语句就是 LIMIT x + y OFFSET 0。

LIMIT x OFFSET y => LIMIT x+y OFFSET 0

当分库分表中间件拿到这两个语句的查询结果之后,就要在内存里进行排序,再找出全局的LIMIT 4 OFFSET 2
可以先回答这种全局排序的思路,关键词就是 LIMIT x + y OFFSET 0

分库分表中间件一般采用的就是全局排序法。假如说我们要查询的是LIMIT X OFFSET y,那么分库分表中间件会把查询改写为LIMIT x+y OFFSET 0,然后把查询请求发送给所有的目标表。在拿到所有的返回值后,在内存中排序,然后根据排序结果找出全局符合条件的目标数据。

接下来可以先从性能问题上刷一个亮点,抓住受影响的三个方面:网络、内存和CPU

这个解决方案的最大问题就是性能不好。
首先是网络传输瓶颈,比如在LIMIT 10 OFFSET 1000这种场景下,如果没有分库分表,只需要传输10条数据;在分库分表的情况下,如果命中了N个表,那么需要传输的是(1000+10)*N条数据。而实际上最终我们只会用其中的10条数据,存在巨大的浪费。
其次是内存瓶颈。收到那么多数据之后,中间件需要维持在内存中排序。
CPU也会成为瓶颈,因为排序本身是一个CPU密集的操作。所以在Proxy形态的分库分表中间件里,分页查询一多,就容易把中间件的内存耗尽,引发OOM,又或是CPU 100%。
不过可以通过归并排序来缓解这些问题。

关键在拿到数据之后,使用归并排序的算法。

在分库分表里,可以使用归并排序算法来给返回的结果排序,也就是说在改写为LIMIT x+y OFFSET 0之后,每个目标表返回的结果都是有序的,自然可以使用归并排序。在归并排序的过程中,我们可以逐条从返回结果中读取,这意味着没必要将所有的结果一次性放到内存中再排序。在分页的场景下,取够了数据可以直接返回,剩下的数据就可以丢弃了

在这里插入图片描述
前面说了全局查询这个方案的性能很差,那么有没有其他方案呢?
的确有,比如平均分页、禁用跨页查询、换用其他中间件等。不过任何方案都不是十全十美的,这些方案也存在一些难点,有的是需要业务折中,有的处理过程非常复杂。我们先来看第一个需要业务折中的平均分页方案

优化方案1:平均分页

看到分页查询的第一个念头应该是:能不能在不同的表上平均分页查询数据,得到的结果合并在一起就是分页的结果
例如,查询中的语句是这样的

SELECT * FROM order_tab ORDER BY id LIMIT 4 OFFSET 2

因为本身有两张表,可以改成这样

SELECT * FROM order_tab_0 ORDER BY id LIMIT 2 OFFSET 1
SELECT * FROM order_tab_1 ORDER BY id LIMIT 2 OFFSET 1

在每一张表都查询从偏移量1开始的2条数据,那么合并在一起就可以认为从全局的偏移量2开始的4条数据。
在这里插入图片描述图里我们能够看出来,按照道理全局的 LIMIT 4 OFFSET 2 拿到的应该是 3、4、5、6 四条数据。但是这里我们拿到的数据却是 2、4、5、9。这也就是这个方案的缺陷:它存在精度问题。也就是说,它返回的数据并不一定是全局最精确的数据

那么这个方案是不是就不能用了呢?并不是的,在一些对顺序、精度要求不严格的场景下,还是可以用的。例如浏览页面,你只需要返回足够多的数据行,但是这些数据具体来自哪些表,用户并不关心。
关键词就是平均分页

在一些可以接受分页结果不精确的场景下,可以考虑平均分页的做法。举个例子来说,如果查询的是 LIMIT 4 OFFSET 2,并且命中了两张目标表,那么就可以考虑在每个表上都查询 LIMIT 2 OFFSET 1。这些结果合并在一起就是 LIMIT 4 OFFSET 2 的一个近似答案。这种做法对于数据分布均匀的分库分表效果很好,偏差也不大

这个方案还有一个进阶版本,就是根据数据分布来决定如何取数据。

更加通用的做法是根据数据分布来决定分页在不同的表上各自取多少条数据。比如说一张表上面有 70% 的数据,但是另一张表上只有 30% 的数据,那么在 LIMIT 10 OFFSET 100 的场景下,可以在 70% 的表里取 LIMIT 7 OFFSET 70,在 30% 的表里取 LIMIT 3 OFFSET 30。所以,也可以把前面平均分配的方案看作是各取 50% 的特例

那如何知道一张表上有70%的数据,另外一张表上有30%。
在开发的时候先用SQL在不同的表上执行一下,看看同样的WHERE条件下各自返回了多少数据,就可以推断出来了。
不过实际上,能够接受不精确的业务场景还是比较少的。所以我们还有一种业务折中的解决方案,它精确并且高效,也就是禁用跨页查询方案。

优化方案2:禁用跨页查询

只允许用户从第0页开始,逐页往后翻,不允许跨页。
假如业务上分页查询是50条数据一页,那么发起的查询依次是:

SELECT * FROM order_tab ORDER BY id LIMIT 50 OFFSET 0
SELECT * FROM order_tab ORDER BY id LIMIT 50 OFFSET 50
SELECT * FROM order_tab ORDER BY id LIMIT 50 OFFSET 100
...

不断增长的只有偏移量,如何控制住这个偏移量呢?
答案是根据ORDER BY的部分来增加一个查询条件。上述例子里的order by是根据id升序排序的,只需要在where部分增加一个大于上次查询的最大id的条件就可以了。max_id 是上一批次的最大id

SELECT * FROM order_tab WHERE `id` > max_id ORDER BY id LIMIT 50 OFFSET 0

即使order by里使用了多个列,规则也是一样的

总体来看,回答要分成两部分,第一部分介绍基本做法,关键词是拿到上一批次的极值

目前比较好的分页做法是禁用跨页查询,然后在每一次查询条件里加上上依次查询的极值,也就是最大值或者最小值。比如说第一次查询的时候ORDER BY ID LIMIT 10 OFFSET 0,那么下一页就可以改为WHERE id > max_id ORDER BY ID LIMIT 10 OFFSET 0。在现在的手机 App 里这个策略是非常好用的,因为手机 App 都是下拉刷新,天然就不存在跨页的问题。

第一部分提到了极值,面试官可能问你什么时候用最大值,什么时候用最小值,可以这样说:

至于用最大值还是最小值,取决于order by。总的原则就是升序用最大值,降序用最小值。如果order by里面包含了多个列,那么针对每一个列是升序还是降序,来确定使用最大值还是最小值。

这种方案并没有彻底解决分库分表查询中的分页问题,但是控制了偏移量,极大的减少了网络通信的消耗和磁盘扫描的消耗。

优化方案3:换用中间件

一种思路是使用NoSQL之类的来存储数据,比如使用Elasticsearch、ClickHouse;另一种思路是使用分布式关系型数据库,相当于把分页的难题抛给了数据库

优化方案4:二次查询(亮点)

先尝试获取某个数据的全局偏移量,再根据这个偏移量来计算剩下数据的偏移量。这里用一个例子来阐述它的基本原理,再抽象出一般步骤。
假设我们的查询是

SELECT * FROM order_tab ORDER BY id LIMIT 4 OFFSET 4

数据分布如图所示:
在这里插入图片描述
全局的LIMIT 4 OFFSET 4 是 5、6、7、8 四条数据

步骤1:首次查询

把SQL语句改写成这样:

SELECT * FROM order_tab_0 ORDER BY id LIMIT 4 OFFSET 2
SELECT * FROM order_tab_1 ORDER BY id LIMIT 4 OFFSET 2

我们只是把OFFSET平均分配了,但是LIMIT没变
第一次查询到的数据是这样
在这里插入图片描述order_tab_0 拿到了 4、6、10、12,而 order_tab_1 拿到了 7、8、9、11

步骤二:确认最小值

id最小的是4,来自order_tab_0

步骤三:二次查询

这一次查询需要利用上一步找出来的最小值以及各自分库的最大值来构造BETWEEN查询,改写得到的SQL是:

SELECT * FROM order_tab_0 WHERE id BETWEEN 4 AND 12
SELECT * FROM order_tab_1 WHERE id BETWEEN 4 AND 11

结果:

  • order_tab_0 返回 4、6、10、12。
  • order_tab_1 返回 5、7、8、9、11,也就是多了 1 条数据,记住这一点。
    在这里插入图片描述
    取过来的所有数据排序之后就是4、5、6、7、8、9、10、11、12

步骤四:计算最小值的全局偏移量

核心是:根据BETWEEN中多出来的数据量来推断全局偏移量

现在我们知道4在order_tab_0中的偏移量是2,也就是说比4小的数据有2条。
在BETWEEN查询里,order_tab_1返回的结果是5,7,8,9,11,其中7在第一次查询里的偏移量是2,所以5的偏移量是1。也就是说,5的前面只有一条比4小的数据。
那么4在order_tab中的全局偏移量就是1+2=3,也就是4前面有三条数据。
在这里插入图片描述
加上4本身,刚好构成了OFFSET 4,因此从5开始取,往后取4条数据。

总结

简化版本:

  1. 首次查询,拿到最小值
  2. 二次查询,确实最小值的全局偏移量
  3. 在二次查询的结果里根据最小值取到符合偏移量的数据

抽象版本:
假设分库分表共有n个表,查询是LIMIT X OFFSET Y,那么:

  1. 首先发送查询语句 LIMIT X OFFSET Y/N 到所有的表
  2. 找到返回结果中的最小值(升序),记作min
  3. 执行第二次查询,关键是BETWEEN min AND max,其中max是第一次查询的数据中每个表各自的最大值
  4. 根据min、第一次查询和第二次查询的值来确定min的全局偏移量。总的来说,min在某个表里的偏移量这样计算:如果第二次查询比第一次查询多了K条数据,偏移量就是Y/N-K。然后把所有表的偏移量加在一起,就是min的全局偏移量
  5. 根据min的全局偏移量,在第二次查询的结果里面向后补足到Y,得到第一条数据的位置,再取X条。

优化方案5:引入中间表(亮点)

引入中间表的意思是额外存储一份数据,只用来排序。这个方案里面就是在中间表里加上排序相关的列
在这里插入图片描述

排序是一个非常常见的需求,那么就可以考虑引入一个中间表来辅助排序。比如说用更新时间来排序的时候,在中间表里加上更新时间。查询的时候先在中间表里查到目标数据,再去目标表里把全部数据都查询出来。
有两个明显的缺陷:一是WHERE只能使用中间表上的列;二是维护中间表也会引起数据一致性问题。

在这里插入图片描述
那么如何解决数据一致性问题呢?

比较简单的做法就是业务保持双写,也就是写入目标表也写入中间表。不过这里我更加建议使用 Canal 之类的框架来监听 binlog,异步更新中间表。这样做的好处是业务完全没有感知,没有什么改造成本。更新的时候可以考虑引入重试机制,进一步降低失败的几率。

面试官可能进一步问你,如果更新中间表经过重试之后也失败了,怎么办?
这时候并没有更好的办法,无非就是引入告警,然后人工介入处理。最后你可以再总结一下这个方案。

这个方案是一个依赖最终一致性的方案,在强调强一致性的场景下并不是很合适。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/web/45851.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【Flutter】 webview_flutter避坑

webview_flutter webview_flutter没有SSL Error接口,也就是说等你的网页出现SSL 错误的时候这个插件无法捕捉处理,除非你改它的源码。 下面这段是webview_flutter官网的例子,它有onHttpError、onWebResourceError、但没有任何捕捉 SSL 错误…

代谢组数据分析(十五):基于python语言构建PLS-DA算法构建分类模型

介绍 本教程描述了一个具有二元分类结果的研究的典型代谢组学数据分析工作流程。主要步骤包括: 从Excel表格导入代谢物和实验数据。基于汇总QC的数据清洗。利用主成分分析可视化来检查数据质量。两类单变量统计。使用偏最小二乘判别分析(PLS-DA)进行多变量分析,包括: 模型…

go语言 fmt的几个打印区别以及打印格式

文章目录 一、打印Print1.1 fmt.Print 和 fmt.Println1.2fmt.Printf1.3 fmt.Sprint, fmt.Sprintf, 和 fmt.Sprintln1.4 fmt.Fprint, fmt.Fprintf, 和 fmt.Fprintln 二、打印格式基本格式动词整数类型浮点数和复数类型字符串和字节切片布尔类型指针 一、打印Print Go 语言的 fm…

字符串类中的常用方法

1 string对象的创建 静态创建 String s1  "abc";  String s2  "abc";  动态创建 String s3  new String("abc"); String s4  new String("abc"); 2string对象的不可变性 任何一个String对象在创建之后都不能对它的…

大数据环境下的房地产数据分析与预测研究的设计与实现

1绪论 1.1研究背景及意义 随着经济的快速发展和城市化进程的推进,房地产市场成为了国民经济的重要组成部分。在中国,房地产行业对经济增长、就业创造和资本投资起到了重要的支撑作用。作为中国西南地区的重要城市,昆明的房地产市场也备受关…

云备份服务端

文件使用工具和json序列化反序列化工具 //文件和json工具类的设计实现 #ifndef __UTIL__ #define __UTIL__ #include<iostream> #include<fstream> #include<string> #include <vector> #include<sys/stat.h> #include"bundle.h" #inc…

FPGA资源容量

Kintex™ 7 https://www.amd.com/zh-tw/products/adaptive-socs-and-fpgas/fpga/kintex-7.html#product-table AMD Zynq™ 7000 SoC https://www.amd.com/en/products/adaptive-socs-and-fpgas/soc/zynq-7000.html#product-table AMD Zynq™ UltraScale™ RFSoC 第一代 AMD Z…

【每日一练】python类的构造方法以及常用的魔术方法详细讲解

在Python中&#xff0c;构造方法是一个用来初始化新创建的对象状态的特殊方法。Python中的构造方法是__init__。此外&#xff0c;Python中的"魔术方法"是Python提供的一系列特殊方法&#xff0c;它们都以双下划线开头和结尾&#xff0c;例如__init__, __str__, __rep…

Redis系列命令更新--Redis列表命令

Redis列表 1、Redis Blpop命令&#xff1a; &#xff08;1&#xff09;说明&#xff1a;Redis Blpop命令移出并获取列表的第一个元素&#xff1b;如果列表没有元素会阻塞列表直到等到超时或发现可弹出元素为止 &#xff08;2&#xff09;语法&#xff1a;redis 127.0.0.1:63…

mybaits-plus自定义分页查询

1. 引入依赖 在 pom.xml 文件中添加必要的依赖&#xff1a; <dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter</artifactId></dependency><dependency><groupId&…

Git进阶(十四):Windows下 git 提示 Repository not found

文章目录 一、前言 一、前言 在Windows10下执行 git clone/pull 的时候出现Repository not found错误。解决方法如下&#xff1a; 打开控制面板 搜索“凭据” 选中“Windows凭据” 在“普通凭据”一栏中查看当前git服务器配置的凭据信息是否正确&#xff0c;出现以上问题的…

081、Python 关于方法重写

所谓方法重写&#xff0c;就是子类对父类已有的方法&#xff0c;重新编写自己的实现版本&#xff0c;这个过程就叫做方法重写&#xff08;override&#xff09;。 说到方法重写&#xff0c;就不得不提多态。因为方法重写是实现多态的一种重要方式。 所谓多态&#xff0c;就是…

Windows的包管理器Chocolatey

Chocolatey 是 Windows 平台上的一个软件包管理工具&#xff0c;类似于 Linux 上的 apt、yum 或者 macOS 上的 Homebrew。你可以通过命令行快速安装、更新和卸载软件包。 一、安装 Chocolatey 查看是否有安装 choco --version 2.3.0如果有显示版本号&#xff0c;说明安装成功…

Laravel IDE Helper:开发者的代码导航灯塔

Laravel IDE Helper&#xff1a;开发者的代码导航灯塔 在Laravel开发过程中&#xff0c;IDE&#xff08;集成开发环境&#xff09;的辅助工具可以极大地提高开发效率和代码质量。Laravel IDE Helper是一个专门为此目的设计的工具&#xff0c;它为PHP IDE提供了额外的上下文信息…

提高自动化测试脚本编写效率 5大关键注意事项

提高自动化测试脚本编写效率能加速测试周期&#xff0c;减少人工错误&#xff0c;提升软件质量&#xff0c;促进项目按时交付&#xff0c;增强团队生产力和项目成功率。而自动化测试脚本编写效率低下&#xff0c;往往会导致测试周期延长&#xff0c;增加项目成本&#xff0c;延…

Python项目部署到Linux生产环境(uwsgi+python+flask+nginx服务器)

1.安装python 我这里是3.9.5版本 安装依赖&#xff1a; yum install zlib-devel bzip2-devel openssl-devel ncurses-devel sqlite-devel readline-devel tk-devel gcc make -y 根据自己的需要下载对应的python版本&#xff1a; cd local wget https://www.python.org/ftp…

git 提交远程仓库 方式

第一种方式&#xff1a; git init //初始化 gitgit clone https://github.com/luckygilrhpp/gitTest.git //克隆远程仓库代码将要提交的文件夹复制到这个clone的远程仓库里面 第二种方式&#xff1a; git init git remote add origin https://github.com/lucky…

高性能存储 SIG 月度动态:优化 xfs dax reflink 时延,独立选型并维护 mdadm 和 ledmon

本次高性能存储月度动态综合了 SIG 在 5、6 两个月的工作进展&#xff0c;包含多项特性支持&#xff0c;性能优化&#xff0c;以及 Bugfix 等。 01 SIG 整体进展 引入 xfs deferred inode inactivation&#xff0c;进一步优化 xfs dax reflink 过程中的抖动时延。 Intel 将在…

UE5.4新功能 - MotionDesign上手简介

MotionDesign是UE中集成的运动图形功能&#xff0c;我们在游戏中经常会见到&#xff0c;例如前方漂浮于空中的若干碎石&#xff0c;当玩家走进时碎石自动吸附合并变成一条路&#xff0c;或者一些装饰性的物件做随机运动等等&#xff0c;在引擎没有集成运动图形时&#xff0c;这…

网络爬虫开发:JavaScript与Python特性的小差异

JavaScript JavaScript具有以下一些主要特点&#xff1a; 动态类型&#xff1a; JavaScript是一种动态类型语言&#xff0c;变量可以存储任意类型的数据&#xff0c;无需事先声明变量的类型。事件驱动&#xff1a;JavaScript主要用于处理用户在浏览器中的各种交互事件&#x…