信息流与推送设计

信息流(Feed)是把“我关注的人发布了什么”按时间或权重排序后展示出来,是社交与内容类产品的核心功能。它的难点不在展示,而在“写入时算好还是读取时现算”。本章讲推模式、拉模式与推拉结合,大 V 的写扩散问题、时间线分页与未读计数,以及推送通道的取舍。

三种架构模式

模式写入时读取时优点缺点
推模式(写扩散)写入每个粉丝的收件箱直接读自己的收件箱读取极快、排序简单大 V 一条内容要写数百万次
拉模式(读扩散)只写自己的发件箱拉取全部关注者发件箱并归并写入轻、存储省关注多时读取慢、分页复杂
推拉结合普通用户写扩散,大 V 只写发件箱读收件箱并合并大 V 发件箱兼顾性能与成本实现最复杂,需处理合并去重

选择依据是粉丝数阈值:粉丝少于阈值(例如几万)走推模式,超过阈值只写发件箱,读取时异步合并。

大 V 写扩散问题

一条内容扇出 500 万粉丝意味着 500 万次写入,写放大极其严重,还会挤占普通用户的时间线更新。常见处理方式:

  • 阈值分流:超过粉丝阈值的大 V 不写扩散,读取时拉取发件箱并合并。
  • 延迟扩散:只给最近活跃的粉丝写扩散,长期不活跃的用户在下次登录时按需拉取。
  • 降级丢弃:扩散写入超时时记录待补偿任务,不阻塞发帖主流程。

时间线存储与游标分页

收件箱本质是一个有序列表,用 Redis 有序集合保存最近 N 条(score 用时间戳或内容 id)即可,更早的数据落库:

ZADD feed:inbox:u9001 <score> <contentId>          # 写入收件箱
ZREVRANGE feed:inbox:u9001 0 19 WITHSCORES         # 读取最新 20 条
ZREMRANGEBYRANK feed:inbox:u9001 0 -1001           # 只保留最近 1000 条,控制内存

数据库侧配合游标分页,避免深分页问题:

-- cursor 为上一页最后一条的 (created_at, id)
SELECT id, author_id, content, created_at
FROM feed_content
WHERE author_id IN (?, ?, ?)
  AND (created_at, id) < (?, ?)
ORDER BY created_at DESC, id DESC
LIMIT 20;

未读计数与去重

  • 未读计数:按用户维度用计数器累加,读取后清零;允许少量误差时,定时用数据库校准即可。
  • 去重:同一条内容可能同时出现在收件箱与发件箱,合并时按内容 id 去重;写入收件箱用 ZADD 天然幂等,相同成员会覆盖 score。
  • 已读位置:只保存“最后已读 id”比保存每条已读状态便宜得多,前端据此渲染未读分隔线。

排序与混排

按时间倒序实现最简单,但内容产品通常需要权重排序:时间衰减、互动量与个性化兴趣分共同决定顺序。工程做法是先取候选集,再在应用层统一打分截断。

候选集(收件箱最近 N 条 + 大V发件箱 + 推荐池,约 300 条)
  → 去重 → 过滤(已删除、已屏蔽、已看过)→ 打分排序 → 截取前 20 条
打分 ≈ 时间衰减分 × 互动权重 + 个性化兴趣分 - 惩罚项

在应用层排序的好处是策略可热更新,代价是每次读取都有计算成本,因此候选集规模必须受控。

拉模式的合并示意

// 以下片段需放入 main 函数中运行(省略仓储实现)
func Timeline(ctx context.Context, uid int64, cursor Cursor) ([]Item, error) {
    inbox, _ := inboxRepo.Range(ctx, uid, cursor, 100)     // 推模式部分:自己的收件箱
    bigVs, _ := followRepo.BigVList(ctx, uid)              // 我关注的大 V 列表
    outbox, _ := outboxRepo.Merge(ctx, bigVs, cursor, 100) // 拉模式部分:大 V 发件箱
    merged := mergeByTime(inbox, outbox)                   // 按时间归并,并按内容 id 去重
    if len(merged) > 20 {
        merged = merged[:20] // 截断为本页数量,游标取最后一条
    }
    return merged, nil
}

冷启动与回填

新用户没有收件箱,长期未登录的用户收件箱也会过期,两种情况都需要回填:按关注关系实时拉取一次生成时间线并标记为冷启动,之后转入正常的推拉流程。回填必须限速,避免登录高峰把存储打满。

推送通道一句话

实时到达用长连接(WebSocket 或 SSE)或厂商推送通道,弱实时场景用客户端定时轮询,服务端只需保证“消息不重复推、丢了能补拉”。

小结:信息流按粉丝规模在推与拉之间取舍,普通用户写扩散、大 V 读扩散并用推拉结合合并结果;存储用有序集合加游标分页避免深分页,未读计数用计数器配合定期校准,推送只承诺最终可达而不承诺强实时。

笔记加载中…