783 字
4 分钟
MySQL 和 Redis 偏移量分页在数据增删场景下的问题与解决方案
一、问题背景:偏移量分页的固有缺陷
无论是 MySQL 的 LIMIT offset, size,还是 Redis 的 ZRANGE key start stop,它们都基于结果集中的位置(偏移量)进行分页,而非基于数据本身的值。
当数据发生插入或删除时,结果集中后续元素的位置会变化,从而导致分页结果异常。
二、具体问题表现
1. MySQL 中的问题
数据删除 → 跳过记录
-- 初始数据SELECT * FROM users ORDER BY id;-- 1 | A-- 2 | B-- 3 | C-- 4 | D-- 5 | E
-- 第一页SELECT * FROM users ORDER BY id LIMIT 0, 2;-- 1 | A-- 2 | B
-- 删除 id=2DELETE FROM users WHERE id = 2;
-- 第二页SELECT * FROM users ORDER BY id LIMIT 2, 2;-- 4 | D-- 5 | E原因:删除后,原第3条(C)变为第2条,但 LIMIT 2,2 跳过前2条,导致 C 被跳过。
数据新增 → 重复显示
-- 第一页SELECT * FROM users ORDER BY id LIMIT 0, 2;-- 1 | A-- 2 | B
-- 插入 id=1.5 的记录INSERT INTO users VALUES (1.5, 'A1');
-- 第二页SELECT * FROM users ORDER BY id LIMIT 2, 2;-- 2 | B-- 3 | C原因:新增数据改变了原有记录的位置,B 被第二页再次包含。
2. Redis Sorted Set(ZSET)中的类似问题
删除元素 → 跳过
ZADD users 1 "A" 2 "B" 3 "C" 4 "D" 5 "E"ZRANGE users 0 1# "A", "B"
ZREM users "B"ZRANGE users 2 3# "D", "E"新增元素 → 重复
ZRANGE users 0 1# "A", "B"
ZADD users 1.5 "A1"ZRANGE users 2 3# "B", "C"根本原因一致:分页依赖索引位置,而非数据值本身。
三、问题本质总结
| 问题类型 | 表现 | 根本原因 |
|---|---|---|
| 数据删除 | 后续记录被跳过 | 偏移量固定,数据前移 |
| 数据新增 | 已展示数据重复 | 新数据插入导致原有数据后移 |
核心缺陷:分页依赖“位置”,而非“数据标识”。
四、解决方案:游标分页(Cursor-based Pagination)
使用上一页最后一条记录的排序字段值作为下一页起点,避免依赖偏移量。
MySQL 游标分页示例
-- 第一页SELECT * FROM users ORDER BY id LIMIT 2;-- 最后一条 id = 2
-- 第二页SELECT * FROM users WHERE id > 2 ORDER BY id LIMIT 2;-- 返回 id=3, 4要求:排序字段需唯一且有序(如自增ID、时间戳)。
Redis 游标分页示例(基于分数)
# 第一页ZRANGEBYSCORE users -inf +inf LIMIT 0 2# 假设最大分数为 2
# 第二页ZRANGEBYSCORE users (2 +inf LIMIT 0 2# 返回 "C", "D"使用 (score 表示开区间,避免重复。
五、优势对比
| 方式 | 是否受增删影响 | 是否支持跳页 | 适用场景 |
|---|---|---|---|
| 偏移量分页(LIMIT / ZRANGE) | 是 | 支持 | 数据静态、小规模、需跳页 |
| 游标分页(WHERE id > ? / ZRANGEBYSCORE) | 否 | 不支持 | 动态数据、高性能、无限滚动 |
六、结论
在数据频繁变动的场景下,应优先使用游标分页(Cursor-based Pagination)替代偏移量分页,以确保分页结果的一致性、完整性与稳定性。
MySQL 和 Redis 偏移量分页在数据增删场景下的问题与解决方案
https://hyglgithub.github.io/AstroBlog/posts/20251002/