信息流与推送设计
信息流(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 读扩散并用推拉结合合并结果;存储用有序集合加游标分页避免深分页,未读计数用计数器配合定期校准,推送只承诺最终可达而不承诺强实时。