大数据量的分页,怎么办?
背景
分页功能这个是大家工作中经常碰到的,相信大家脑海中直接跳出limit offset,size . 当进行深度分页时会卡崩,为什么?因为执行这条sql需要先读取offset+size条数据,根据实际业务在需要进行排序,然后丢弃到offset条数据。
例如:假设有一张表,三个字段,分别为id,name,age. 其中id是主键。age 普通索引。
需求:我们需要查找出按照年龄排序offset=1000000 size =10 的记录。
如果使用limit offset,size,需要进行offset+size 次回表操作。直接卡崩你~
不同公司的存储架构不同,需针对单表和分库分表来考虑。
一、单表
1.1 覆盖索引
保证你所查询的字段能通过索引来查询出来。
例如:
ALTER TABLE user ADD INDEX idx_age_name(age, name);
SELECT age, name FROM user ORDER BY age LIMIT 100000,10;
优点:减少了 offset+size 次回表操作。
1.2 子分页查询
-- 先查索引定位ID,再查数据
SELECT * FROM user
WHERE id >= (SELECT id FROM user ORDER BY age LIMIT 100000, 1)
LIMIT 10;
原理:通过主键索引提前查询到offsite的位置,在通过这个进行分页,减少了offsite次回表操作。
缺点:id 必须是连续递增的;不适合过滤条件筛选
1.3 游标分页
-- 第一页
SELECT * FROM user ORDER BY age LIMIT 10;-- 获取下一页(假设上一页最后一条记录的id是12345)
SELECT * FROM user
WHERE id > 12345
ORDER BY age
LIMIT 10;
优点:避免了OFFSET带来的性能问题,适合无限滚动场景
缺点:禁止跳页
1.4 分区表
这个是根据自己的业务来实现,适合时间或者id范围进行分区。
CREATE TABLE user(id INT NOT NULL,age int NOT NULL,name char(100)
) PARTITION BY RANGE (age) (PARTITION p10 VALUES LESS THAN (11),PARTITION p20 VALUES LESS THAN (21),PARTITION p30 VALUES LESS THAN (31),PARTITION pmax VALUES LESS THAN MAXVALUE
);
优点:
-
减少单个分区数据量,提高查询效率
-
可以并行查询不同分区
-
便于维护(可单独备份/恢复分区)
缺点:
-
所有分区仍在同一数据库实例
-
分区键选择不当可能导致性能下降
-
某些操作(如跨分区JOIN)可能更慢
二、分库分表
假设分为5个库,每个库有2张表,按照用户id来分片。
问题分析痛点:
- 1:数据分散,全局排序难
各分片数据独立排序,合并后可能乱序,必须全量捞数据重排。 - 2:深分页=分片全量扫描
每张表都要查offset + size
条数据,性能随分片数量指数级下降。 - 3:内存归并压力大
100万条数据 × 10个分片 = 1000万条数据在内存排序,非常可能触发oom。
1.1 游标分页
-- 第一页
SELECT * FROM user ORDER BY age LIMIT 10;-- 获取下一页(假设上一页最后一条记录的id是12345)
SELECT * FROM user
WHERE id > 12345
ORDER BY age
LIMIT 10;
缺点:禁止用户跳页
1.2 二次查找法
-- 第一次查询(查询的offset=准备查询的偏移量/片键)
SELECT id FROM user ORDER BY age LIMIT 1000000/10,10;-- 第二次查询(假设第一次查询的第一条记录的id是12345)
SELECT * FROM user
WHERE id > 12345
ORDER BY age
LIMIT 10;
原理: 第一次查找出所有分片上的排序数据。第二查询根据分片上查询的结果进行分页上全局最小的数据进行查询。
优点:避免全局数据排序;支持跳页
缺点:两次查询,麻烦
1.3 结合ES进行查询
ES 适合复杂的查询及其分页(倒排索引)。
#1,在es中查找出符合的id
GET /user/_search
{ "query": { "match_all": {} }, "sort": [{"create_time": "desc"}], "from": 1000000, "size": 10
}
# 第二次查询(假设第一次查询的id是1,2,3,4,5,6.。。。。10)
SELECT * FROM user
WHERE id in (1,2,3,4,5,6,7,89,10)
ORDER BY age
优点:
- 大数据量情况性能秒杀数据库,百万数据毫秒响应
- 支持复杂的搜索条件(ES擅长之处)
缺点:
- 需要额外引入es组件,架构复杂度搞
- 数据一致性问题
题外话:如何保证Mysql+ES的数据一致性问题
- 双写模式
// 伪代码示例
public void createUser(User user) {// 1. 分库分表写入int shard = user.getId() % SHARD_COUNT;userDao.insert(shard, user);// 2. ES写入esClient.index(user.toDocument());// 3. 可以考虑加入本地事务表保证一致性
}
- 基于CDC的异步同步方案
-
使用Canal/Debezium/Maxwell 监听MySQL binlog
-
通过Kafka消息队列解耦
-
最终写入Elasticsearch