Redis分页其实没那么复杂,设计简单又实用,性能还挺不错的
- 问答
- 2025-12-28 21:43:08
- 1
直接整合自知乎、博客园等技术社区的相关讨论)
说起分页,大家最先想到的肯定是数据库的LIMIT和OFFSET,比如LIMIT 10 OFFSET 20,意思就是从第21条开始拿10条数据,这个方法很直接,但在Redis这种内存数据库里,如果数据量很大,直接用类似的思路可能会遇到问题。
有人可能会想,Redis不是有List(列表)这种结构吗?我是不是可以用LRANGE命令来模拟分页?比如一个存储文章ID的List,用LRANGE mylist 0 9 就能拿到第一页10条数据,用LRANGE mylist 10 19拿到第二页,看起来确实很简单,但这里有个隐藏的问题,就像知乎用户“某技术爱好者”提到的,如果列表中间的数据被修改或删除了,基于下标的分页就会错乱,比如你删除了第5条数据,那么原来第6条数据就会变成第5条,整个后面的顺序都变了,用户翻页时可能会看到重复数据或者漏掉数据,用List做静态的、顺序不变的数据分页还行,但对于频繁更新的动态数据,比如朋友圈、新闻流,就不太合适了。
那怎么办呢?Redis里更常用也更好用的方法是使用Sorted Set,也就是有序集合,这个东西功能很强,它里面的每个成员都有一个分数(score)来排序,我们可以把要分页的数据的ID作为成员,而把创建时间戳或者一个自增的ID作为分数,这样,所有数据就按照时间倒序或者顺序排好了。

具体怎么分页呢?可以用ZREVRANGE命令(如果你要最新的在前)或者ZRANGE命令,ZREVRANGE myzset 0 9 WITHSCORES,就是拿出分数最高的前10条(也就是最新的10条),这解决了第一页的问题,那怎么翻到下一页呢?关键点来了:我们不用OFFSET,而是用上一次查询结果里最后一条数据的分数作为“游标”(cursor)。
第一页拿到的10条数据里,最后一条的分数是1523432532(这是一个时间戳),那么查询第二页的时候,我就不说“从第10条之后开始拿”,而是告诉Redis:“给我找分数小于1523432532的前10条数据”,对应的命令是 ZREVRANGEBYSCORE myzset (1523432532 0 WITHSCORES LIMIT 0 10,这个命令的意思是:在分数从高到低的排序里,找到分数小于1523432532(括号表示不包含这个分数本身)的数据,然后从第一个(0)开始,取10条。
博客园的一位博主“老张”在他的文章里详细解释过这种方法的优势:第一,性能极好,因为Sorted Set底层是跳表数据结构,按分数范围查询的速度非常快,是O(log(N))的复杂度,跟你数据总量关系不大,第二,非常稳定,无论你之前的数据有没有增删,只要新数据的时间戳(分数)是递增的,那么每次基于上次查询的最小分数去取下一页,结果就一定是准确的,不会出现错行、重复的问题,这就完美解决了List分页的痛点。

这种方法有个前提,就是你的分数必须是唯一的、递增的,时间戳通常能满足这个要求,如果可能出现相同分数,那最好在分数后面拼接上一个唯一ID,确保每个成员的分数都是独一无二的。
还有一种更简单的场景,如果你的数据本身就有天然的唯一且递增的ID(比如数据库的自增主键),那你甚至可以直接用这个ID作为Sorted Set里的分数,分页逻辑是完全一样的。
除了Sorted Set,如果分页需求非常简单的,也可以考虑用Hash(哈希)结构,比如你把所有数据存在一个大的Hash里,key是数据ID,value是数据的详情,然后你单独用一个List或者Set来存储所有的数据ID,分页的时候,你先从ID列表里用LRANGE或者通过计算取出当前页的ID集合,然后再用HMGET批量从Hash里取出这些ID对应的详细数据,这种方法在数据量不大、ID列表不常变化时也可行,但同样有List那种因数据增删导致错乱的风险,而且需要两次查询,不如Sorted Set一次查询来得高效和稳定。
所以总结一下,Redis分页的核心思路,特别是对于动态数据,就是放弃使用偏移量OFFSET,转而使用基于值的游标(通常是时间戳或唯一ID),Sorted Set是实现这个思路最得心应手的工具,它设计上稍微多想一步,但实现起来代码很简洁,带来的好处是性能的稳定和数据的准确,应对百万甚至千万级的数据分页都游刃有余,确实就像很多开发者体验后说的,理解了核心概念后,Redis分页设计起来简单又实用,性能也真的很不错。
本文由称怜于2025-12-28发表在笙亿网络策划,如有疑问,请联系我们。
本文链接:https://haoid.cn/wenda/70266.html
