随着数据量越来越大,架构不得不分库分表来提高系统的读写速度,但是这种架构也带来了很多问题。这篇文章会讲跨库/表的分页查询的解决方案。架构背景笔者曾在大型电子商务系统中做过订单服务。企业早期,业务量很小,单库单表基本可以搞定。但是随着时间的推移,数据量越来越大,订单服务的读写性能逐渐变差,架构团队也尝试了各种优化方案,比如冷热分离、查询分离的方案前面介绍过。虽然有一些性能提升,但是在每天百万级数据增长的情况下,还是杯水车薪。最后经过架构组的讨论,选定了分库分表;如何拆分,如何选择shardkey等细节不是本文的重点,这里不再赘述。在分库分表之前,我们来拆解一下业务需求:C端用户需要查询自己所有的订单后台管理员,客服需要查询订单信息(根据订单号查询,用户信息...)查询自己店铺的订单信息。根据以上三个要求,判断优先级。当然首先要满足C端用户的业务场景,所以最终选择uid作为shardingKey。当然,选择uid作为shardingKey只满足C端用户的业务场景。,后台和C端用户的业务场景怎么做?很简单,你只需要在ES或HBase中存储一份数据异构性即可,比较简单,不再赘述。假设订单表按照hash(uid%2+1)拆分成两张表,如下图:假设现在需要根据订单的时间(shardingKey这里不讨论路由,直接进行全表扫描),在单表中的SQL如下:select*fromt_orderorderbytimeasclimit5,5;这个SQL很好理解,就是翻页查询第二页的数据,每页查询5条数据,其中offest=5假设t_order_1和t_order_2中的数据如下:上面20条数据从小到大排序如下:t_order_1中对应排序如下:t_order_2中对应排序如下:那么单表结构下的最终结果只需要查询一次,结果如下:分表的结构如何分页查询?下面介绍几种解决方案1、在全局查询方法中拆分数据后,如果上面的语句还是一样,则直接在两个表中执行,变成如下两条SQL:select*fromt_order_1orderbytimeasc限制5,5;select*fromt_order_2orderbytimeasclimit5,5;获取到的数据会在内存中重新排序,那么最后的结果如下:可见上面的结果肯定是错误的。所以正确的SQL改写如下:select*fromt_order_1orderbytimeasclimit0,10;select*fromt_order_2orderbytimeasclimit0,10;全部查询出来,然后在内存中重新排序,最后取出第二页的数据。这是全局查询方法。这种方案的缺点非常明显:随着页数的增加,每个节点返回的数据也会增加,性能非常高。低服务层需要进行二次排序,增加了服务层的计算量。如果数据太大,对内存和CPU的要求也很高。但是针对这个方案有很多优化方式,比如Sharding-JDBC。方案进行了优化,采用流处理+归并排序的方式,避免内存占用过多。有兴趣的可以自行了解。2、禁止跳页查询方式当数据量较大时,可以禁止跳页查询,只提供下一页的查询方式,如APP或小程序中的下拉翻页。这是一种业务折衷方案,但是,它可以大大降低业务复杂度。例如,第一页的排序数据如下:查询第二页时,可以将上一页的最大值1664088392作为查询条件。此时将两张表中的SQL改写如下:select*fromt_order_1wheretime>1664088392orderbytimeasclimit5;select*fromt_order_2time>1664088392orderbytimeasclimit5;然后还需要在内存中重新重新排序,最后取出前5条数据不过这样的好处是不需要把前两页的数据全部返回。只需要返回一页数据,页数大的时候也是一样的。性能提升非常大。这种方案的缺点也很明显:不能跳页查询,只能逐页查询,比如从第一页直接跳到第五页,因为无法获取到第四页的最大值,所以这种页面跳转查询肯定是不行的。3.二次查询方式以上两种方案或多或少都有缺点。下面介绍二次查询的方法。这个方案可以同时满足性能需求和业务需求,但是相对于前面两个方案来说,理解起来比较困难。还是上面的SQL:select*fromt_orderorderbytimeasclimit5,5;1.SQL改写第一步需要改写上面的SQL:select*fromt_orderorderbytimeasclimit2,5;注意:原来的SQLoffset=5称为全局偏移量。由于拆分成两张表,改写后的offset=globaloffset/2=5/2=2最终落入各表的SQL如下:select*fromt_order_1orderbytimeasclimit2,5;select*fromt_order_2orderbytimeasclimit2,5;执行后结果如下:下图中红色部分为最终结果:2.返回数据的最小值t_order_1:5条数据中的最小值是:1664088479t_order_2:5条数据中的最小值条数据为:1664088392那么两张表中的最小值为1664088392,记录为time_min,从表t_order_2中,这个过程只需要比较每个分库的第一项数据,时间复杂度很低3.二次查询改写二次SQL改写也很简单,使用between语句,起点为第2步返回的最小值time_min,终点为各表中第一个查询的最大值。对于t_order_1表,第一次查询时最大值为1664088581,改写SQL后:select*fromt_order_1wheretimebetween$time_minand1664088581orderbytimeasc;对于表t_order_2,第一次查询时的最大值如果是1664088481,则改写SQL:select*fromt_order_2wheretimebetween$time_minand1664088481orderbytimeasc;此时查询的结果如下(红色部分):上面的例子只是由于数据重合导致的第2步和第3步的结果相同。实际上,第3步的结果通常会返回比第2步的结果更多的数据。4.在每个结果集中虚拟出一条time_min记录,求time_min的全局偏移量在每个结果集中虚拟出一条time_min记录,求出time_min的全局偏移量,下图中蓝色部分为虚拟time_min,红色部分为第二步的查询结果集是因为第一步修改的SQL的偏移量为2,所以查询结果集中每个子表的第一个数据偏移量为3(2+1);t_order_1中的第一个数据是1664088479,这里的offset是3,然后往上找虚拟的time_min,那么offset=2t_order_2中的第一个数据是time_min,然后是offset=3,那么就是此时time_min的全局偏移量=2+3=55。找到final数据找到time_min的finalglobaloffset=5后,就可以知道排序后的数据了。将步骤2得到的两个结果集在内存中重新排序后,结果如下:现在time_min是offset=5of1664088392,那么原来的SQL:select*fromt_orderorderbytimeasclimit5,5;结果很明显,如果向后移动一位,结果是:正好匹配上一个结果,说明第二种查询方案没有问题。这种方案的优点:可以准确的返回业务需要的数据,而且每次返回的数据量很小,不会随着翻页增加返回的数据量。缺点也很明显:需要两次查询。总结本文介绍分库分表后分页查询的三种方案:全局查询方式:这种方案最简单,但是随着页码的增加,性能越来越低。禁止跳页查询方式:业务上改了这个方案,不允许跳页查询。由于只返回一页数据,性能更高。二级查询方式:dataAccurate,适用于数据分布均衡的情况,查询的数据较少,返回的数据量不会随着翻页而增加,性能更高
