Redis滚动分页的使用
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);
}