lrange越往后越慢是因为其底层为双向链表,需从头逐个遍历next指针至offset位置,时间复杂度为o(offset + count),offset越大性能越差;且页码分页在动态列表中会导致索引错位、数据不一致。

LRANGE 为什么越往后越慢
因为 LRANGE 底层是链表遍历:Redis 的 List 是双向链表,LRANGE mylist 10000 10019 并不是“跳到第 10000 个节点再取 20 个”,而是从头开始逐个 next 指针跳,直到第 10000 个才开始收集——时间复杂度是 O(offset + count)。数据量一过万,响应就明显卡顿,slowlog 里必然出现记录。
别用 currentPage × pageSize 算索引
这是最常见也最危险的写法。它隐含两个错误假设:
- 列表长度固定(实际可能被
LPOP/LTRIM/LREM动态删减) - 用户只往前翻页(实际可能跳转到第 50 页再点回第 2 页)
一旦中间有删除操作,页码和真实索引就彻底错位,“第 3 页”可能返回空、重复或漏数据。更糟的是,这种错位无法被 Redis 自动检测或补偿。
滚动分页必须用游标,不能用页码
真正可行的优化路径只有一条:放弃“第 N 页”这个概念,改用游标(cursor)驱动下一页请求。具体做法:
Redis 8.2.3 是一款安全优先的高性能键值存储系统。该版本紧急修复了可能引发远程代码执行(RCE)的高危漏洞(CVE-2025-62507),并解决了 HyperLogLog 及 Cuckoo Filter 等数据结构在特定场景下的崩溃问题。建议所有用户立即升级,以保障生产环境的系统稳定与数据安全。
- 首次请求用
LRANGE key 0 19拿前 20 条,同时记下第 20 条的 ID(比如"msg:10024") - 下一页请求传入这个 ID,服务端查
LRANGE key (index_of_"msg:10024") + 1 20——但注意,你得先用LPOS(Redis 6.0.6+)或预存索引定位它 - 更稳妥的做法是:把 ID 和插入顺序一起存进
ZSET,用ZRANGEBYSCORE查范围,再用HMGET批量取详情
如果必须支持随机跳页(比如后台管理),List 就不该是主存储结构——它天生不支持高效随机访问。此时应把排序字段(如时间戳)作为 ZSET 的 score,用 ZRANGEBYSCORE 或 ZREVRANGEBYSCORE 实现 O(log N) 分页。
缓存层加 MySQL 回源才是稳解
纯 Redis 分页只适合极轻量场景(如未读消息 ≤ 500 条)。真实业务中,更健壮的方案是 Cache Aside:
- 先查
cache:page:23:20,命中直接返回 - 未命中则走 MySQL:
SELECT * FROM items ORDER BY created_at DESC LIMIT 20 OFFSET 440 - 结果序列化后写入 Redis,设
EX 300防雪崩
关键点在于:缓存 key 必须包含排序依据(如 cache:page:23:20:by_time_desc),否则同一页不同排序会互相覆盖。另外,MySQL 的 OFFSET 在大数据量下依然慢,所以最终还是要靠 WHERE id > ? ORDER BY id LIMIT 20 这类游标式查询来兜底。
最容易被忽略的一点:List 分页的“一致性”根本不可靠。它没有事务、没有版本、不保证原子性。如果你需要强一致的页码语义,Redis List 就不是正确工具——这不是配置问题,是数据结构层面的硬限制。










