热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

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

以下内容来自公众号逆锋起笔,关注每日干货及时送达来源:https:www.cnblogs.comyjmyzzp12149737.html当业务数据达到一

以下内容来自公众号逆锋起笔,关注每日干货及时送达1b249edb335ef573ba0592e92e1919fc.png

来源:https://www.cnblogs.com/yjmyzz/p/12149737.html

当业务数据达到一定量级(比如: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,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)相比,显然不对。

模余均摊法(比如:字段值对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 , 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段

(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。(注:一般情况下,需要用分库分表的场景,数据量必然很大,所以这个方法,实际中基本上没法用)。

| 二次查询法

这也是"业界难题-跨库分页”一文中提到的一个方法,大致思路如下:在某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

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

逆锋起笔专注于程序员圈子,你不但可以学习到javapython等主流技术干货,还可以第一时间获悉最新技术动态内测资格BAT大佬的经验精品视频教程副业赚钱经验,微信搜索readdot关注!

MySQL 8.0 可以操作 JSON 了!!

MySQL 大批量插入,如何过滤掉重复数据?

SQL 中如何给指定数据库创建只读用户?

MySQL 5.7 vs 8.0,哪个性能更牛?

大厂 MySQL 规范,从入门到精通!

9316ab35ee5c84c0b5978fe86472d0c1.png

点赞是最大的支持


推荐阅读
  • 如何实现织梦DedeCms全站伪静态
    本文介绍了如何通过修改织梦DedeCms源代码来实现全站伪静态,以提高管理和SEO效果。全站伪静态可以避免重复URL的问题,同时通过使用mod_rewrite伪静态模块和.htaccess正则表达式,可以更好地适应搜索引擎的需求。文章还提到了一些相关的技术和工具,如Ubuntu、qt编程、tomcat端口、爬虫、php request根目录等。 ... [详细]
  • svnWebUI:一款现代化的svn服务端管理软件
    svnWebUI是一款图形化管理服务端Subversion的配置工具,适用于非程序员使用。它解决了svn用户和权限配置繁琐且不便的问题,提供了现代化的web界面,让svn服务端管理变得轻松。演示地址:http://svn.nginxwebui.cn:6060。 ... [详细]
  • 本文介绍了如何使用PHP向系统日历中添加事件的方法,通过使用PHP技术可以实现自动添加事件的功能,从而实现全局通知系统和迅速记录工具的自动化。同时还提到了系统exchange自带的日历具有同步感的特点,以及使用web技术实现自动添加事件的优势。 ... [详细]
  • EPICS Archiver Appliance存储waveform记录的尝试及资源需求分析
    本文介绍了EPICS Archiver Appliance存储waveform记录的尝试过程,并分析了其所需的资源容量。通过解决错误提示和调整内存大小,成功存储了波形数据。然后,讨论了储存环逐束团信号的意义,以及通过记录多圈的束团信号进行参数分析的可能性。波形数据的存储需求巨大,每天需要近250G,一年需要90T。然而,储存环逐束团信号具有重要意义,可以揭示出每个束团的纵向振荡频率和模式。 ... [详细]
  • 本文讨论了同事工资打听的话题,包括同工不同酬现象、打探工资的途径、为什么打听别人的工资、职业的本质、商业价值与工资的关系,以及如何面对同事工资比自己高的情况和凸显自己的商业价值。故事中的阿巧发现同事的工资比自己高后感到不满,通过与老公、闺蜜交流和搜索相关关键词来寻求解决办法。 ... [详细]
  • 本文是一位90后程序员分享的职业发展经验,从年薪3w到30w的薪资增长过程。文章回顾了自己的青春时光,包括与朋友一起玩DOTA的回忆,并附上了一段纪念DOTA青春的视频链接。作者还提到了一些与程序员相关的名词和团队,如Pis、蛛丝马迹、B神、LGD、EHOME等。通过分享自己的经验,作者希望能够给其他程序员提供一些职业发展的思路和启示。 ... [详细]
  • Android中高级面试必知必会,积累总结
    本文介绍了Android中高级面试的必知必会内容,并总结了相关经验。文章指出,如今的Android市场对开发人员的要求更高,需要更专业的人才。同时,文章还给出了针对Android岗位的职责和要求,并提供了简历突出的建议。 ... [详细]
  • Centos7.6安装Gitlab教程及注意事项
    本文介绍了在Centos7.6系统下安装Gitlab的详细教程,并提供了一些注意事项。教程包括查看系统版本、安装必要的软件包、配置防火墙等步骤。同时,还强调了使用阿里云服务器时的特殊配置需求,以及建议至少4GB的可用RAM来运行GitLab。 ... [详细]
  • 本文介绍了在使用Python中的aiohttp模块模拟服务器时出现的连接失败问题,并提供了相应的解决方法。文章中详细说明了出错的代码以及相关的软件版本和环境信息,同时也提到了相关的警告信息和函数的替代方案。通过阅读本文,读者可以了解到如何解决Python连接服务器失败的问题,并对aiohttp模块有更深入的了解。 ... [详细]
  • 闭包一直是Java社区中争论不断的话题,很多语言都支持闭包这个语言特性,闭包定义了一个依赖于外部环境的自由变量的函数,这个函数能够访问外部环境的变量。本文以JavaScript的一个闭包为例,介绍了闭包的定义和特性。 ... [详细]
  • 2022年的风口:你看不起的行业,真的很挣钱!
    本文介绍了2022年的风口,探讨了一份稳定的副业收入对于普通人增加收入的重要性,以及如何抓住风口来实现赚钱的目标。文章指出,拼命工作并不一定能让人有钱,而是需要顺应时代的方向。 ... [详细]
  • GetWindowLong函数
    今天在看一个代码里头写了GetWindowLong(hwnd,0),我当时就有点费解,靠,上网搜索函数原型说明,死活找不到第 ... [详细]
  • 【MicroServices】【Arduino】装修甲醛检测,ArduinoDart甲醛、PM2.5、温湿度、光照传感器等,数据记录于SD卡,Python数据显示,UI5前台,微服务后台……
    这篇文章介绍了一个基于Arduino的装修甲醛检测项目,使用了ArduinoDart甲醛、PM2.5、温湿度、光照传感器等硬件,并将数据记录于SD卡,使用Python进行数据显示,使用UI5进行前台设计,使用微服务进行后台开发。该项目还在不断更新中,有兴趣的可以关注作者的博客和GitHub。 ... [详细]
  • 微信开放外链的第二阶段:腾讯和阿里巴巴的博弈
    2021年11月30日,微信开始进行“开放外链”的第二阶段,允许在微信个人会话中打开外部链接和在微信群中打开电商链接。虽然这是腾讯和阿里巴巴都能接受的阶段性结果,但双方都不会太满意。接下来几个月,腾讯和阿里将展开复杂的博弈,我们作为外人很难看清全过程。工信部从未要求腾讯无条件开放微信API,本次开放的也只是普通的HTTP链接。 ... [详细]
  • 目前Miniconda3的主要版本已经不支持python3.6,以Windows为例,在官网Miniconda—Condadocumentation中只有python3.7 ... [详细]
author-avatar
mthp
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有