MySQL 跨库分页/ 分表分页/ 跨库分页,为什么这么难?

简介: 当业务数据达到一定量级(比如:mysql单表记录量>1千万)后,通常会考虑“分库分表”将数据分散到不同的库或表中,这样可以大大提高读/写性能。但是问题来了,对于 select * from table limit offset , pagesize 这种分页方式,原来一条语句就可以简单搞定的事情会变得很复杂,本文将与大家一起探讨分库分表后"分页"面临的新问题。

当业务数据达到一定量级(比如:mysql单表记录量>1千万)后,通常会考虑“分库分表”将数据分散到不同的库或表中,这样可以大大提高读/写性能。但是问题来了,对于 select * from table limit offset , pagesize 这种分页方式,原来一条语句就可以简单搞定的事情会变得很复杂,本文将与大家一起探讨分库分表后"分页"面临的新问题。


一、分表对分页的影响

比如有一张表,里面有8条记录(为简单起见,假设该表上只有1个自增ID),数学上可以抽象成1个(有序)数列(注:为方便讨论,不加特殊说明的情况下,文本中数列的顺序,均指升序)


(1,2,3,4,5,6,7,8)


如果要取出上面红色标识的2,3这二条记录,limit 1,2 就行了。


现在假如分成2张表(即:原来的数列,拆分成2个非空子数列),一般来讲,有二种常用分法:


1.1 分段法(比如:有时间属性的数据,类似订单这种,可以按下单时间拆分,每个月1张表)


(1,2,3,4)


(5,6,7,8)


沿用之前的limit x,y的思路,每个分表上 limit 1,2,会得到如下2个子数列:


(2,3)


(6,7)


然后在内存中合并排序,再取前2条 (2,3,6,7) => (2,3) ,貌似看上去也符合预期(这个思路也称为归并),但这只是假象。当要取的分页数据落在不同的子数列上时,就能发现问题:


(1,2,3,4,5,6,7,8) 比如,我们要从4个位置开始,连续取2个元素,即: limit 3,2


(1,2,3,4) => limit 3,2 =>(4)


(5,6,7,8) => limit 3,2 =>(8)


最后合并出来的结果是(4,8) 与正确结果 (4,5)相比,显然不对。


1.2 模余均摊法(比如:字段值对2取模求余数,根据余数决定分到哪个表,该方法也简称为取余法)


(1,3,5,7)


(2,4,6,8)


归并排序的思路在分段法上行不通,对于取模均摊同样也不行,仍以 limit 1,2为例,原始序列取出来的结果是(2,3),如果用归并的思路:


(1,3,5,7)=> limit 1,2 =>(3 ,5)


(2,4,6,8)=> limit 1,2 =>(4, 6)


内存合并排序后,取前2个,最终结果为(3 , 4)


结论:不管分库分表采用什么分法,简单归并的思路,都无法正确解决分页问题。


二、全局法(limit x+y)

反思一下刚才的归并思路,本质上我们在每个子数列(即:分表)上limit x,y 时,取出来的数据就有可能已经产生缺失了。网上有一篇广为流转的文章"业界难题-跨库分页”,作者在文中提出了一个方案:把范围扩大,分表sql上的limit x,y 变成 limit 0, x+y ,这样改写后,相当于分表中把"每页最后一条数据"之前的所有数据全都取出来了(当然:这里面可能会有不需要的多余数据),然后内存中合并在一起,再取x偏移量后的y条数据。


用前面的例子验证一下:


原序列:(1,2,3,4,5,6,7,8),需要取出limit 1,2 ,即:(2,3)


2.1 按分段法拆成2段:


(1 , 2 , 3 , 4) => limit 1,2 =>改写成 limit 0, 1+2 => (1,2,3)


(5 , 6 , 7 , 8) => limit 1,2 =>改写成 limit 0, 1+2 => (5,6,7)


将子数列合并排序=> { 1,2,3,5,6,7} => 按原始偏移量 limit 1,2 =>{2,3} 正确


如果原数列中要取的数据,正好落在2个子数列上(1,2,3,4,5,6,7,8),需要取出limit 3,2 ,即:(4,5)


(1 , 2 , 3 , 4) => limit 3,2 =>改写成 limit 0, 3+2 => (1,2,3,4)


(5 , 6 , 7 , 8) => limit 3,2 =>改写成 limit 0, 3+2 => (5,6,7,8)


将子数列合并排序=> (1,2,3,4,5,6,7,8) => 按原始偏移量 limit 3,2 => (4,5) 也符合预期。


2.2 取模均摊拆成2段


(1,3,5,7) => limit 1,2 ->改写成 limit 0, 1+2 => (1,3 ,5)


(2,4,6,8) => limit 1,2 ->改写成 limit 0, 1+2=> (2,4,6)


将子序列合并=> (1,2,3,4,5,6) => 按原始偏移量 limit 1,2 =>(2,3) 正确


该方法缺点也很明显:取出的记录太多了,比如 limit 10000000,10 -> 改写后变成 limit 0, 10000010 遇到海量数据,mysql中查询有可能直接超时,这么多数据从db传到应用层,网络开销也很大,更不用说如果是java应用,大量数据放到List或Map中,容易出现OOM。(注:一般情况下,需要用分库分表的场景,数据量必然很大,所以这个方法,实际中基本上没法用)


另外,MySQL 系列面试题和答案全部整理好了,微信搜索Java技术栈,在后台发送:面试,可以在线阅读。


三、二次查询法

这也是"业界难题-跨库分页”一文中提到的一个方法,大致思路如下:在某1页的数据均摊到各分表的前提下(注:这个前提很重要,也就是说不会有一个分表的数据特别多或特别少),换句话说:这个方案不适用分段法,按如下步骤操作:


1)原sql中的limit offset,pagesize 改写成 limit offset/n ,pagesize (注:n为分表个数,如果offset/n除不尽,向下取整,避免最后的结果丢数据)-- 这个的意思,其实就是假设原表这一页的数据,会均分到各个分表(所以,我一再强调,前提是数据是均摊的,如果某个分表的记录很少,极端情况下,甚至是空的,这个就不对了,最终结果会少数据)


2)分表上,执行改写后的sql,得到一堆结果集,然后找出这堆结果中的最小id (假设id是关键的排序字段),记为min_id -- 这一步的目的,是为了找出最小的起始点,保证第1页数据起点正确。


3)各分表上的sql,where条件部分改写成 id between min_id and origin_max_id (注:origin_max_id为上一步,每个分表查询结果集中的最大值,显然min_id=自身最小id的那张分表,不用再重复查询) -- 这一步的目的在于,因为步骤1)查出来的结果,通常会比原表上该页的数据少,所以这里重新将起始点设置到正确的位置,即:min_id,再查1次,相当于范围扩大了,以保证数据不会丢。不过,这里有一个可优化的地方,仔细想想,这1次查询出来的结果,跟步骤1)中的查出来的结果,必然有一部分是重复的,因此改写部分,只需要 id between min_id and origin_min_id就可以了(origin_min_id 即为原来分表结果上的最小id)


4)将上一步查询出来的结果,在内存中合并排序去重(注:如果上一步采用了优化方案,就应该是把1)与3)这二次查询的结果全取出来合并排序去重),然后从开始连续取pagesize条数据即可(注:offset/n除不尽的话,向下取整了,也就是起始点可能向前多移了,所以有可能开始的第1条记录,其实是上1页的最后1条记录,要追求精确的话,可以在应用层记录上一页最后1条记录的id,然后跟本次查询结果前1条记录对比,如果发现是一样的,开始取数据的位置,就要向后移1位,如果考虑id有重复的话,就要根据情况多移几位)


验证一下看看效果:


场景1(前提:取余法)


原序列:(1,2,3,4,5,6,7,8),需要取出limit 2,2 ,即:(3,4)


第1次查询


(1,3,5,7) -> limit 2,2 -> 改写成 limit 1,2 -> (3,5)


(2,4,6,8) -> limit 2,2 -> 改写成 limit 1,2 -> (4,6)


最小值为3


第2次查询


(1,3,5,7) -> between 3 and 5 -> (3,5)


(2,4,6,8) -> between 3 and 6 -> (4,6)


将第2次查询的结果合并:


(3,4,5,6) ->取头开始,取pageSize即2个元素 -> (3,4) 正确


-----------------------------------------------------


场景2(前提:取余法)


原序列:(1,2,3,4,5,6,7,8),需要取出limit 1,2 ,即:(2,3)


第1次查询


(1,3,5,7) -> limit 1,2 -> 改写成 limit 0,2 -> (1,3) --注:因为1/2除不尽,这里向下取整了


(2,4,6,8) -> limit 1,2 -> 改写成 limit 0,2 -> (2,4)


最小值为1


第2次查询


(1,3,5,7) -> between 1 and 3 -> (1,3)


(2,4,6,8) -> between 1 and 4 -> (2,4)


将上面的结果合并:


(1,2,3,4) -> (2,3) (注:起始点,第1次查询改写时,向下取整了,所以这里要向移1位,从第2个数字开始取pagesize条数据)


--------------------------------------------------------


场景3(前提:分段法)


为什么说分段法,这个方案不适用,可以看下面的分析:


原序列:(1,2,3,4,5,6,7,8),需要取出limit 2,2 ,即:(3,4)


第1次查询


(1,2,3,4) -> limit 2,2 -> limit 1,2 -> {2,3} --注:这里就已经把正确的数据给丢掉了


(5,6,7,8) -> limit 2,2 -> limit 1,2 -> {5,6} --注:这一段里根本就没有这一页的数据


最小值2


第2次查询


(1,2,3,4) -> between 2 and 3 -> {2,3}


(5,6,7,8) -> between 2 and 6 -> {5,6}


(2,3,5,6) -> (2,3) 这个跟预期结果就对不上了。


-------------------------------------------------------


场景4(前提:取余法)


取余法的前提下,如果某个分表的数据,被清掉一部分,也就是某个分表数据偏少,会发生什么?


比如下面这个,按奇数、偶数分成2个子序列,但是偶数故意删除了几个(相当于现实业务中,这个分表上的数据被干掉了一部分)


原序列:(1,3,5,6,7,8,9,11),需要取出limit 2,2 ,即:(5,6)


第1次查询


(1,3,5,7,9,11) -> limit 2,2 -> 改写成limit 1,2 -> (3,5)


(6,8) -> limit 2,2 -> 改写成limit 1,2 -> (8)


第2次查询


(1,3,5,7,9,11) -> between 3 and 5 -> (3,5)


(6,8) -> between 3 and 8 -> (6,8)


合并后


(3,5,6,8) -> (3,5) 这跟预期结果也对不上。


四、禁止跳页

相当于只允许向上或向下翻页,原理很简单,比如:下一页,先记录上一页的最大id,然后下一页时,只需要 where id > 上一页最大id limit pagesize, 每次只翻1页。显然,这是一个牺牲用户体验的做法。


结论:分表分页不存在一个通用的解决方案,要么性能有问题(比如:全局法 limit x+y),要么必须具备一定的前提条件(比如:二次查询),或者产品设计上牺牲用户体验,仍然是一个业内难题。


参考文章:


https://juejin.im/post/5d1f52e46fb9a07eb3099bbf


https://shardingsphere.apache.org/document/current/cn/features/sharding/use-norms/pagination/


https://stackoverflow.com/questions/3927537/how-do-you-implement-sorting-and-paging-on-distributed-data


http://kmiku7.github.io/2019/08/01/Do-Pagination-With-Table-Database-Sharding/


https://segmentfault.com/a/1190000013225860


https://mp.weixin.qq.com/s/h99sXP4mvVFsJw6Oh3aU5A


本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。


相关实践学习
每个IT人都想学的“Web应用上云经典架构”实战
本实验从Web应用上云这个最基本的、最普遍的需求出发,帮助IT从业者们通过“阿里云Web应用上云解决方案”,了解一个企业级Web应用上云的常见架构,了解如何构建一个高可用、可扩展的企业级应用架构。
MySQL数据库入门学习
本课程通过最流行的开源数据库MySQL带你了解数据库的世界。   相关的阿里云产品:云数据库RDS MySQL 版 阿里云关系型数据库RDS(Relational Database Service)是一种稳定可靠、可弹性伸缩的在线数据库服务,提供容灾、备份、恢复、迁移等方面的全套解决方案,彻底解决数据库运维的烦恼。 了解产品详情: https://www.aliyun.com/product/rds/mysql 
相关文章
|
2月前
|
关系型数据库 MySQL Linux
MySQL包安装 -- SUSE系列(SUSE资源库安装MySQL)
本文介绍了在openSUSE系统上通过SUSE资源库安装MySQL 8.0和8.4版本的完整步骤,包括配置国内镜像源、安装MySQL服务、启动并验证运行状态,以及修改初始密码等操作,适用于希望在SUSE系列系统中快速部署MySQL的用户。
226 3
MySQL包安装 -- SUSE系列(SUSE资源库安装MySQL)
|
2月前
|
运维 Ubuntu 关系型数据库
MySQL包安装 -- Debian系列(Apt资源库安装MySQL)
本文介绍了在Debian系列系统(如Ubuntu、Debian 11/12)中通过APT仓库安装MySQL 8.0和8.4版本的完整步骤,涵盖添加官方源、配置国内镜像、安装服务及初始化设置,并验证运行状态,适用于各类Linux运维场景。
842 0
MySQL包安装 -- Debian系列(Apt资源库安装MySQL)
|
2月前
|
存储 关系型数据库 MySQL
MySQL介绍和MySQL包安装 -- RHEL系列(Yum资源库安装MySQL)
MySQL是一款开源关系型数据库,高性能、易用、跨平台,支持多种存储引擎,广泛应用于Web开发、企业级应用等领域。本教程介绍其特点、架构及在主流Linux系统中的安装配置方法。
579 0
MySQL介绍和MySQL包安装 -- RHEL系列(Yum资源库安装MySQL)
|
11月前
|
SQL 缓存 关系型数据库
如何解决MySQL 的深度分页问题?
在构建高性能Web应用程序时,数据库查询性能至关重要。本文深入探讨了MySQL中`LIMIT ... OFFSET ...`语法的性能瓶颈,并介绍了一种更高效的分页方法——游标分页(Cursor Pagination)。通过记录每页最后一个记录的唯一标识,游标分页能显著提升查询效率,将时间复杂度从O(n + m)降低到O(log n + m),特别适用于大规模数据的分页查询场景。此外,文章还介绍了其他优化方法,如覆盖索引分页、分区表、缓存和基于时间戳的分页,并提供了实践中的最佳建议,帮助开发者选择最适合的分页策略,提升系统性能和用户体验。
689 9
|
7月前
|
存储 SQL 关系型数据库
京东面试:mysql深度分页 严重影响性能?根本原因是什么?如何优化?
京东面试:mysql深度分页 严重影响性能?根本原因是什么?如何优化?
京东面试:mysql深度分页 严重影响性能?根本原因是什么?如何优化?
|
SQL 监控 关系型数据库
MySQL 延迟从库介绍
本文介绍了MySQL中的延迟从库功能,详细解释了其工作原理及配置方法。延迟从库允许从库在主库执行完数据变更后延迟一段时间再同步,主要用于快速恢复误操作的数据。此外,它还可用于备份、离线查询及数据合规性需求。通过合理配置,可显著提升数据库系统的稳定性和可靠性。
444 4
|
SQL 关系型数据库 MySQL
MySQL操作利器——mysql-connector-python库详解
MySQL操作利器——mysql-connector-python库详解
2480 0
|
10月前
|
监控 关系型数据库 MySQL
MySQL和SQLSugar百万条数据查询分页优化
在面对百万条数据的查询时,优化MySQL和SQLSugar的分页性能是非常重要的。通过合理使用索引、调整查询语句、使用缓存以及采用高效的分页策略,可以显著提高查询效率。本文介绍的技巧和方法,可以为开发人员在数据处理和查询优化中提供有效的指导,提升系统的性能和用户体验。掌握这些技巧后,您可以在处理海量数据时更加游刃有余。
921 9
|
关系型数据库 MySQL PHP
php实现一个简单的MySQL分页
通过本文的详细步骤和代码示例,我们实现了一个简单的PHP MySQL分页功能。主要步骤包括计算总记录数、设置分页参数、查询当前页的数据以及生成分页链接。这种分页方式适用于大多数Web应用,能够有效提升用户体验和页面响应速度。
294 4
|
SQL 关系型数据库 MySQL
mysql分页读取数据重复问题
在服务端开发中,与MySQL数据库进行数据交互时,常因数据量大、网络延迟等因素需分页读取数据。文章介绍了使用`limit`和`offset`参数实现分页的方法,并针对分页过程中可能出现的数据重复问题进行了详细分析,提出了利用时间戳或确保排序规则绝对性等解决方案。
502 1

推荐镜像

更多