炸!业界难题,跨库分页的几种常见方案

简介: 为什么需要研究跨库分页?

互联网很多业务都有分页拉取数据的需求,例如:

(1)消息过多时,拉取第N页消息;

(2)下单过多时,拉取第N页订单;

(3)浏览58同城,查看第N页帖子;

 

这些业务场景对应的消息表,订单表,帖子表分页拉取需求,都有这样一些共同的特点

(1)有个业务主键idmsg_id, order_id, tiezi_id;

(2)分页按照非业务主键id来排序,业务中经常按照时间time来排序order by

 

在数据量不大时,如何来实现跨库分页的需求呢?

(1)在排序字段time上建立索引;

(2)利用SQL提供的offset/limit就能实现;


例如:

select from t_msg order by time offset 200 limit 100;

select from t_order order by time offset 200 limit 100; 

select * from t_tiezi order by time offset 200 limit 100;

画外音:此处假设一页数据为100条,均拉取第3页数据。

 

为什么会有分库的需求?

高并发大流量的互联网架构,一般通过服务层来访问数据库,随着数据量的增大,数据库需要进行水平切分,分库后将数据分布到不同的数据库实例(甚至物理机器)上,以达到降低数据量,增加实例数的扩容目的。

 

一旦涉及分库,逃不开“分库依据” patition key要使用哪一个字段来水平切分数据库呢?

大部分的业务场景,会使用业务主键id

 

确定了分库依据 patition key 后,接下来怎么确定分库算法呢?

大部分的业务场景,会使用业务主键id取模的算法来分库,这样的好处是:

(1)即能够保证每个库的数据分布是均匀的;

(2)又能够保证每个库的请求分布是均匀的;

实在是简单实现负载均衡的好方法,此法在互联网架构中应用颇多。

 

一个更具体的例子:
image.png

用户库user,水平切分后变为两个库:

(1)分库依据patition keyuid

(2)分库算法是uid取模:uid%2余0的数据会落到db0uid%2余1的数据会落到db1

 

数据库进行了水平切分之后,如果业务要查询“最近注册的第3页用户”,即跨库分页查询,该如何实现呢

单库上,可以

select from t_user order by time offset 200 limit 100;

变成两个库后,分库依据是uid,排序依据是time,数据库层失去了time排序的全局视野,数据分布在两个库上,此时该怎么办呢?

 

如何满足“跨越多个水平切分数据库,且分库依据与排序依据为不同属性,并需要进行分页”的查询需求,实现:

select from T order by time offset X limit Y;

这类跨库分页SQL,是后文将要讨论的技术问题。

方案一:全局视野法
image.png
如上图所述,服务层通过uid取模将数据分布到两个库上去之后,每个数据库都失去了全局视野,数据按照time局部排序之后,不管哪个分库的第3页数据,都不一定是全局排序的第3页数据。

那到底哪些数据才是全局排序的第3页数据呢?

需要分三种情况讨论。

 

(1)极端情况,两个库的数据完全一样
image.png

如果两个库的数据完全相同,只需要每个库offset一半,再取半页,就是最终想要的数据(如上图中粉色部分数据)。

 

(2)极端情况,结果数据来自一个库
image.png

也可能两个库的数据分布及其不均衡,例如db0的所有数据的time都大于db1的所有数据的time,则可能出现:一个库的第3页数据,就是全局排序后的第3页数据(如上图中粉色部分数据)。

 

(3)一般情况,每个库数据各包含一部分
image.png

正常情况下,全局排序的第3页数据,每个库都会包含一部分(如上图中粉色部分数据)。

 

由于不清楚到底是哪种情况,所以必须:

(1)每个库都返回3页数据;

(2)所得到的6页数据在服务层进行内存排序,得到数据全局视野;

(3)再取第3页数据,便能够得到想要的全局分页数据。

 

再总结一下这个方案的步骤:

(1)将SQL语句改写,即

order by time offset X limit Y;

改写成

order by time offset 0 limit X+Y;

(2)服务层将改写后的SQL语句发往各个分库

(3)假设共分为N个库,服务层将得到N(X+Y)条数据

(4)服务层对得到的N(X+Y)条数据进行内存排序

(5)内存排序后再取偏移量X后的Y条记录,就是全局视野所需的一页数据;

 

全局视野法有什么优点?

通过服务层修改SQL语句,扩大数据召回量,能够得到全局视野,业务无损,精准返回所需数据

 

全局视野法的缺点呢?

缺点显而易见:

(1)每个分库需要返回更多的数据,增大了网络传输量(耗网络);

(2)除了数据库按照time进行排序,服务层还需要进行二次排序,增大了服务层的计算量(耗CPU);

(3)最致命的,这个算法随着页码的增大,性能会急剧下降,这是因为SQL改写后每个分库要返回X+Y行数据:返回第3页,offset中的X=200;假如要返回第100页,offset中的X=9900,即每个分库要返回100页数据,数据量和排序量都将大增,性能平方级下降。


“全局视野法”虽然性能较差,但其业务无损,数据精准,不失为一种方案,有没有性能更优的方案呢?

 

“任何脱离业务的架构设计都是耍流氓”,技术方案需要折衷,在技术难度较大的情况下,业务需求的折衷能够极大的简化技术方案

 

方案二:禁止跳页查询法

在数据量很大,翻页数很多的时候,很多产品并不提供“直接跳到指定页面”的功能,而只提供“下一页”的功能,这一个小小的业务折衷,就能极大的降低技术方案的复杂度。
image.png

如上图,不能跳页,那么第一次只能够查第一页:

(1)将查询

order by time offset 0 limit 100;

改写成

order by time where time>0 limit 100;

(2)上述改写和offset 0 limit 100的效果相同,都是每个分库返回了一页数据(上图中粉色部分);
image.png

(3)服务层得到2页数据,内存排序,取出前100条数据,作为最终的第一页数据,这个全局的第一页数据,一般来说每个分库都包含一部分数据(如上图粉色部分);

 

这个方案也需要服务器内存排序,岂不是和“全局视野法”一样么?第一页数据的拉取确实一样,但每一次“下一页”拉取的方案就不一样了

 

点击“下一页”时,需要拉取第二页数据,在第一页数据的基础之上,能够找到第一页数据time的最大值:

image.png
上一页记录的time_max,会作为第二页数据拉取的查询条件

(1)将查询

order by time offset 100 limit 100;

改写成

order by time where time>$time_max limit 100;
image.png

(2)这下不是返回2页数据了(“全局视野法,会改写成offset 0 limit 200”),每个分库还是返回一页数据(如上图中粉色部分);
image.png

(3)服务层得到2页数据,内存排序,取出前100条数据,作为最终的第2页数据,这个全局的第2页数据,一般来说也是每个分库都包含一部分数据(如上图粉色部分);

 

如此往复,查询全局视野第100页数据时,不是将查询条件改写为

offset 0 limit 9900+100;(返回100页数据)

而是改写为

time>$time_max99 limit 100;(仍返回一页数据)

以保证数据的传输量和排序的数据量不会随着不断翻页而导致性能下降

 

方案三:允许数据精度损失法

“全局视野法”能够返回业务无损的精确数据,在查询页数较大,例如第100页时,会有性能问题,此时业务上是否能够接受,返回的100页不是精准的数据,而允许有一些数据偏差呢?

 

先来了解一下,数据库分库-数据均衡原理。


什么是,数据库分库-数据均衡原理?

使用patition key进行分库,在数据量较大,数据分布足够随机的情况下,各分库所有patition key属性,在各个分库上数据分布的统计概率情况是一致的

 

例如,在uid随机的情况下,使用uid取模分两库,db0db1

(1)性别属性,如果db0库上的男性用户占比70%,则db1上男性用户占比也应为70%;

(2)年龄属性,如果db0库上18-28岁少女用户比例占比15%,则db1上少女用户比例也应为15%;

(3)时间属性,如果db0库上每天10:00之前登录的用户占比为20%,则db1上应该是相同的统计规律;


image.png

利用这一原理,要查询全局100页数据,只要将:

offset 9900 limit 100;

改写为

offset 4950 limit 50;

即每个分库偏移一半4950),获取半页数据(50条),得到的数据集的并集,基本能够认为,是全局数据的offset 9900 limit 100的数据,当然,这一页数据并不是精准的

 

根据实际业务经验,用户都要查询第100页网页、帖子、邮件的数据了,这一页数据的精准性损失,业务上往往是可以接受的,但此时技术方案的复杂度大大降低了,既不需要返回更多的数据,也不需要进行服务内存排序了。

画外音:如果业务能够接受,这种方案的性能最好,强烈推荐。

 

方案四:二次查询法

有没有一种技术方案,即能够满足业务的精确需要,无需业务折衷,又高性能的方法呢?这就是接下来要介绍的终极武器,“二次查询法”。

 

为了方便举例,假设一页只有5条数据,查询第200页的SQL语句为:

select from T order by time offset 1000 limit 5;

 

步骤一:查询改写

select from T order by time offset 1000 limit 5;

改写为

select from T order by time offset 500 limit 5;

并投递给所有的分库,注意,这个offset的500,来自于全局offset的总偏移量1000,除以水平切分数据库个数2。

画外音:因为数据量比较大,数据随机性较强,不妨设仍然符合“数据库分库-数据均衡定理”。

 

如果是3个分库,则可以改写为

select from T order by time offset 333 limit 5;


假设这三个分库返回的数据(time, uid)如下:
image.png

可以看到,每个分库都是返回的按照time排序的一页数据。

 

步骤二:找到所返回3页全部数据的最小值

第一个库,5条数据的time最小值是1487501123;

第二个库,5条数据的time最小值是1487501133;

第三个库,5条数据的time最小值是1487501143;
image.png

故,三页数据中,time最小值来自第一个库,time_min=1487501123,这个过程只需要比较各个分库第一条数据,时间复杂度很低。

画外音:这个time_min非常重要,后文每一个步骤要都要用到time_min。

 

步骤三:查询二次改写

第一次改写的SQL语句是

select from T order by time offset 333 limit 5;


第二次要改写成一个between语句:

  • between的起点是time_min

  • between的终点是原来每个分库各自返回数据的最大值


第一个分库,第一次返回数据的最大值是1487501523

所以查询改写为:

select from T order by time where time between time_min and 1487501523;

 

第二个分库,第一次返回数据的最大值是1487501323

所以查询改写为

select from T order by time where time between time_min and 1487501323;

 

第三个分库,第一次返回数据的最大值是1487501553

所以查询改写为

select from T order by time where time between time_min and 1487501553;

 

相对第一次查询,第二次查询条件放宽了,故第二次查询会返回比第一次查询结果集更多的数据,假设这三个分库返回的数据(time, uid)如下:
image.png

可以看到:

分库一的结果集,由于time_min来自原来的分库一,所以分库一的返回结果集和第一次查询相同(所以其实这次访问是可以省略的);
分库二的结果集,比第一次多返回了1条数据,头部的1条记录(time最小的记录)是新的(上图中粉色记录);

分库三的结果集,比第一次多返回了2条数据,头部的2条记录(time最小的2条记录)是新的(上图中粉色记录);

 步骤四:在每个结果集中虚拟一个time_min记录,找到time_min在全局的offset
image.png

在第一个库中,time_min在第一个库的offset是333;


在第二个库中,(1487501133, uid_aa)的offset是333(根据第一次查询条件得出的),故虚拟time_min在第二个库的offset是331;

画外音:从333往前推演。


在第三个库中,(1487501143, uid_aaa)的offset是333(根据第一次查询条件得出的),故虚拟time_min在第三个库的offset是330;


画外音:从333往前推演。

 

综上,time_min在全局的offset是333+331+330=994。

 

步骤五:既然得到了time_min在全局的offset,就相当于有了全局视野,根据第二次的结果集,就能够得到全局offset 1000 limit 5的记录
image.png

第二次查询在各个分库返回的结果集是有序的,又知道了time_min在全局的offset是994,一路排下来,容易知道全局offset 1000 limit 5的一页记录(上图中黄色记录)。

 这种方法的优点是:可以精确的返回业务所需数据,每次返回的数据量都非常小,不会随着翻页增加数据的返回量。


帅气不帅气!!!

 

总结

今天介绍了解决“跨N库分页”这一难题的四种方法:

 

方法一:全局视野法

(1)SQL改写,将

order by time offset X limit Y;

改写成

order by time offset 0 limit X+Y;

(2)服务层对得到的N*(X+Y)条数据进行内存排序,内存排序后再取偏移量X后的Y条记录;

这种方法随着翻页的进行,性能越来越低

 

方法二:禁止跳页查询法

(1)用正常的方法取得第一页数据,并得到第一页记录的time_max;

(2)每次翻页,将

order by time offset X limit Y;

改写成

order by time where time>$time_max limit Y;

以保证每次只返回一页数据,性能为常量

 

方法三:允许模糊数据法

(1)SQL查询改写,将

order by time offset X limit Y;

改写成

order by time offset X/N limit Y/N;

性能很高,但拼接的结果集不精准

 

方法四:二次查询法

(1)SQL改写,将

order by time offset X limit Y;

改写成

order by time offset X/N limit Y;

(2)多页返回,找到最小值time_min;

(3)between二次查询

order by time between $time_min and $time_i_max;

(4)设置虚拟time_min,找到time_min在各个分库的offset,从而得到time_min在全局的offset;

(5)得到了time_min在全局的offset,自然得到了全局的offset X limit Y;


文章比较长,希望大家有收获。

思路比结论更重要。

本文转自“架构师之路”公众号,58沈剑提供。

目录
相关文章
|
缓存 NoSQL Java
【JetCache】JetCache的使用方法与步骤
【JetCache】JetCache的使用方法与步骤
7748 1
|
SQL 存储 关系型数据库
解析MySQL Binlog:从零开始的入门指南【binlog入门指南】
解析MySQL Binlog:从零开始的入门指南【binlog入门指南】
13845 0
|
SQL 存储 Oracle
6 张图带你彻底搞懂分布式事务 XA 模式
XA 协议是由 X/Open 组织提出的分布式事务处理规范,主要定义了事务管理器 TM 和局部资源管理器 RM 之间的接口。目前主流的数据库,比如 oracle、DB2 都是支持 XA 协议的。
14157 1
6 张图带你彻底搞懂分布式事务 XA 模式
|
前端开发 网络协议 Dubbo
超详细Netty入门,看这篇就够了!
本文主要讲述Netty框架的一些特性以及重要组件,希望看完之后能对Netty框架有一个比较直观的感受,希望能帮助读者快速入门Netty,减少一些弯路。
93449 33
超详细Netty入门,看这篇就够了!
|
9月前
|
SQL 存储 关系型数据库
简单聊聊MySQL的三大日志(Redo Log、Binlog和Undo Log)各有什么区别
在MySQL数据库管理中,理解Redo Log(重做日志)、Binlog(二进制日志)和Undo Log(回滚日志)至关重要。Redo Log确保数据持久性和崩溃恢复;Binlog用于主从复制和数据恢复,记录逻辑操作;Undo Log支持事务的原子性和隔离性,实现回滚与MVCC。三者协同工作,保障事务ACID特性。文章还详细解析了日志写入流程及可能的异常情况,帮助深入理解数据库日志机制。
1120 0
|
12月前
|
人工智能 前端开发 Java
Spring AI Alibaba + 通义千问,开发AI应用如此简单!!!
本文介绍了如何使用Spring AI Alibaba开发一个简单的AI对话应用。通过引入`spring-ai-alibaba-starter`依赖和配置API密钥,结合Spring Boot项目,只需几行代码即可实现与AI模型的交互。具体步骤包括创建Spring Boot项目、编写Controller处理对话请求以及前端页面展示对话内容。此外,文章还介绍了如何通过添加对话记忆功能,使AI能够理解上下文并进行连贯对话。最后,总结了Spring AI为Java开发者带来的便利,简化了AI应用的开发流程。
9130 2
Spring AI Alibaba + 通义千问,开发AI应用如此简单!!!
|
消息中间件 存储 SQL
代码很少,却很优秀!RocketMQ的NameServer做到了!
本文深入剖析了RocketMQ的注册中心NameServer,基于RocketMQ release-5.2.0版本。NameServer作为Broker、Producer与Consumer之间的纽带,仅由少数几个类构成,却实现了高性能与轻量化。文章详细介绍了NameServer的AP设计思想、简洁的数据结构及心跳机制。AP设计避免了复杂的分布式协议,简化了网络开销;数据结构主要包括路由表、Broker信息等;心跳机制则通过定时扫描确保Broker的活跃状态。通过这些核心设计,NameServer实现了高效稳定的注册与发现功能。
614 5
|
Java UED Spring
Springboot通过SSE实现实时消息返回
通过Spring Boot实现SSE,可以简单高效地将实时消息推送给客户端。虽然SSE有其限制,但对于许多实时消息推送场景而言,它提供了一种简洁而强大的解决方案。在实际开发中,根据具体需求选择合适的技术,可以提高系统的性能和用户体验。希望本文能帮助你深入理解Spring Boot中SSE的实现和应用。
5941 1
|
消息中间件 canal 缓存
Redis与MySQL双写一致性如何保证:延迟双删?binlog异步删除?
Redis与MySQL双写一致性如何保证:延迟双删?binlog异步删除?
|
SQL druid Java
解决 ‘The last packet successfully received from the server was xxx milliseconds ago‘ 问题
解决 ‘The last packet successfully received from the server was xxx milliseconds ago‘ 问题
7532 0

热门文章

最新文章