Feed 流:推 vs 拉
「设计 Twitter timeline」是出场率最高的面试题之一,因为它的核心矛盾(粉丝量级差异 8 个数量级)逼你做真正的权衡——没有标准答案,只有按数字说话。本课的主要依据是 Twitter 工程负责人 Raffi Krikorian 的经典分享《Timelines at Scale》[1]。
§1 · 题目与本质
题目:设计 Twitter/微博式 timeline——用户打开 App,看到「关注的所有人」的推文按时间排序。本质矛盾一句话:
发一条推文是 1 次写,但可能需要让 1 亿人的 timeline 更新;读 timeline 却是每天几十亿次。—— 写读两边的量级差异横跨 8 个数量级
怎么处理这个不对称,就是「推 vs 拉」之争。
§2 · 两种极端扇出写 vs 扇出读
| 方案 | 机制 | 代价 |
|---|---|---|
| 推(扇出写) Fan-out on write |
发推时,立刻写入每个粉丝的收件箱(Redis List,每人保留最近 K 条)。读时只读自己的收件箱,O(1) | 大 V 之痛:1 亿粉丝 = 一次写放大 1 亿次;僵尸粉白白消耗写入;删除推文要追遍收件箱 |
| 拉(扇出读) Fan-out on read |
发推只写自己的「发件箱」;读时现场遍历关注列表,合并 K 个人的最新推文(k 路归并) | 读之痛:关注 1000 人 = 一次读扇出 1000 次查询;大 V 的推文被反复重复读 |
推 = 把成本压在写侧(发推的人买单);拉 = 把成本压在读侧(看 timeline 的人买单)。读比写多几个数量级(Lesson 0002 直觉)→ 所以纯拉很少见,纯推是主流起点。成本压到量级小的那侧,是通用架构直觉。
§3 · 工业答案混合推拉
Twitter 的经典方案(正是为了解决「Justin Bieber 问题」——他的粉丝比系统里一半用户还多)[1]:
- 普通用户发推 → 推:异步扇出(发事件进 MQ,L7;worker 写入各粉丝收件箱),粉丝量 < 阈值(如 1 万);
- 大 V 发推 → 不推:只写自己的发件箱;
- 读 timeline → 合并:自己收件箱(已含普通用户推文)∪ 关注的大 V 发件箱(现场拉取),按推文 ID 排序(ID 里带时间戳,Snowflake 类设计)。
关键工程细节:
- 收件箱裁剪:每人只留最近 N 条(如 800),LRU 淘汰——timeline 不需要历史全量(历史走「翻页加载」另路查询);
- 扇出离线化:推的过程全异步,发推接口只写发件箱 + 发事件就返回;
- 活跃度过滤:僵尸粉可以延迟推送甚至不推,省下巨额写入(工业界按活跃度分级)。
① 忘了问「粉丝量分布」就选纯推——大 V 场景直接爆炸;② 推的过程做成了同步——发推接口延迟由最慢粉丝决定;③ 只顾推文本体,忘了 timeline 的「翻历史」与「 timeline 是另一个存储」。先问分布,再选方案。
§4 · 随堂检测
§5 · 检索练习
场景题:1 亿日活、平均关注 200 人、1% 用户粉丝超 100 万(最大 8 千万)。凭记忆写出你的推拉混合方案:谁推谁不推、读侧怎么合并、收件箱怎么存。
对照参考要点
① 粉丝 < 阈值(如 1 万)→ 异步推收件箱(MQ 扇出,worker 写 Redis List);大 V 只写发件箱不推。② 读 = 自己收件箱 ∪ 现场拉关注的大 V 发件箱,按 ID(含时间戳)k 路归并。③ 收件箱每人截断最近 ~800 条 + 活跃度分级过滤。数字不同结论可能不同——关键是你先要到了「粉丝分布」这个数字。
§6 · 本周行动
在你常用的社交 App 上做个侦探实验:关注一个大 V,等 TA 发新内容,观察你的 timeline 更新延迟(刷新才出现?推通知?延迟几秒还是几分钟?)。再转发一条大 V 的内容,观察它出现在粉丝 timeline 的方式——用本课知识反推背后的推拉策略,写两句你的推理。
§7 · 延伸资源
- 首选精读:Raffi Krikorian《Timelines at Scale》——推拉混合方案的原始出处,Bieber 问题一词的来源[1]
- Snowflake ID:timeline 排序依赖「ID 即时间」的设计[2]
- Alex Xu Vol.1「Design Twitter Feed」章可作对照答案
- 下一课 → Lesson 0012 大案例 II:秒杀系统