Feed 流系统设计
Feed 流(信息流)是社交和内容产品的核心——微博首页、朋友圈、抖音推荐,本质上都是在回答同一个问题:”用户打开 App 时,应该看到哪些内容,按什么顺序?”
一、业务场景
核心操作:
- 用户 A 发布内容 → A 的所有粉丝的 Feed 中可见
- 用户 B 打开 App → 看到关注的人最新发布的动态
核心矛盾:
1 | 写操作:一条内容需要"分发"给 N 个粉丝(推模式) |
二、推模式(Fanout on Write / 写扩散)
原理:用户发内容时,直接把内容 ID 推送到所有粉丝的 Feed 收件箱中。
1 | 用户 A 发布动态 |
优点:读路径极短,打开 App 即时展示(O(1))
缺点:写放大严重——大 V 发一条,要写 100 万次
存储选型:
1 | 粉丝收件箱 → Redis List / Sorted Set |
三、拉模式(Fanout on Read / 读扩散)
原理:用户发内容只存到自己发件箱,粉丝打开 App 时从所有关注者的发件箱聚合拉取。
1 | 粉丝打开 App |
优点:写路径简单,无写扩散
缺点:读路径重——关注 500 人就要做 500 次查询 + 内存归并排序
适用场景:用户关注数在 100~500 之间,粉丝数不可控
四、推拉结合(混合模式)
生产环境普遍采用的方案,核心思想:按粉丝数分级处理。
1 | 用户发一条动态 |
粉丝侧读取逻辑调整:
1 | 打开 App |
阈值选择经验:
- 阈值太低 → 退化为推模式,写放大问题
- 阈值太高 → 退化为拉模式,读聚合开销大
- 实践建议:1 万 ~ 10 万粉丝(根据系统压力动态调整)
五、存储与分页
5.1 Redis 存储结构
1 |
|
5.2 冷热分离
| 数据层级 | 存储 | 时间范围 | 策略 |
|---|---|---|---|
| 热数据 | Redis Sorted Set | 最近 3 天 | TTL = 7 天,自动过期 |
| 温数据 | MySQL | 3 天 ~ 3 个月 | 按时间分区,定时归档 |
| 冷数据 | 对象存储(COS/OSS) | 3 个月以上 | 压缩存储,按需加载 |
5.3 分页与一致性
- 游标分页:使用时间戳作为游标,避免 offset 分页在数据变化时的重复/丢失
- MySQL 层面:
WHERE create_time < last_cursor ORDER BY create_time DESC LIMIT 20 - 关注关系变化:如果在分页过程中关注/取关某人,当前页不受影响,下一页反映最新关系
六、时间线一致性
问题:用户 B 先关注 A,A 发布动态,然后 B 取关 A。B 的 Feed 中是否应该看到这条动态?
推模式下,动态已在 B 的收件箱中,取关后仍可见——这其实是合理的,因为发布时刻 B 确实是 A 的粉丝。
生产实践:读取时做一次关注关系校验,过滤掉已取关的用户内容。开销很小(关注关系可走本地缓存)。
七、生产注意事项
大 Key 问题:大 V 的动态详情可能很大(视频、长文),Redis 大 Key 影响稳定性。解决方案:Feed 收件箱只存 ID(几十字节),详情存独立 Key 或 MySQL。
热点问题:大 V 发内容后,短时间内被大量粉丝拉取。本地缓存(Caffeine)缓存热门内容详情 5 分钟,CDN 缓存图片/视频等静态资源。
消息队列消峰:推模式下,大 V 发布触发百万级粉丝写入,通过消息队列分批消费。每批 1000 个粉丝,持续 10-30 秒完成全量扩散。
八、小结
Feed 流系统的核心选择是推拉策略——没有银弹,只有权衡。推模式读快写慢,适合粉丝数可控的场景;拉模式写快读重,适合关注数可控的场景;混合模式在两者间找平衡,是大多数社交产品的最终选择。