系统设计 Deep Dive:设计 Twitter/微博信息流系统
系统设计深度解析:设计Twitter/微博信息流系统。涵盖Fan-out架构、读写优化、实时推送与海量消息存储策略,还原真实系统设计面试的完整思考过程与面试官追问方向。
面试官真实提问
“请设计 Twitter 的信息流(News Feed)系统。当用户打开 Twitter 时,能够看到他关注的人发布的推文。”
“如何优化’拉取信息流’的性能?如果某用户关注了 1000 个人,怎么处理?”
这是系统设计面试中最经典的题目之一,几乎每个大厂都会问。它考察了读写权衡、缓存策略和数据模型设计的综合能力。
需求澄清清单
功能需求
Must-have:
- 发布推文
- 拉取关注用户的时间线(信息流)
- 按时间倒序排列
Nice-to-have:
- 点赞、评论、转发
- 媒体内容(图片、视频)
- 算法推荐排序
- 无限滚动(Infinite scroll)
规模估算
| 指标 | 估算值 |
|---|---|
| MAU | 5 亿 |
| DAU | 2 亿 |
| 每日推文 | 5 亿条 |
| 平均关注数 | 每人关注 200 人 |
| 读写比 | 约 10:1(读远多于写) |
| P99 延迟 | 信息流加载 < 200ms |
非功能需求
- 可用性: 99.9%
- 一致性: 最终一致性可接受(推文延迟几秒出现 OK)
- 扩展性: 支持水平扩展
第 1 步:高层设计
┌─────────┐
│ 客户端 │ Web / App
└────┬────┘
│
┌────▼──────┐
│ 负载均衡器 │
└────┬──────┘
│
┌─┴─┐
┌──▼──┐ ┌──▼──┐
│Tweet│ │ Feed│ 写路径 / 读路径
│ API │ │ API │
└──┬──┘ └──┬──┘
│ │
┌──▼──┐ ┌▼──────┐
│Tweet│ │ Feed │
│Store│ │ Store │ Cassandra / Redis
└──┬──┘ └───────┘
│
┌──▼───────┐
│ Fan-out │ Kafka
│ Service │
└────┬─────┘
│ 异步推送
└──→ Feed Store
第 2 步:核心组件设计
API 设计
# 发布推文
POST /api/v1/tweets
{
"text": "Hello, world!",
"media_urls": []
}
# 拉取信息流
GET /api/v1/feed?user_id=123&since_id=456&max_id=789&count=20
# 获取用户推文
GET /api/v1/users/123/tweets
数据模型
-- 用户表
CREATE TABLE users (
user_id BIGINT PRIMARY KEY,
username VARCHAR(50) UNIQUE,
display_name VARCHAR(100),
followers_count INT,
following_count INT,
created_at TIMESTAMP
);
-- 关注关系
CREATE TABLE follows (
follower_id BIGINT,
followee_id BIGINT,
created_at TIMESTAMP,
PRIMARY KEY (follower_id, followee_id)
);
-- 推文表
CREATE TABLE tweets (
tweet_id BIGINT PRIMARY KEY,
user_id BIGINT,
text VARCHAR(280),
created_at TIMESTAMP,
media_urls JSON,
INDEX idx_user_created (user_id, created_at DESC)
);
-- 信息流(预生成)
CREATE TABLE feeds (
user_id BIGINT,
tweet_id BIGINT,
created_at TIMESTAMP,
PRIMARY KEY (user_id, tweet_id)
);
Feed 生成策略(核心考点)
这是这道题最重要的部分,面试官会重点考察你对 Push vs Pull 模型的理解。
方案 1:Pull 模型(扇出读取)
用户请求信息流
│
▼
查询用户关注了谁
(用户 A, 用户 B, 用户 C...)
│
▼
查询每个关注用户的最新推文
│
▼
合并、排序、去重
│
▼
返回信息流
(拉模式:读时合并)
| 优点 | 缺点 |
|---|---|
| 存储效率高(不预生成信息流) | 读操作慢(需要查询多个用户 + 合并排序) |
| 写操作快(发推文只需写入 tweets 表) | 关注人多时性能差(关注 1000 人 = 1000 次查询) |
| 适合关注数少的用户 |
方案 2:Push 模型(扇出写入)
用户 A 发推文
│
▼
查询用户 A 的所有粉丝
(粉丝 1, 粉丝 2, 粉丝 3...)
│
▼
将推文 ID 写入每个粉丝的信息流
│
▼
粉丝拉取信息流
(直接读取预生成的信息流)
(推模式:写时分发)
| 优点 | 缺点 |
|---|---|
| 读操作极快(一次查询即可) | 写放大问题(名人发推文 = 数百万次写入) |
| 信息流加载 < 50ms | 存储浪费(很多人不怎么看信息流) |
写放大计算:
- 假设用户有 100 万粉丝
- 发 1 条推文 = 写入 100 万条 feed 记录
- 每秒 1000 名人发推文 = 10 亿次写入/秒 → 不可行
方案 3:Hybrid 模型(推荐)
粉丝数 < 10,000 → Push 模型(快读)
粉丝数 >= 10,000 → Pull 模型(省写)
实现:
def publish_tweet(user_id, tweet):
# 存入 tweets 表
tweets_store.write(tweet)
followers = follow_service.get_followers(user_id)
if len(followers) < 10000:
# Push:写入粉丝的信息流
for follower_id in followers:
feed_store.write(follower_id, tweet.id)
else:
# 名人:不 Push,粉丝 Pull 时再查
pass
def get_feed(user_id):
# 1. 从预生成的信息流读取(Push 的部分)
push_feed = feed_store.read(user_id)
# 2. 找出粉丝数 >= 10000 的关注用户
celebrity_followings = get_celebrity_followings(user_id)
# 3. Pull 名人的推文
pull_tweets = []
for celeb_id in celebrity_followings:
pull_tweets.extend(tweets_store.get_recent(celeb_id))
# 4. 合并、排序、去重
return merge_and_sort(push_feed, pull_tweets)
详细请求流程
发推文流程:
- 客户端 POST
/api/v1/tweets - Tweet API 验证用户、存储推文
- 查询粉丝数
- 粉丝 < 10000 → 异步推送到粉丝信息流(Kafka)
- 粉丝 >= 10000 → 只存储,不推送
- 返回成功
拉取信息流流程:
- 客户端 GET
/api/v1/feed?user_id=123 - Feed API 查 Redis 缓存(用户信息流)
- 缓存命中 → 直接返回
- 缓存未命中 → 查 Feed Store
- 合并名人 Pull 的推文
- 写入缓存(TTL 30 秒)
- 返回信息流
第 3 步:扩展性与优化
瓶颈分析
瓶颈 1:信息流读取
- 2 亿 DAU,每人每天拉取 50 次 = 100 亿次读取/天
- 解决: 多级缓存
- L1:API Server 本地缓存(Guava,TTL 10 秒)
- L2:Redis Cluster(TTL 30 秒)
- L3:CDN 缓存(对公开信息流)
瓶颈 2:名人写放大
- 顶级名人(如马斯克)发推文 = 数亿次写入
- 解决: Hybrid 模型 + 名人推文单独缓存
瓶颈 3:合并排序性能
- 关注 50 个名人,每人 20 条推文 = 合并 1000 条
- 解决: 预排序 + 堆合并(N-way merge)
容量估算
| 指标 | 计算 | 结果 |
|---|---|---|
| 推文存储 | 5 亿/天 × 500 字节 × 30 天 | 750TB |
| Feed 存储 | 50% 用户用 Push,每人 200 条 | 1000 亿条记录 |
| Redis 内存 | 1000 亿 × 100 字节 | 10TB(200 个节点) |
缓存策略
请求 → 本地缓存 → Redis → Feed Store → DB
10秒 TTL 30秒 TTL 预生成 原始数据
缓存失效策略:
- 用户发新推文 → 清除自己的 feed 缓存
- 关注/取消关注 → 清除相关 feed 缓存
- 被动失效:TTL 过期自动刷新
面试官常问 Trade-offs 与实战问答
Q1:你选择 Push 还是 Pull 模型?
候选人回答:
“我选择 Hybrid 模型。对于粉丝数 < 10,000 的普通用户,使用 Push 模型,预生成信息流,读取极快。对于粉丝数 >= 10,000 的名人,使用 Pull 模型,避免写放大问题。这样兼顾了读写性能。”
面试官追问:
“为什么选 10,000 这个阈值?”
候选人回答:
“这是基于容量估算的权衡。假设名人每秒发 1 条推文,10,000 粉丝就是每秒 10,000 次写入,Redis 可以承受。如果粉丝数更高,写放大就太严重了。这个阈值可以根据实际负载动态调整。“
Q2:名人发推文导致写放大怎么办?
候选人回答:
“对于名人(粉丝 > 10,000),我不 Push 到粉丝的信息流。粉丝拉取信息流时,先读取预生成的部分,再 Pull 名人的推文。同时我会缓存名人的推文,减少数据库查询。”
面试官追问:
“粉丝拉取名人推文时延迟怎么保证?”
候选人回答:
“名人推文单独缓存(Redis + CDN),缓存命中率很高。即使缓存未命中,名人推文数据库查询也是优化过的(按时间索引),P99 延迟 < 50ms。“
Q3:信息流一致性怎么保证?
候选人回答:
“采用最终一致性。推文发布后,粉丝可能在几秒后看到。对于需要实时性的场景(如用户自己的推文),可以主动轮询或 WebSocket 推送。大多数场景下,几秒延迟是可以接受的。”
面试官追问:
“如果用户投诉没看到推文怎么办?”
候选人回答:
“提供’刷新’按钮,主动拉取最新信息。同时监控信息流延迟指标,如果延迟过高,触发告警和扩容。“
Q4:合并排序性能怎么优化?
候选人回答:
“使用 N-way merge 算法。每个关注用户的推文按时间排序,用最小堆合并。时间复杂度 O(N log K),N 是推文总数,K 是关注人数。同时会预排序和缓存合并结果。”
面试官追问:
“如果关注 1000 个人怎么办?”
候选人回答:
“1000 个关注 × 20 条推文 = 20,000 条推文合并。N-way merge 仍然高效,但会限制返回数量(如最多合并 5000 条)。同时会优先合并活跃用户的推文。“
Q5:缓存失效策略是怎样的?
候选人回答:
“主动失效 + TTL 被动失效。用户发推文、关注/取消关注时,主动清除相关缓存。同时设置 TTL(10-30 秒),确保缓存不会过期太久。对于热点数据,TTL 更短;对于冷数据,TTL 更长。”
面试官追问:
“缓存穿透怎么办?”
候选人回答:
“布隆过滤器拦截不存在的请求。同时缓存空值(TTL 很短),避免重复查询数据库。“
进阶扩展方向
- 算法推荐: 不只按时间排序,按相关性/兴趣排序
- 媒体内容: CDN 分发图片/视频,缩略图预生成
- 无限滚动: 游标分页(cursor-based pagination)
- 多媒体信息流: 图片、视频、链接预览混合排序
常见踩坑点
| # | 踩坑点 | 解决方案 |
|---|---|---|
| 1 | 名人写放大:没有做 Hybrid 模型 | 粉丝 > 10,000 用 Pull |
| 2 | 缓存一致性:发推文后缓存没有及时失效 | 主动清除 + TTL |
| 3 | 合并排序 O(N×M):关注 N 个人,每人 M 条推文 | N-way merge + 限制数量 |
| 4 | 时间同步:分布式系统时间不一致 | 使用单调时钟或 NTP |
总结
Twitter Feed 系统设计考察:
| 能力 | 考察点 |
|---|---|
| 推拉模型 | 核心 trade-off,必须深入理解 |
| 缓存策略 | 多级缓存应对读多写少 |
| 容量估算 | 从业务数据推导系统规模 |
| 名人问题 | 极端场景下的系统设计 |
面试提示:这道题没有标准答案。关键是展示你对 trade-offs 的思考。先提出 Push 模型,再讨论它的写放大问题,最后引出 Hybrid 方案——这个过程比直接给出答案更有价值。
推荐阅读
- 系统设计面试完全指南 — 掌握万能回答框架
- 设计实时聊天系统 — 实时系统设计的另一道经典题目
💡 需要面试辅导?
如果你对准备技术面试感到迷茫,或者想要个性化的面试指导和简历优化,欢迎联系 Interview Coach Pro 获取一对一辅导服务。
👉 联系我们 获取专属面试准备方案