跨库分页

单库的情况下,分页查询可以很容易通过MySQL提供的limit关键字来实现。

假如每页100条记录,我们要查询第2页的数据,则可以使用如下SQL来实现:

# 按时间排序,取从1条记录开始(包含)的100条记录
SELECT uid, name FROM user ORDER BY time LIMIT 0, 100
# 或者
SELECT uid, name FROM user ORDER BY time LIMIT 100 OFFSET 100

但是在将表进行水平拆分后,拆分后的表在不同的库中,此时,假如还是取按时间排序的第2页记录,由于此时对每个库而言,时间都不具备全局性,因此,就无法使用上述语句来完成。

2针对这种“跨越多个水平切分数据库,且分库依据与排序依据为不同字段,需要进行分页”的情况,一般有如下解决方案。

假设user(uid, name)按照uid列进行Hash取模拆分为user_1(uid, name)和user_2(uid, name)。

全局视野法

我们想要的第2页的数据,可能全部在user_1中,可能在user_1和user_2上各一部分。如果我们在每个库中,都将前2页的数据返回,然后将4页数据在应用程序中进行排序,然后再取第2页数据,就一定是准确的结果,这就是全局视野法。

该方案的步骤如下(要获取第N页,则每个分库都取前N页的所有数据,再在程序中排序):

(1)将order by time offset X limit Y,改写成order by time offset 0 limit X+Y

(2)服务层将改写后的SQL语句发往各个分库:即例子中的各取2页数据

(3)假设共分为N个库,服务层将得到N*(X+Y)条数据:即例子中的4页数据

(4)服务层对得到的N*(X+Y)条数据进行排序,排序后再取偏移量X后的Y条记录,就是全局视野所需的一页数据

方案优点:业务无损,精准返回所需数据。

方案缺点:

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

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

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

业务折衷法

业务折衷一:禁止跳页查询,只提供“下一页”的功能

对于第1页数据:每个分库order by time where time>0 limit 100的规则去取,然后再在程序中排序,返回真正的第1页数据。

对于第2页数据:根据第1页的time的最大值(假设为time_max),按照order by time where time >time_max limit 100去查询,每个分库返回1页数据,然后再在程序中排序,返回真正的第2页数据。

对于第n页数据:根据第n-1页的time的最大值(假设为time_max),按照order by time where time > time_max limit 100去查询,每个分库返回1页数据,然后再在程序中排序,返回真正的第2页数据。

相比全局视野法,每次分页查询时,每个分库只需要返回1页的数据量。

业务折衷二:允许数据精度损失

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

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

例如,在uid随机的情况下,使用uid取模分两库,user_1和user_2:

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

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

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

利用这一原理,user_1和user_2只需要分别取50条数据,就可以得到非精确的近似数据。此时,每次分页查询时,每个分库只需要返回0.5页的数据量即可。根据实际业务经验,用户都要查询第100页网页、帖子、邮件的数据了,这一页数据的精准性损失,业务上往往是可以接受的,但此时技术方案的复杂度便大大降低了,既不需要返回更多的数据,也不需要进行服务内存排序了。

二次查询法

原理:对于一个有序序列分成 M 个长度不等的有序子序列,M个有序子序列中每个有序子序列前X个元素中的最大值集中起来,再取其中最小值,则该最小值一定小于等于原来有序序列的第 M*X 个元素值。 假设该最小值大于原序列的第 M_X 个元素值,那么M个有序子序列后面第X+个元素值都大于原序列的第 M_X 个元素值,即构成原序列前M_X 个元素只能是M个有序子序列的前X-个元素,因为M_X- < M*X,所以假设不成立。

步骤:(其中,X为原始偏移量,M为分库数量, Y为原始每页大小)

  1. 改写分库sql为:order by time offset ceil(X/M) limit Y

  2. 获取所有分库sql中返回的最小time中的最小time,即time_min

  3. 改写分库sql为:select * from T order by time between time_min and 各自分库的最大time(从第1步中得到)

  4. 计算全局_offset :第3步的各个分库的返回结果比第一步多,当然time_min的那个分库的返回结果肯定不变(所以time_min的那个分库的sql在实现时可以不用执行)。假设所有分库总共多出 K 条数据,则全局_offset = ceil(X/M) * M - K 。

  5. 合并并排序第3步返回的结果集,合并后的第一条数据就是time_min的那条,它的全局_offset由第四步已经得到;然后,其余数据依次设置递增的offset;最后直接在该结果集的中从第(原始sql的offset - _offset + 2)条数据(包含)开始获取Y条数据。

优点:该策略的性能是几乎恒定

缺点:两次查询,需要内存排序

以上面的举例,user(uid, name)拆分为user_1(uid, name)和user_2(uid, name),现在,每页100条记录,要查询第2页的数据。

第1步:

user_1中查询:SELECT uid, name FROM user_1 OFFSET 50 LIMIT 100,假设返回100条记录S11

user_2中查询:SELECT uid, name FROM user_2 OFFSET 50 LIMIT 100,假设返回100条记录S21

第2步:

从S11和S21中,找到time中的最小time的记录,记为R,该最小时间记为time_min;

记user_1的结果集中time的最大值为time_max1

记user_2的结果集中time的最大值为time_max2

第3步:

在user_1中查询:SELECT uid, name FROM user_1 BETWEEN time_min AND time_max1,假设返回102条记录S12

在user_2中查询:SELECT uid, name FROM user_2 BETWEEN time_min AND time_max2,假设返回110条记录S22

第4步:

计算R的全局偏移量为:ceil(原始偏移量/分库数量) - (S12+S22-S11-S21) = 50*2 - (102+110-100-100)= 88。

第5步:

将S12与S22在程序中进行排序(N个有序子序列合并为一个有序序列),从第14(100-88+2)条记录开始(包含第14条),取100条记录,即为最终结果。

参考

分布式 - 跨库分页

业界难题-“跨库分页”的四种方案

沈剑:数据库切分技术实践解析

Last updated