1. 分库分表下的分页查询
业务数据达到一定数据量时,必定会引入数据库分片,但当对于分片的情况下,分页查询是如何做到的?
比如: 数据库db1,中有三个user表,user_0,user_1,user_2,三个表的分片策略是以userId 与 3 取余。分片配置入下
# 指定user表的数据分布情况
spring.shardingsphere.sharding.tables.user.actual-data-nodes = db1.user_$->{0..2}
# 指定user表的分片策略,分片策略包括分片键和分片算法
spring.shardingsphere.sharding.tables.user.table-strategy.inline.sharding-column = id
spring.shardingsphere.sharding.tables.user.table-strategy.inline.algorithm-expression = user_$->{id % 3}
现在执行分页查询语句
select * from `user` order by id limit 10000,10;
考虑到三个表的分片策略,如果要正确获取到 10000-100010 区间的数据的话,需要将三个表都查询出100010 条数据,再做归并排序,最后才能得到正确的结果集。
这样便产生了最让人担忧的问题,如果没张表都要查询那么多数据在我内存中排序,如果我有上百张表,那内存迟早溢出,产生OOM?
2. ShardingJDBC 的优化
官方文档在这里:shardingsphere
ShardingSphere 对分片查询的优化是通过使用 流式归并+优先级队列 实现的,可以减少对本地内存的占用(只有每个数据源的游标而已),但是并不能减少带宽资源的消耗。
ShardingJDBC 对这种分片查询的处理方式为:
- 从各个数据节点获取对应的数据集。
- 将上述获取的数据集,进行组合、归并, 最后得到一个符合预期的数据集。
- 将正确的数据集返回。
上述三个步骤中最重要的就是 归并的策略…
ShardingJDBC 的归并由归并引擎负责,归并引擎提供了三种归并方式
流式归并、内存归并和装饰者归并
-
流式归并是指每一次从结果集中获取到的数据,都能够通过逐条获取的方式返回正确的单条数据,它与数据库原生的返回结果集的方式最为契合。遍历、排序以及流式分组都属于流式归并的一种。
-
内存归并则是需要将结果集的所有数据都遍历并存储在内存中,再通过统一的分组、排序以及聚合等计算之后,再将其封装成为逐条访问的数据结果集返回。
-
装饰者归并是对所有的结果集归并进行统一的功能增强,目前装饰者归并有分页归并和聚合归并这2种类型。
因为流式归并是从数据库中返回的结果集是逐条返回的,并不需要将所有的数据一次性加载至内存中,因此,在进行结果归并时,沿用数据库返回结果集的方式进行归并,能够极大减少内存的消耗,是归并方式的优先选择。
3. 流式归并的原理
ShardingJDBC 的流式处理和JDBC的ResultSet的原理是一样的,主要是通过和数据库保持长连接,每次next都只取当前游标所在位置的一条数据,然后在内存中进行归并。
具体流程如下:
假设user表分为 db0: user_0, db1: user_1, db2: user_2 三张表
查询语句为:
select * from user order by id limit 10,10;
- 因不确定数据分布关系,所以,下发到三个表的sql指令都为
select * from user_$ order by id limit 0,20;
-
数据源执行了sql后,并不会将查询到的数据集直接返回给客户端,而是先将结果集存储在数据源本地,等待client通过游标一条条读取。每一个表都会维护一个自己表的游标,初始位置为第一条记录。
-
每一轮都只传输游标当前指向的记录,client会将接收到的记录加入优先级队列,第一轮的时候client 维护的优先级队列如下所示。优先级队列是按照sql要求的排序字段排序。
-
优先级队列队首出队,会执行next,去对应的db中取下一条记录,此时数据源维护的游标要向下移动一格。上述例子中,便会去user_1 中取出下一条记录,再重新入队进行排序,第二轮的结果如下图所示。
- 优先级队列操作next的同时,内部维护了一个 rowNumber,用来表示当前记录是第几个,每次取next时,都会 +1 ,源码部分如下:
limit分页的时候,便是通过这个字段找到对应开始的记录,开始拼接有效的结果集。