0
点赞
收藏
分享

微信扫一扫

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

分库分表的一般做法

一般会使用三种算法:

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

分库分表中间件的形态

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

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

面试准备

还需要弄清楚几个问题:

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

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

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

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

基本思路

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

最后你可以加一个总结。

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

全局查询

理论上说,分页查询要在全局有序的情况下进行,但是在分库分表以后,要做到全局有序就很难了。假如说我们的数据库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

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

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

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

优化方案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。这也就是这个方案的缺陷:它存在精度问题。也就是说,它返回的数据并不一定是全局最精确的数据

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

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

那如何知道一张表上有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里使用了多个列,规则也是一样的

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

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

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

优化方案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:引入中间表(亮点)

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

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

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

举报

相关推荐

0 条评论