跨库分页
单库的情况下,分页查询可以很容易通过MySQL提供的limit关键字来实现。
假如每页100条记录,我们要查询第2页的数据,则可以使用如下SQL来实现:
但是在将表进行水平拆分后,拆分后的表在不同的库中,此时,假如还是取按时间排序的第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为原始每页大小)
改写分库sql为:order by time offset ceil(X/M) limit Y
获取所有分库sql中返回的最小time中的最小time,即time_min
改写分库sql为:select * from T order by time between time_min and 各自分库的最大time(从第1步中得到)
计算全局_offset :第3步的各个分库的返回结果比第一步多,当然time_min的那个分库的返回结果肯定不变(所以time_min的那个分库的sql在实现时可以不用执行)。假设所有分库总共多出 K 条数据,则全局_offset = ceil(X/M) * M - K 。
合并并排序第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