系统设计:Feed 流(朋友圈/Twitter/微博)
Feed 流是系统设计的硬核题——读多写多、要排序、要实时、要扩展。本文讲清两种核心模型(推模式 vs 拉模式 vs 推拉结合)、时间线存储、热门内容、分页与"读到底",以及不同规模(几万 vs 上亿用户)该选哪种。
本文要点
- 主题聚焦:DevOps
- 适合人群:独立开发者、全栈工程师、AI 应用开发者
- 核心价值:快速理解背景、实践路径、常见误区和工程建议
系统设计:Feed 流(朋友圈/Twitter/微博)
一、Feed 流到底在解决什么问题
打开微博、Twitter、朋友圈,首页那条按时间或兴趣排好的内容流,就是 Feed。看起来只是个列表,背后的系统却是名副其实的"硬核题"。
它的核心矛盾在于:读多写多,还要排序,还要实时,还要能扩展。一个普通社交产品,用户日均发帖可能就几条,但日均刷 Feed 几十次、每次几十条——读写比天然严重倾斜到读侧。可你也不能因此轻视写侧:只要出一个大 V,他一条动态要触达千万粉丝,写扩散瞬间就变成灾难。
把需求拆开看:
- 关注关系:谁关注了谁,是有向图。A 关注 B,不代表 B 关注 A。
- 时间线排序:Feed 默认按发布时间倒序,最新优先。
- 实时性:理想情况下,你关注的人发帖后几秒内就能刷到。
- 读多写多:刷的动作远多于发的动作,但发的瞬间压力极大。
- 可扩展:用户从一万涨到一亿,系统不能推倒重写。
这五条相互拉扯。"实时"和"读快"你都想要,但为读快做的预计算越多,写的代价就越大。Feed 系统的设计本质,就是在读写之间、实时与扩展之间反复找平衡。
二、推模式(fan-out on write):发帖时把活干完
最直观的思路:写的时候就把每个粉丝的 Feed 都算好。
发帖人 B 发了一条帖子,系统立刻找出 B 的所有粉丝,把这条帖子的 ID 写到每个粉丝的"收件箱"(inbox)里。粉丝 A 刷 Feed 时,直接读自己的收件箱,一次查询就拿到结果。
B 发帖 → 查粉丝列表 [A, C, D, E...] → 写入 A.inbox / C.inbox / D.inbox ...
A 刷 Feed → 直接读 A.inbox(已排好序)
- 优点:读极快,O(1) 拿到现成结果;读路径简单,容易缓存。
- 缺点:写放大严重。一个百万粉的大 V 发一帖,要做一百万次写入;而且如果粉丝当时不在线,这些写全是浪费。
大 V 灾难是推模式绕不过去的坎。想象某个明星有 5000 万粉丝,他发一条"早上好",系统要同步扇出 5000 万次。轻则延迟飙升、队列堆积,重则把缓存和数据库一起打挂。
所以推模式适合用户基数小、粉丝数普遍不多的场景,比如企业内网微博、小型社区。把它当默认方案没问题,但当你的产品开始出现"头部账号"时,就得准备换挡了。
三、拉模式(fan-out on read):读时再算
换个思路:写的时候啥也不干,读的时候临时去拉。
发帖人 B 发帖,只写到自己的"发件箱"(outbox)。粉丝 A 刷 Feed 时,系统先查 A 关注了哪些人,再去每个人的发件箱里拉最新 N 条,合并、排序、去重,返回给 A。
B 发帖 → 只写 B.outbox
A 刷 Feed → 查关注列表 [B, C, D] → 各拉最新 20 条 → 归并排序
- 优点:写路径 O(1),大 V 发帖和普通人没区别;无写放大,存储省。
- 缺点:读路径重得可怕。关注 500 人就要查 500 个发件箱,即使每次都有缓存,合并和排序也吃 CPU;实时性受读延迟影响。
关注太多就慢是拉模式的硬伤。一个重度用户关注了 2000 人,他每次刷 Feed 都要在 2000 个发件箱里翻一遍,体验直接崩。
所以拉模式适合关注数普遍不多、读 QPS 可控的场景,或者专门用来兜底大 V。注意:拉模式对缓存的依赖极重,缓存命中率一掉,P99 就上天。
四、推拉结合:经典工程解
既然推模式怕大 V、拉模式怕关注多,那就对它们分别处理:
- 普通用户发帖 → 走推模式:粉丝不多,扇出代价可控,读得快。
- 大 V 发帖 → 走拉模式:不扇出,只写发件箱,粉丝读时去拉。
怎么定义"大 V"?常见做法是设一个粉丝数阈值(比如 1 万、10 万),超过就算大 V,阈值可以动态调整。粉丝刷 Feed 时,先从自己收件箱拿普通关注人推过来的内容,再去关注的大 V 发件箱里拉最新几条,合并后返回。
A 刷 Feed:
1. 读 A.inbox(普通关注人的推送)
2. 对 A 关注的大 V,逐个拉最新 N 条
3. 合并、去重、排序
4. 返回
这是 Twitter 早期、微博的经典做法,80% 的工程场景用推拉结合就够了。它不是理论最优,但是工程性价比最高的解。别被"推 vs 拉"的非此即彼骗了,真实系统几乎都是混合的。
五、存储:收件箱、发件箱、关注关系
Feed 系统的三块核心存储:
- 发件箱(outbox):每个用户一份,存自己发过的帖子 ID + 时间戳。天然是写追加、按时间有序的流。
- 收件箱(inbox / user timeline):每个用户一份,存"应该被这个人看到"的帖子 ID。推模式下被写,拉模式下不存。
- 关注关系(follow edge):
follower → followee的有向边,既要正查(B 的粉丝是谁,用于推),也要反查(A 关注了谁,用于拉)。
实现选型上,实战经验是:
- 发件箱/收件箱用 Redis 的 ZSet(按时间戳打分)或 List 最常见,内存够就够快。
- 关注关系用关系数据库能撑一阵,规模上来后切图数据库或专门的图存储。
- 帖子正文单独存 KV 或文档库,Feed 里只放 ID 是铁律——别把正文塞进 ZSet,空间爆、更新难。
一个常被忽视的细节:收件箱不能无限长。一般给个上限(比如最近 1000 条),超了就淘汰老数据——绝大多数用户根本不会翻那么深,省下的内存比那点理论完整性值钱得多。
六、排序:时间线之外
最简单的 Feed 是按时间倒序,但现代产品几乎都引入了推荐/热度排序:
- 时间序:实现简单,但内容质量参差,容易刷到水帖;纯时间序的产品越来越少。
- 热度/推荐序:要一个独立的排序服务,综合考虑点赞、评论、转发、时效衰减、用户兴趣等,输出一个分数。这就是推荐系统的活了。
工程上常见的分层做法:先用时间线保证实时性(最新几条按时间),再用推荐服务重排后续内容。Instagram 是接近纯时间序的代表,Twitter、微博、抖音都引入了算法重排。
排序服务通常异步工作:用户发帖后,先按时间序让粉丝秒看到,再由排序服务离线算分、回填。不要让排序阻塞写路径,否则实时性全毁。排序服务挂了,Feed 应该能降级到纯时间序,而不是直接 500。
七、分页:为什么 cursor 比 offset 好
Feed 必须分页。但 LIMIT 20 OFFSET 1000 这种传统分页,在 Feed 场景下是反模式:
- offset 越大,数据库要扫描跳过的行越多,深度分页慢到爆炸。
- Feed 是动态的,你翻第二页时第一页可能已经新进了帖子,offset 会错位、重复、漏内容。
正确做法是 cursor 分页:用上一页最后一条的 (时间戳, 帖子 ID) 作为游标,下一页查 WHERE (ts, id) < (cursor_ts, cursor_id) LIMIT 20。
- 索引命中稳定,深度不衰减。
- 不受新数据插入影响,不会错位。
"读到底"的挑战:很多 Feed 设计上永远加载不完(微博首页能刷一整天)。工程上要么限制最大深度(翻到 N 页就提示"看完了"),要么用混合策略——近端用收件箱,远端用推荐服务补流。真正意义的"无限瀑布流"在分布式下极难做,因为收件箱的物理容量是有限的,翻到深处必然要换数据源。
八、不同规模怎么选
不要一上来就上推拉结合 + 推荐系统 + 分布式 timeline。规模决定选型,过度设计比设计不足更贵:
- 小规模(几万用户、人均关注几十):纯推模式。Redis ZSet 当收件箱,简单粗暴,读写都快。一个实习生一周能上线。
- 中规模(几十万到几百万用户,有大 V):推拉结合。给粉丝数设阈值,大 V 走拉,普通人走推。这是性价比最高的拐点。
- 大规模(上亿用户、读 QPS 数十万):分布式 timeline 服务 + 独立推荐系统。收件箱按用户分片到多个 Redis 集群,排序下沉到专门的 Ranking Service,引入 Kafka 做异步扇出。这个阶段,"推还是拉"已经退化成一个实现细节,真正的难点是成本、延迟、一致性的三角权衡。
九、真实案例:Twitter 与微博的演进
Twitter 是 Feed 系统设计的教科书案例。早期纯推模式,把每个用户的 timeline 物化到 Redis。但随着用户增长,大 V 的扇出成本爆炸——据说 Bieber 一条推文要触发数十万次写入。Twitter 后来逐步转向混合架构,核心 timeline 走推荐重排(算法 Feed),同时引入拉模式兜底大 V。他们工程博客上 fanout 服务的演进是必读材料。
微博面对的问题更棘手:中文社交里大 V 粉丝量动辄上亿(明星、官方号),纯推完全不可行。微博很早就上推拉结合,大 V 仅写发件箱,粉丝侧按权重拉取;后续又引入了大规模的机器学习推荐排序,首页 Feed 早就不只是时间序。微博技术博客里有大量关于"收件箱分片""粉丝阈值动态调整"的实战经验,值得翻出来细读。
两个案例的共同点:架构都是被规模逼着演进的,没有谁一上来就设计出完美的 Feed 系统。好的架构不是一步到位,而是留出演进的余地。
小结
- Feed 的核心矛盾是"读多写多 + 实时 + 排序",任何方案都在读写之间做权衡。
- 推模式读快写慢,怕大 V;拉模式写快读慢,怕关注多;推拉结合是 80% 场景的性价比解。
- 存储三件套:发件箱、收件箱、关注关系;Feed 里只放 ID,正文单独存。
- 分页一律用 cursor,offset 是反模式;"无限瀑布流"在分布式下极难,要靠推荐服务兜底。
- 规模决定选型:小用推、中用推拉结合、超大才上分布式 timeline + 推荐系统。
金句:Feed 系统没有银弹,只有与规模匹配的妥协。推拉之争,本质是把写的成本换到读,还是把读的成本换到写——而你的用户量,决定了哪一侧的代价你更付得起。
Related