挑战分库分表后实现跨分页查询

想象你有一个图书馆(单库单表),所有书按顺序放在书架上。当你要找第100-110本书时,直接数到第100本就能拿到。但图书馆的书爆炸式增长后,馆长决定:

  1. 分库:把书分到10个房间(10个数据库)

  2. 分表 :每个房间再分成20个书架(20张表)

    每个书架只放特定规则的书(比如按ID取模:ID % 200

问题来了

当用户要求「按时间倒序排列,显示第1000-1010条数据」时:

  • 每个房间的书架都是独立排序的

  • 无法直接知道全局第1000条数据在哪里

解决方案:

想象你管理10个快递分拣站(分库分表),每个站点有自己的包裹编号。现在要找出全国第1000-1010个发出的包裹:

  • 笨方法:让所有站点把前1010个包裹发到总部,总部排序后挑出需要的(全局排序法)

  • 聪明方法

    1. 先问每个站点第1010个包裹的发货时间

    2. 找到全国第1010个包裹的时间

    3. 让各站点只发送比这个时间晚的包裹,再次排序(二次查询法)

  • 最懒但有效的方法:把所有包裹信息抄一份到总部的智能黑板(Elasticsearch),随时查黑板即可!

下面具体说下各个方案、优缺点和做法:

方案 适用场景 技术实现难度 缺点
全局排序法 数据量小,分片少 性能差,深分页爆炸
二次查询法 排序字段有索引,分片较多 需要两次查询
游标分页法 有全局有序字段(如ID、时间戳) 不支持随机跳页
搜索引擎辅助 高频复杂查询,容忍秒级延迟 维护成本高,数据延迟

(1)全局排序法(简单但低效)

实现步骤

  1. 每个分片独立执行ORDER BY create_time DESC LIMIT 0, 1010

  2. 把所有分片的结果汇总到一个中心节点

  3. 中心节点对所有数据进行全局排序,再取第1000-1010条

缺点

  • 分片数越多,数据传输量指数级增长

  • 深分页(offset很大)时性能灾难(例如:offset=100万)

(2)二次查询法(优化性能)

核心思路:避免全量数据传输,分两次查询缩小范围

步骤示例(查询第1000-1010条):

  1. 第一次查询 :每个分片返回前1010条数据的最小时间戳

    • 例如分片1的最小时间戳是T1,分片2是T2...
  2. 取所有分片中第1010小的全局时间戳T_global

  3. 第二次查询 :各分片查询create_time >= T_global的数据

  4. 汇总后再次排序取最终结果

优点

  • 大幅减少数据传输量

  • 适合排序字段有索引的情况(如时间戳)

(3)游标分页法(避免深分页)

核心思路:用连续且唯一的字段(如自增ID、时间戳)作为游标

优势

  • 完全避免offset带来的性能问题

  • 天然适合分库分表,每个分片只需按游标条件过滤

限制

  • 必须有一个全局有序且唯一的字段

  • 用户无法直接跳转到任意页码(如第1000页)

(4)搜索引擎辅助(终极武器)

适用场景 :高频复杂分页查询
实现方式

  1. 将分库分表的数据实时同步到Elasticsearch/Solr

  2. 所有分页查询直接走搜索引擎

  3. 搜索引擎内部维护全局排序

优势

  • 完全屏蔽分库分表复杂性

  • 支持复杂条件过滤+高并发查询

代价

  • 需要维护额外系统,数据一致性延迟增加

实际开发中的建议

  1. 优先使用游标分页

    • 如App的瀑布流页面,用户只会不断下滑,不需要跳页
  2. 限制最大分页深度

    • 例如最多允许查看前100页,防止恶意深分页攻击
  3. 结合业务优化

    • 电商按价格排序时,可预先对价格分段(0-100元、100-200元...)
  4. 终极方案

    • 分库分表+Elasticsearch组合,牺牲一定实时性换取高性能查询