在社交应用中,Feed流(信息流)是连接用户与内容的核心桥梁。如何高效分发和读取海量动态,直接决定用户体验。本文聚焦Redis在Feed流场景下的经典实现,深入剖析推拉模式的取舍,并详解基于有序集合的滚动分页技术,助你构建高性能关注推送系统。

Feed流业务场景与模式选择

Feed流通常分为两种模式:Timeline(时间线)智能排序(算法推荐)

  • Timeline模式:严格按发布时间排序,如朋友圈、关注页。优点是数据完整、逻辑简单;缺点是信息噪音大,阅读效率随关注量下降。
  • 智能排序:通过算法(如协同过滤)过滤低质内容,如抖音推荐页。优点是用户粘性高;缺点是实现复杂,依赖大量数据训练。

本文场景聚焦个人页面的关注动态流,需保证内容完整性,因此采用Timeline模式

Timeline模式的核心挑战

Timeline模式的核心在于数据分发与读取的效率平衡。假设用户A关注了B,当B发布动态时,系统需快速让A看到。这涉及两个操作:

  • 写扩散:发布时主动推送给粉丝。
  • 读扩散:读取时实时聚合关注列表内容。

不同实现方案的本质,就是权衡写操作与读操作的复杂度、存储成本及一致性。

三种实现方案深度对比

拉模式(读扩散)

每个用户维护自己的发件箱(Outbox),发布内容写入自身发件箱。粉丝读取时,实时拉取所有关注用户的发件箱,合并排序后返回。

工作流程:发布时写入发件箱(Redis ZSet,score为时间戳);读取时并行拉取关注列表的发件箱,内存归并排序。

优点:存储成本低,每条内容仅一份;写操作轻量,复杂度O(1)。

缺点:读操作重,关注越多延迟越高;大V发件箱可能成为热点。

适用场景:用户关注数有限(如朋友圈)、读请求量不大。

技术要点:使用ZSet存储,多路归并排序优化,可配合本地缓存。

推模式(写扩散)

每个用户维护收件箱(Inbox)。发布时,系统将内容推送给所有粉丝,写入粉丝收件箱。读取时直接拉取,无需合并。

工作流程:发布时查询粉丝列表,批量写入粉丝收件箱(Redis List/ZSet);读取时直接从收件箱按时间倒序获取。

优点:读操作极轻,O(1)读取;实时性好,内容预聚合。

缺点:写放大严重,大V发布需写入百万级收件箱;存储成本高,内容冗余;删除/更新复杂。

适用场景:粉丝数可控、读多写少的场景(如企业内部系统)。

技术要点:异步写扩散(消息队列),收件箱限制容量(如只存最近1000条),大V特殊处理。

推拉结合模式(混合读写)

结合推拉优势,将用户分为普通用户(推模式)大V用户(拉模式)。普通用户发布时推送所有粉丝;大V只推送给活跃粉丝,不活跃粉丝读取时采用拉模式。

工作流程:发布时判断用户类型;读取时先取收件箱,再拉取大V发件箱,合并排序。

优点:平衡读写压力,节省存储,提升读性能。

缺点:实现复杂,需维护用户类型和活跃状态,处理合并去重。

适用场景:大型社交平台(如微博、Twitter)。

技术要点:活跃度判定(最近登录时间),收件箱容量限制,并发拉取优化,推拉阈值动态调整(如粉丝数超10万采用拉模式)。

方案对比总结

维度拉模式推模式推拉结合
写复杂度O(1)O(粉丝数)O(活跃粉丝数)
读复杂度O(关注数)O(1)O(1 + 大V数)
存储成本
实时性依赖合并性能
适用场景好友少、读少粉丝少、读多大V存在、读写均衡
实现难度

选择哪种模式,需结合业务规模、读写比例和存储成本。通常,中小型应用采用推模式即可,大型平台需混合模式。

案例实战:基于推模式的Redis实现

在博主发布博客后,将博客推送给粉丝。粉丝读取收件箱时需分页。以下为核心代码实现(推模式):

@Override
public Result queryBlogOfFollow(Long max, Integer offset) {
    Long userId = UserHolder.getUser().getId();
    //查询收件箱 ZREVRANGEBYSCORE key max min LIMIT offset count
    String key = RedisConstants.FEED_KEY + userId;
    Set> typedTuples = stringRedisTemplate.opsForZSet()
            .reverseRangeByScoreWithScores(key, 0, max, offset, 2);
    if (typedTuples==null||typedTuples.isEmpty()){
        return Result.ok();
    }
    List ids = new ArrayList<>(typedTuples.size());
    long minTime = 0;
    int os=1;
    //解析数据:blogId,minTime(时间戳),offset
    for (ZSetOperations.TypedTuple typedTuple : typedTuples){
        ids.add(Long.valueOf(typedTuple.getValue()));
        //获取分数(时间戳)
        long Time = typedTuple.getScore().longValue();
        if (Time == minTime){
            os++;
        }else {
            minTime = Time;
            os = 1;
        }
    }
    String idstr= StrUtil.join(",", ids);
    List blogs = query().in("id", ids)
            .last("ORDER BY FIELD(id,"+idstr+")").list();
    for (Blog blog : blogs){
        isBlogLiked(blog);
    }
    ScrollResult scrollResult = new ScrollResult();
    scrollResult.setList(blogs);
    scrollResult.setOffset(os);
    scrollResult.setMinTime(minTime);
    return Result.ok(scrollResult);
}

Redis滚动分页查询详解

初识有序集合与ZREVRANGEBYSCORE

Redis有序集合(Sorted Set)为每个元素关联一个分数(score),按分数从小到大排列。常用于排行榜、时间轴等。ZREVRANGEBYSCORE命令用于按分数从大到小返回元素,语法如下:

参数说明

  • key:键名
  • maxmin:分数范围(先max后min)
  • WITHSCORES:返回分数
  • LIMIT offset count:分页参数,跳过offset个,取count个

示例:获取分数100-200之间最高分前3个元素:

ZREVRANGEBYSCORE myzset 200 100 LIMIT 0 3

滚动分页的需求与挑战

传统分页(LIMIT+OFFSET)存在性能问题(offset大时效率低)和数据不一致(集合变化导致重复/遗漏)。滚动分页(Cursor-based)基于上一页最后一条记录的分数作为下一页起点,避免重复和遗漏,且查询效率稳定。

参数设计解析

为什么用时间戳作为分数?最新内容分数最高,第一页查询max设为当前时间戳,min设为0;后续页以上一页最小分数作为max,保证只取更早记录。

处理分数重复:offset的妙用若多个元素同分(如同一秒发布),仅用分数作游标会导致重复或遗漏。解决方案:记录上一页中与最小分数相同的元素个数,作为下一页的offset,跳过这些已取元素。

参数变化举例:假设集合数据如下:

memberscore(时间戳)
A1700000003
B1700000002
C1700000002← 分数重复
D1700000001
E1700000000

第一页(每页2条):max=1700000003, min=0, offset=0, count=2,结果:A(1700000003)、B(1700000002)。此时最小分数1700000002出现1次。

第二页:max=1700000002, min=0, offset=1, count=2,跳过B,取C(1700000002)和D(1700000001)。

第三页:max=1700000001, offset=1,结果E(1700000000)。

命令执行细节

分数范围与LIMIT协作:ZREVRANGEBYSCORE先按分数筛选出有序子集,再应用LIMIT截断,全程服务端完成。

为何min固定为0?时间戳为正整数,0涵盖所有历史数据。

结束条件:查询结果不足count条,说明已到末尾;结果为空则停止。

实际应用注意事项

  • ⚠️ 命令版本:ZREVRANGEBYSCORE在Redis 6.2.0后废弃,推荐使用ZRANGEBYSCORE并加REV参数,如:

  • ⚠️ 并发修改:滚动过程中集合变化可能导致offset失效,但静态或只追加数据集无碍。
  • 性能稳定:每次查询时间复杂度O(log(N)+M),不随页数增加而变慢。
  • WITHSCORES:实现滚动分页时建议返回分数,以便记录下一页的max和offset。

总结

Feed流架构的核心是读写平衡。拉模式节省存储但读延迟高,推模式读取快但写放大,混合模式折中但复杂。实际应用中,推模式+异步写扩散是多数场景的优选。滚动分页通过时间戳游标和offset处理同分问题,确保分页稳定高效。掌握这些技术,你就能构建一个高性能的关注推送系统。

[AFFILIATE_SLOT_1]

[AFFILIATE_SLOT_2]

ZREVRANGEBYSCORE key max min [WITHSCORES] [LIMIT offset count]ZRANGEBYSCORE key min max REV LIMIT offset count