Feed流中不能采用传统的分页模式:假设一页10条,按时间排序,用户看第一页时关注对象又刚好发布了十条内容,请求第二页就会返回跟看过的第一页一模一样的内容。
因此要使用游标分页模式,第一次请求时不携带游标,按照排序获取前n条,然后返回一个游标和页数据,请求下一页时根据游标计算下一页的内容。

Redis实现


使用ZSetZRANGEBYSCOREZREVRANGEBYSCORE可以方便地实现滚动分页。
以把时间戳作为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);  
}