Redis滚动分页的使用

文章探讨了Feed流的两种模式(Timeline和智能排序),比较了拉模式、推模式和推拉结合的优缺点,重点介绍了如何通过滚动分页解决数据分页问题,以及在Redis中使用ScoreSet实现滚动分页的方法。

Feed流

关注推送也叫Feed流。通过无限下拉刷新获取新的信息。

Feed流产品常见有两种模式:

Timeline: 不做内容筛选,简单的按照内容发布时间排序,常用于好友或关注。例如朋友圈

优点:信息全面,不会有缺失。并且实现也相对简单

缺点:信息噪音较多,用户不一定感兴趣,内容获取效率低。

智能排序:利用智能算法屏蔽掉违规的、用户不感兴趣的内容。推送用户感兴趣信息来吸引用户

优点:投喂用户感兴趣信息,用户粘度很高,容易沉迷

缺点:如果算法不精准,可能起到反作用。

Timeline模式的实现方案有三种:拉模式、推模式、推拉结合

拉模式

拉模式也叫读扩散。这是因为,进行阅读的用户本身不接受任何数据,当开始阅读时,才会将关注的内容拉取。模型图如下

优点:

节省内存空间,因为收件箱阅读结束后就会清空,相当于消息只在个人的发件箱中保存了一份。

缺点:

耗时较久,因为除了需要拉取消息外,还需要对消息进行排序,这个过程比较耗时,当一个用户关注的人较多,一次性拉取的消息过多这个耗时会更久。

推模式

推模式也叫写扩散。这是因为当一个人发表动态时,会将该信息推送到每个关注他的收件箱。

优点:

延时低,当粉丝读取消息时,已经是一个排序好的数据。

缺点:

占用内存空间,同一份消息需要存储n份。当一个人粉丝很多时,存储过多重复数据。

推拉结合

也叫读写混合,兼具推拉两种模式的优点。

粉丝数量多的账号拥有一个发件箱,发件箱会推送给活跃用户的收件箱,而对于普通用户不经常使用的,采取拉模式,当开始阅读时,主动从发件箱中拉去。对于粉丝数量不多的一般用户,直接采取推模式即可。

三种模式对比

拉模式

推模式

推拉结合

写比例

读比例

用户读取延迟

实现难度

复杂

简单

很复杂

使用场景

很少使用

用户量少,没有大V

过千万用户量,存在大V

Feed流的分页问题

我们通常对一个页面能够展示的数据有一定限制,因此当收件箱存在很多数据时,我们需要实现分页功能。如何实现Feed流的分页功能是一个复杂的问题,如果采用MyBatis中的分页插件是不能满足分页功能的,因为Feed流是一个动态的,数据会不断更新,因此数据的角标会不停更新,当我们需要查询第5-10条的数据时,可能又保存了一个新的数据到数据库,从而导致我们分页查找的数据存在与上一次查询的数据重复的问题,具体问题流程图如下。

因此我们需要采用滚动分页,所谓滚动分页就是每次查询过数据库后,我们需要记录这一次查询结果的最后一条数据,下一次查询时,从上一次的最后一条开始往后查。流程图如下

而满足滚动分页的Redis数据结构只有ScoreSet,记录每次执行结束后最后一个数据的分数,下一次查询时,从该分数的下一个开始查询。因为我们要根据时间进行排序,因此,这里的分数应该是时间戳。那么实现滚动分页的语句应该为

ZRANGEBYSCORE key -inf +inf WITHSCORES LIMT offset count

命令解释:

  • -inf:表示最小值
  • +inf:表示最大值,这样可以在不知道分数的情况下,对全部的值进行操作
  • offset:偏移量,表示当前数据之后第几个偏移量后开始获取元素
  • count:需要获取元素的数量

在我们实际开发中,还需要注意一个问题就是score重复的问题。对于score重复我们应该记录本次查询结果中,最小score的数量,来充当下一次的偏移量,比如说我在Redis中存在如下几个数据

如果我们一次查询3条数据,在记录到当前查询的最小分数后,下次查询时指定偏移量为1时,那么可能会存在如下问题

zrevrangebyscore feed:test +inf -inf withscores limit 0 3

此时我们查找到m5与n5,接下来我们应该从i5开始查找,那么如果将偏移量设置为 1 进行测试观察结果

zrevrangebyscore feed:test 5 -inf withscores limit 1 3

可以看到,他是从第一个分数等于5的开始计算,获取之后的三个元素,因此,我们需要在开发中,对偏移量进行计算,方便我们下次进行查询时设置偏移量,大概的Java代码如下

//lastId指的是本次查询的最小分数值
//offset指的是偏移量
public Result queryPage(Long lastId, Integer offset) {
    //2、查询属于自己的收件箱
    Set<ZSetOperations.TypedTuple<String>> typedTuples =
            stringRedisTemplate.opsForZSet()
                    .reverseRangeByScoreWithScores(FEED_KEY, 0, lastId, offset, 3);
    //如果set等于null
    if (typedTuples==null || typedTuples.isEmpty()){
        return Result.ok();
    }
    //3、解析数据minTime
    //记录最小分数
    long score=0;
    //记录偏移量
    int os=1;
    List<String> pageValue=new ArrayList<>(typedTuples.size());//设置列表长度为typedTuples的大小
    for (ZSetOperations.TypedTuple<String> typedTuple : typedTuples) {
        String value = typedTuple.getValue();
        //将查询的数据存储在集合中
        pageValue.add(value);
        //记录当前分数
        long time = typedTuple.getScore().longValue();
        if (time==score){
        	//如果当前分数相同,则偏移量加1 
            os++;
        }else {
            //如果不同,说明当前的分数更低,刷新偏移量
            score=time;
            os=1;
        }
    }
    //返回最小的分数以及偏移量供下次调用时使用。
    Pages pages = new Pages();
    pages.setPageValue(pageValue);
    pages.setLastId(score);
    pages.setOffset(os);
    return Result.ok(pageValue);
}
<think>首先,用户的问题是关于使用Redis的ZSET实现排行榜并实现分页功能,同时询问是否有类似PageHelper的工具。用户引用了几个参考内容,我需要基于这些引用来回答。 关键点: - 使用Redis的ZSET(有序集合)实现排行榜。 - 实现分页功能。 - 询问是否有类似PageHelper的工具用于Redis。 参考引用: - [^1]:提到了一个PageDataBean类,用于分页数据,包含数据列表和分页对象(页数、页大小、总记录数等)。 - [^2]:介绍了Redis的基本特性。 - [^3]:描述了Redis的启动、连接和数据类型,包括ZSET。 - [^4]:讨论了分页查询的优化,但内容有点混杂。 - [^5]:提到了基于Redis实现滚动分页使用offset来处理相同score的条目。 我需要构建一个清晰的回答: 1. **介绍Redis ZSET用于排行榜**:ZSET是Redis的有序集合,每个成员有一个分数(score),可以按分数排序,非常适合排行榜。 2. **实现分页功能**:使用ZSET的命令如`ZREVRANGE`或`ZRANGE`来获取指定范围的成员,实现分页。 3. **类似PageHelper的工具**:PageHelper是MyBatis的分页插件,用于数据库分页。对于Redis,没有直接的官方工具,但可以自定义分页逻辑,类似于引用[^1]中的PageDataBean类。 4. **提供代码示例**:既然用户提到了类似PageHelper的工具,我应该展示如何用Java实现分页逻辑,包括计算分页参数。 回答结构: - **步骤1:使用Redis ZSET实现排行榜** - 解释ZSET如何工作。 - 添加成员和分数。 - **步骤2:实现分页功能** - 使用`ZREVRANGE`命令获取排行榜数据(从高到低排序)。 - 计算分页参数:页码(pageNum)、页大小(pageSize)、总记录数(totalCount)。 - **步骤3:分页工具的实现** - 创建一个类似PageHelper的工具类或方法。 - 引用[^1]中的PageDataBean作为参考。 - 展示如何封装分页数据。 - **代码示例**:提供Java代码片段,展示如何调用Redis命令并处理分页。 - **相关问题**:按照系统指令,生成相关问题。 系统指令要求: - 行内数学表达式用$...$,例如分数计算。 - 独立公式用$$...$$。 - 使用中文回答。 - 生成相关问题。 - 回答中引用的段落末尾添加引用标识,如[^1]。 在回答中,我需要自然地引用提供的参考内容。例如: - 当提到分页封装时,引用[^1]。 - 当提到Redis特性时,引用[^2]或[^3]。 - 当提到滚动分页或offset时,引用[^5]。 最终,生成相关问题,如优化、应用等。 完整回答大纲: 1. **介绍Redis ZSET用于排行榜**:简要说明ZSET的特性。 2. **实现分页的基本方法**: - 获取总成员数:`ZCARD key`。 - 获取分页数据:`ZREVRANGE key start stop WITHSCORES`。 - 计算start和stop索引:基于pageNum和pageSize。 3. **封装分页工具**: - 创建一个类似PageHelper的类,处理分页逻辑。 - 参考引用[^1]的PageDataBean。 4. **代码示例**:展示Java代码,使用Jedis或Spring Data Redis。 5. **类似PageHelper的工具**:讨论是否有现成工具,并建议自定义实现。 6. **总结**。 7. **相关问题**:生成3-5个相关问题。 相关问题建议: 1. 如何优化Redis ZSET分页的性能? 2. 如何处理相同分数的成员排序? 3. Redis ZSET排行榜的应用场景有哪些? 4. 是否有其他Redis命令用于分页? 5. 如何与Spring Boot集成实现分页? 现在,构建回答。</think>### Redis ZSET 实现排行榜与分页功能详解 Redis 的 ZSET(有序集合)是实现排行榜的理想选择,因为它能根据分数(score)对成员自动排序,支持高效的范围查询。结合分页功能,可以轻松实现类似“游戏排行榜”或“热门列表”的场景。下面我将一步步解释如何实现,并回答关于类似 PageHelper 工具的问题。 #### 步骤 1: Redis ZSET 的基本用法 ZSET 存储键值对,其中成员(member)关联一个分数(score),Redis 会按分数升序或降序排序。常用命令: - 添加成员:`ZADD key score member`(例如,`ZADD leaderboard 100 user1`)。 - 获取排行榜(降序):`ZREVRANGE key start stop WITHSCORES`,其中 `start` 和 `stop` 是索引位置(从 0 开始)。 - 获取总成员数:`ZCARD key`。 例如,排行榜按分数从高到低排序时,使用 `ZREVRANGE` 可以高效获取指定范围的数据。Redis 支持数据持久化和高性能查询,适合高并发场景[^2][^3]。 #### 步骤 2: 实现分页功能 分页的核心是计算索引范围和封装分页数据。假设每页显示 `pageSize` 条记录,当前页码为 `pageNum`: - **总记录数(totalCount)**:通过 `ZCARD` 获取。 - **起始索引(start)和结束索引(stop)**:计算公式为: $$ \text{start} = (\text{pageNum} - 1) \times \text{pageSize}, \quad \text{stop} = \text{start} + \text{pageSize} - 1 $$ 例如,`pageNum=2`, `pageSize=10` 时,`start=10`, `stop=19`。 - **获取分页数据**:使用 `ZREVRANGE key start stop WITHSCORES` 返回指定页的成员和分数。 - **处理相同分数成员**:如果多个成员分数相同,ZSET 默认按字典序排序。需要额外偏移时,可参考滚动分页方法,使用 `offset` 参数跳过相同分数的条目[^5]。 #### 步骤 3: 类似 PageHelper 的分页工具实现 PageHelper 是 MyBatis 的分页插件,用于数据库查询的自动分页。对于 Redis,没有直接等效的官方工具,但可以自定义一个分页工具类,封装分页逻辑。参考引用[^1]中的 `PageDataBean`,我设计一个简化版的分页工具: - **分页数据封装类**:类似 `PageDataBean`,存储分页结果和元数据。 ```java import java.util.List; @Data // Lombok 注解,自动生成 getter/setter public class PageData<T> { private List<T> dataList; // 当前页的数据列表 private int pageNum; // 当前页码 private int pageSize; // 每页大小 private long totalCount; // 总记录数 private int totalPage; // 总页数 public PageData(List<T> dataList, long totalCount, int pageSize, int pageNum) { this.dataList = dataList; this.pageNum = pageNum; this.pageSize = pageSize; this.totalCount = totalCount; // 计算总页数: $ \text{totalPage} = \lceil \frac{\text{totalCount}}{\text{pageSize}} \rceil $ this.totalPage = (int) Math.ceil((double) totalCount / pageSize); } } ``` - **分页工具方法**:使用 Spring Data Redis 或 Jedis 调用 Redis 命令。 ```java import org.springframework.data.redis.core.ZSetOperations; import org.springframework.data.redis.core.StringRedisTemplate; import java.util.Set; import java.util.stream.Collectors; public class RedisPageHelper { private final StringRedisTemplate redisTemplate; public RedisPageHelper(StringRedisTemplate redisTemplate) { this.redisTemplate = redisTemplate; } /** * 获取 ZSET 分页数据 * @param key ZSET 的键(如 "leaderboard") * @param pageNum 页码(从 1 开始) * @param pageSize 每页大小 * @return PageData 对象,包含分页数据和元数据 */ public PageData<Object> getZsetPage(String key, int pageNum, int pageSize) { // 计算索引范围 long start = (pageNum - 1) * (long) pageSize; long stop = start + pageSize - 1; // 获取总记录数 Long totalCount = redisTemplate.opsForZSet().zCard(key); if (totalCount == null || totalCount == 0) { return new PageData<>(List.of(), 0, pageSize, pageNum); } // 获取分页数据(降序排序) Set<ZSetOperations.TypedTuple<String>> tuples = redisTemplate.opsForZSet() .reverseRangeWithScores(key, start, stop); // 转换为数据列表(例如:List<String> 或自定义对象) List<Object> dataList = tuples.stream() .map(tuple -> tuple.getValue() + ": " + tuple.getScore()) // 示例:拼接成员和分数 .collect(Collectors.toList()); return new PageData<>(dataList, totalCount, pageSize, pageNum); } } ``` - **说明**: - 使用 `reverseRangeWithScores` 实现降序分页查询。 - 分页参数计算封装在工具方法中,类似 PageHelper 的自动化处理。 - 在实际应用中,`dataList` 可以映射为自定义对象(如 `LeaderboardEntry`)。 - 此工具类可直接集成到 Spring Boot 项目中,减少重复代码[^1][^5]。 #### 步骤 4: 是否有类似 PageHelper 的现成工具? - **Redis 官方无直接等效工具**:Redis 本身不提供分页插件,但社区有一些库(如 Redisson)支持高级分页功能,但不如 PageHelper 自动化。 - **推荐自定义实现**:如上所示,通过封装分页逻辑,可以创建轻量级的“Redis PageHelper”。优点包括: - 高性能:ZSET 查询时间复杂度为 $O(\log(N) + M)$(N 是总成员数,M 是页大小),适合大数据量。 - 易于扩展:添加缓存、异常处理等。 - **集成建议**:在 Spring Boot 中,结合 `StringRedisTemplate`,实现类似引用[^1]的分页返回格式。 #### 性能优化提示 - **减少网络开销**:使用管道(pipeline)批量查询。 - **处理大数据量**:如果总记录数过大,避免频繁调用 `ZCARD`,可缓存结果。 - **滚动分页**:对于无限滚动场景,使用 `ZREVRANGEBYSCORE` 并记录最后一个分数作为游标[^5]。 总结:Redis ZSET 是实现排行榜分页的高效方案,通过自定义分页工具类(如基于 `PageData` 的封装),可以模拟 PageHelper 的功能。若与 Spring Boot 集成,代码更简洁[^1][^3]。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

zmbwcx2003

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值