Feed流中不能采用传统的分页模式:假设一页10条,按时间排序,用户看第一页时关注对象又刚好发布了十条内容,请求第二页就会返回跟看过的第一页一模一样的内容。
因此要使用游标分页模式,第一次请求时不携带游标,按照排序获取前n条,然后返回一个游标和页数据,请求下一页时根据游标计算下一页的内容。
Redis实现
使用ZSet和ZRANGEBYSCORE或ZREVRANGEBYSCORE可以方便地实现滚动分页。
以把时间戳作为score,按时间降序的分页(每页n条)为例:
请求时带上minTime和offset两个参数,minTime作为分页范围的判断,offset指示要跳过的minTime数量。
第一次请求时,minTime为一个足够大的数,offset为0,然后获取按时间降序的前n条数据,newOffset为最小时间戳出现的次数。返回此次查询的minTime和newOffset。
第二次查询以上一次的minTime和newOffset作为参数。
nextOffset的初始值要初始化为上一次的offset,防止下一页的全部时间都和上一页的最小时间相等,此时newOffset会变为offset+一页的条数。
public Result query(Long max, Integer offset) {
Long userId = UserHolder.getUser().getId();
String key = RedisConstants.FEED_KEY + userId;
Set<ZSetOperations.TypedTuple<String>> tuples = stringRedisTemplate.opsForZSet()
.reverseRangeByScoreWithScores(key, 0, max, offset, 2);
if (tuples == null || tuples.isEmpty()) {
return Result.ok();
}
List<Long> blogIds = new ArrayList<>(tuples.size());
long minTime = 0;
int nextOffset = offset;
for (ZSetOperations.TypedTuple<String> tuple : tuples) {
String blogIdStr = tuple.getValue();
Double timeDouble = tuple.getScore();
if (blogIdStr == null || timeDouble == null) {
continue;
}
Long blogId = Long.parseLong(blogIdStr);
blogIds.add(blogId);
long time = timeDouble.longValue();
if (time == minTime) {
nextOffset++;
} else {
minTime = time;
nextOffset = 1;
}
}
String blogIdsStr = StrUtil.join(",", blogIds);
List<Blog> blogs = query()
.in("id", blogIds)
.last("order by field ( id ," + blogIdsStr + ")")
.list();
ScrollResult scrollResult = new ScrollResult();
scrollResult.setList(blogs);
scrollResult.setMinTime(minTime);
scrollResult.setOffset(nextOffset);
return Result.ok(scrollResult);
}