怎么在同一页中分页_分库分表业界难题,跨库分页的几种常见方案

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

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

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

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

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

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

(1)有个业务主键id, msg_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)又能够保证每个库的请求分布是均匀的;

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

一个更具体的例子:

7e32e8db158cd1a934daf7b14dc6c426.png

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

(1)分库依据patition key是uid;

(2)分库算法是uid取模:uid%2余0的数据会落到db0,uid%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,是后文将要讨论的技术问题。

方案一:全局视野法

5174979dadd122e959aa96512fd64c06.png

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

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

需要分三种情况讨论。

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

8f0aa285afc7fc513678d45d6cabc4aa.png

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

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

2a1b087773ce83f1ebb5a422d21f65cb.png

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

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

3cfcb8c8914e8175eedf875c61132bb1.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页数据,数据量和排序量都将大增,性能平方级下降。

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

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

方案二:禁止跳页查询法

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

4766fe130dd7f746c8342ab722108135.png

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

(1)将查询

order by time offset 0 limit 100;

改写成

order by time where time>0 limit 100;

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

1c7d817deca6aad474da709885334073.png

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

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

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

a85c874843ac031526a729016d7c9c12.png

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

(1)将查询

order by time offset 100 limit 100;

改写成

order by time where time>$time_max limit 100;

a95e07e35479d333d4ad2e75cbde8387.png

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

f6ca57b58aee84987099a1452df4d71a.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取模分两库,db0和db1:

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

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

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

d1eb37feda29440ba9c9726ae65b1637.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)如下:

d8ade1fdf4accfa399edfe6a8610e484.png

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

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

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

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

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

d5f31706ca521c73adef8fe4b6f7906e.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)如下:

fe69b3765a2980ed5c1641c8fedda7eb.png

可以看到:

分库一的结果集,由于time_min来自原来的分库一,所以分库一的返回结果集和第一次查询相同(所以其实这次访问是可以省略的);

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

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

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

88b89b7041f096df0286f813e93380eb.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的记录

df3778092a39dc481d5d0f24e240ff1d.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;

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

思路比结论更重要。

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

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

相关文章

content add tpl.php,phpcms后台批量上传添加图片文章方法详解(一)

注:以下所有代码中,红色部分为增加部分。一、在后台增加批量添加按钮打开“phpcms\modules\content\templates\content_list.tpl.php”文件搜索“$category[‘catname‘]));?>”在这句话的后天的添加:a echo"" href":;&q…

sap 供应商表_财务人员学习SAP的路线图

有许多网友在公众号给我们留言,咨询财务人员学习SAP的事情,如何才能快速掌握SAP,有没有捷径什么的。今天就给大家分享一下财务人员学习SAP的经验,希望能够为财务人员揭开SAP神秘的面纱,学习SAP少走弯路。刚接触SAP的财…

nodejs搭配phantomjs highcharts后台生成图表

简单分享一下,后台使用nodejs结合highcharts、phantomjs生成报表图片的方法。这主要应用在日报邮件。 主要参考以下资料: http://www.highcharts.com/component/content/article/2-news/52-serverside-generated-charts#phantom_usagehttps://bitbucket.…

vue 页面切换动画_Flutter Hero动画让你的APP页面切换充满动效 不一样的体验 不一样的细节处理...

优美的应用体验 来自于细节的处理,更源自于码农的自我要求与努力,当然也需要码农年轻灵活的思维。本文章实现的Demo效果,如下图所示:class HeroHomePage extends StatefulWidget { override _TestPageState createState() > …

自定义左右侧滑菜单

实现效果: 左右侧滑菜单,侧滑栏占主屏比为60%监听触控,自定义滑动动画,当侧边栏滑动超过50%松开触控将自动滑动到60%,未超过50%松开触控回归侧边栏隐藏为主屏设置蒙版效果,根据侧滑菜单的占屏比设置主屏蒙版…

ubuntu php7 memcache,linux上安装php7 memcache扩展

php7安装memcache扩展需要memcache php7的分支 否则安装会失败php7的memcache扩展安装,真的很让人心碎!下面则是php7的扩展memcache安装了。用之前的php版本安装是没有问题,但是用了php7安装 http://pecl.php.net/package/memcache 下的任一…

好文推荐系列--------(3)GruntJS 在线重载 提升生产率至新境界

好文原文地址:http://segmentfault.com/a/1190000000354555 本文将首先介绍grunt-markdown插件如何配合HTML模板使用,接着我将介绍如何使用grunt-watch插件将工作效率提升至新层次。如果你不熟悉GruntJS,请先阅读我关于GruntJS的文章。 Githu…

二叉树的创建_大多数人都不会手写创建并遍历二叉树,一航这里帮你终结了

创建二叉树、遍历二叉树、二叉树的最近公共祖先任何疑问、意见、建议请公众号留言或联系qq474356284先序、后序创建二叉树先中后层序遍历二叉树二叉树的最近公共祖先 输入格式:创建二叉树时的输入:如序列:{1 2 -1 -1 3 -1 -1}表示1结点有2,…

zookeeper 密码_阿里资深JAVA架构带你深度剖析dubbo和zookeeper关系

为什么要用dubbo?当网站规模达到了一定的量级的时候,普通的MVC框架已经不能满足我们的需求,于是分布式的服务框架和流动式的架构就凸显出来了。单一应用架构当网站流量很小时,只需一个应用,将所有功能都部署在一起&…

nw.js FrameLess Window下的窗口拖拽与窗口大小控制

nw.js FrameLess Window下的窗口拖拽与窗口大小控制 很多时候,我们觉得系统的Frame框很难看,于是想自定义。 自定义Frame的第一步是在package.config文件中将frame选项设置为false。 { "name": "1", "main": "index.…

linux 文件重命名_如何在 Linux 上重命名一组文件 | Linux 中国

要用单个命令重命名一组文件,请使用 rename 命令。它需要使用正则表达式,并且可以在开始前告诉你会有什么更改。-- Sandra Henry-stocker几十年来,Linux 用户一直使用 mv 命令重命名文件。它很简单,并且能做到你要做的。但有时你需…

oracle插入性能优化,Oracle-insert性能优化

看见朋友导入数据,花了很长时间都没完成!其实有很多快速的方法,整理下! 向表中插入数据有很多办法,但是方法不同,性能差别很看见朋友导入数据,,花了很长时间都没完成!其实有很多快速…

dba_segments和dba_tables的不同

create table tset as select * from dba_objects; select count(*) from tset; select table_name,blocks,empty_blocks from dba_tables where table_name’TSET’; select segment_name,bytes,blocks,extents from dba_segments where segment_name’TSET’; 问题来了&#…

微博air客户端_打磨近十年,接近「完美」的 macOS 第三方微博客户端:Maipo

2020年11月13日,macOS Big Sur正式推送当天,Maipo for 微博也迎来了4.0.0大版本更新。从Weibo for Mac(2011年)、WeiboX(2014年)到Maipo(2017年),跨度近十年,…

输入框联动查询

目的&#xff1a;类似于百度的搜索联动&#xff0c;输入前面的几个字&#xff0c;查询出可能的结果供用户选择&#xff0c;如下&#xff1a; html部分&#xff1a;在“中”这个输入框下面隐藏一个ul属性&#xff0c;例如: <ul class"am-padding-left-0 uhide" id&…

LoadRunner函数

一、基础函数简介 在VU左边导航栏中&#xff0c;有三个LoadRunner框架函数&#xff0c;分别是vuser_init()、Action()、vuser_end()。这三个函数存在于任何Vuser类型的脚本中。 vuser_init:虚拟用户的初始化函数&#xff0c;一般将用户初始化的操作放在这里&#xff0c;如登录操…

TPLink 备份文件bin文件解析

TPLink 路由器备份文件bin文件 测试路由器 WR885&#xff0c;备份文件加密方式DES&#xff0c;密钥&#xff1a;478DA50BF9E3D2CF linux端&#xff1a; openssl enc -d -des-ecb -nopad -K 478DA50BF9E3D2CF -in config.bin python&#xff1a; python默认没有安装crypto需要自…

linux的文件搜索命令,Linux文件搜索命令find的用法 | 术与道的分享

不管在Windows还是Linux中&#xff0c;最重要的问题不是说你能搜索到这个文件&#xff0c;而是最好少用搜索&#xff0c;应该是你在整个服务器的规划里面&#xff0c;把所以的文件目录规划的很好。就像如果你在家里找衣服&#xff0c;如果不是你乱扔&#xff0c;就不可能花费太…

vue v-if判断数组元素的值_Vue项目上线做的一些基本优化

前言本文主要是做一个Vue性能优化的帖子&#xff0c;做一个参考文档&#xff0c;对以后项目上线做一些集合文档。如果对各位在项目优化时&#xff0c;做一个文档参照。开发过程在开发项目的时候&#xff0c;就要注意项目的一些小技巧&#xff0c;下面我就罗列一些经常用到的优化…

BZOJ 4000: [TJOI2015]棋盘( 状压dp + 矩阵快速幂 )

状压dp, 然后转移都是一样的, 矩阵乘法快速幂就行啦. O(logN*2^(3m)) ---------------------------------------------------------------------------------------------#include<cstdio>#include<cstring>#include<algorithm>using namespace std;#define …