想象你有一个图书馆(单库单表),所有书按顺序放在书架上。当你要找第100-110本书时,直接数到第100本就能拿到。但图书馆的书爆炸式增长后,馆长决定:
-
分库:把书分到10个房间(10个数据库)
-
分表 :每个房间再分成20个书架(20张表)
每个书架只放特定规则的书(比如按ID取模:
ID % 200
)
问题来了 :
当用户要求「按时间倒序排列,显示第1000-1010条数据」时:
-
每个房间的书架都是独立排序的
-
无法直接知道全局第1000条数据在哪里
解决方案:
想象你管理10个快递分拣站(分库分表),每个站点有自己的包裹编号。现在要找出全国第1000-1010个发出的包裹:
-
笨方法:让所有站点把前1010个包裹发到总部,总部排序后挑出需要的(全局排序法)
-
聪明方法:
-
先问每个站点第1010个包裹的发货时间
-
找到全国第1010个包裹的时间
-
让各站点只发送比这个时间晚的包裹,再次排序(二次查询法)
-
-
最懒但有效的方法:把所有包裹信息抄一份到总部的智能黑板(Elasticsearch),随时查黑板即可!
下面具体说下各个方案、优缺点和做法:
方案 | 适用场景 | 技术实现难度 | 缺点 |
---|---|---|---|
全局排序法 | 数据量小,分片少 | 低 | 性能差,深分页爆炸 |
二次查询法 | 排序字段有索引,分片较多 | 中 | 需要两次查询 |
游标分页法 | 有全局有序字段(如ID、时间戳) | 低 | 不支持随机跳页 |
搜索引擎辅助 | 高频复杂查询,容忍秒级延迟 | 高 | 维护成本高,数据延迟 |
(1)全局排序法(简单但低效)
实现步骤:
-
每个分片独立执行
ORDER BY create_time DESC LIMIT 0, 1010
-
把所有分片的结果汇总到一个中心节点
-
中心节点对所有数据进行全局排序,再取第1000-1010条
缺点:
-
分片数越多,数据传输量指数级增长
-
深分页(offset很大)时性能灾难(例如:offset=100万)
(2)二次查询法(优化性能)
核心思路:避免全量数据传输,分两次查询缩小范围
步骤示例(查询第1000-1010条):
-
第一次查询 :每个分片返回前1010条数据的最小时间戳
- 例如分片1的最小时间戳是T1,分片2是T2...
-
取所有分片中第1010小的全局时间戳T_global
-
第二次查询 :各分片查询
create_time >= T_global
的数据 -
汇总后再次排序取最终结果
优点:
-
大幅减少数据传输量
-
适合排序字段有索引的情况(如时间戳)
(3)游标分页法(避免深分页)
核心思路:用连续且唯一的字段(如自增ID、时间戳)作为游标
优势:
-
完全避免
offset
带来的性能问题 -
天然适合分库分表,每个分片只需按游标条件过滤
限制:
-
必须有一个全局有序且唯一的字段
-
用户无法直接跳转到任意页码(如第1000页)
(4)搜索引擎辅助(终极武器)
适用场景 :高频复杂分页查询
实现方式:
-
将分库分表的数据实时同步到Elasticsearch/Solr
-
所有分页查询直接走搜索引擎
-
搜索引擎内部维护全局排序
优势:
-
完全屏蔽分库分表复杂性
-
支持复杂条件过滤+高并发查询
代价:
- 需要维护额外系统,数据一致性延迟增加
实际开发中的建议
-
优先使用游标分页:
- 如App的瀑布流页面,用户只会不断下滑,不需要跳页
-
限制最大分页深度:
- 例如最多允许查看前100页,防止恶意深分页攻击
-
结合业务优化:
- 电商按价格排序时,可预先对价格分段(0-100元、100-200元...)
-
终极方案:
- 分库分表+Elasticsearch组合,牺牲一定实时性换取高性能查询