设计新闻 Feed / 朋友圈(News Feed System)
这道题的核心难点不在存储,而在如何高效地把内容分发给每个用户。
第一步:明确需求与规模
功能需求:
- 发布动态(文字、图片、视频)
- 查看 Feed 流(关注的人按时间倒序)
- 点赞、评论、转发
规模估算:
日活用户(DAU):1亿 平均关注数:200人 每人每天发帖:0.1条 → 写 QPS ≈ 120/s 每人每天刷 Feed:10次 → 读 QPS ≈ 12,000/s(读远多于写)
第二步:核心问题——Feed 如何生成?
这是整道题最关键的设计决策,有三种方案:
方案一:拉模式(Pull / Fan-out on Read)
用户刷 Feed 时,实时去拉取所有关注人的最新帖子,归并排序后返回。
用户A刷Feed → 查询 A 关注的 200 人 → 分别读取每人的最新帖子 → 归并排序,取 Top 20 → 返回
优点: 写入极简,发帖只需写一条记录。
缺点: 读时延高——关注 200 人就要并发查 200 次,归并代价大;用户刷 Feed 体验差。
方案二:推模式(Push / Fan-out on Write)⭐
用户发帖时,主动推送到所有粉丝的 Feed 队列里,粉丝刷 Feed 时直接读自己的队列。
用户A发一条帖子 → 查询 A 的所有粉丝列表(假设1000人) → 异步写入 1000 个粉丝的 Feed 队列 → 粉丝刷Feed时直接读自己的队列,O(1)
Feed队列(Redis Sorted Set):
Key: feed:{user_id}
Member: post_id
Score: 发帖时间戳(用于排序)
优点: 读极快,直接取队列 top N。
缺点: 大 V 问题——微博明星粉丝 5000 万,发一条帖子要写 5000 万次,写入风暴。
方案三:推拉结合(Hybrid)⭐⭐ 工业界主流
按粉丝量区分策略:
普通用户(粉丝 < 阈值,如1万)→ 推模式,写时 fan-out 大 V 用户(粉丝 > 阈值) → 拉模式,读时实时拉取
用户刷 Feed 时:
1. 直接读自己的 Feed 队列(包含普通好友的帖子) 2. 再拉取关注的大V的最新帖子 3. 两部分归并后返回
大 V 帖子数量少(刷一次最多拉几十个大 V),归并代价可控;普通人用推模式读极快。两全其美。
第三步:整体架构
┌─────────────────┐
用户发帖 │ Post Service │
──────────────────→ │ 写帖子到 PostDB │
└────────┬────────┘
│ 发 MQ 消息
┌────────▼────────┐
│ Fan-out Worker │ (异步)
│ 消费消息,推送 │
└────────┬────────┘
│
┌──────────────┴──────────────┐
▼ ▼
普通用户粉丝的 Feed Queue 大V不推,跳过
Redis Sorted Set (feed:{uid})
用户刷 Feed
──────────────────→ Feed Service
│
┌─────────┴──────────┐
▼ ▼
读自己的 Feed Queue 拉取关注大V的帖子
(Redis, 极快) (PostDB, 少量)
└─────────┬──────────┘
▼
归并排序
▼
返回 Top N
第四步:关键细节
Feed 队列的设计
用 Redis Sorted Set,score 是时间戳,天然有序:
ZADD feed:user_A 1720000001 post_123 ZADD feed:user_A 1720000002 post_456 # 取最新 20 条 ZREVRANGE feed:user_A 0 19 WITHSCORES
队列只存 post_id,帖子内容另存。读 Feed 时:
- 拿到 post_id 列表
- 批量
MGET帖子内容(也在 Redis 缓存) - 一次返回
帖子内容存储
PostDB(MySQL): id, user_id, content, media_urls, created_at
Post Cache(Redis): key=post:{id}, value=帖子JSON, TTL=7天
媒体文件: CDN + 对象存储(S3/OSS)
翻页(Pagination)
不能用 OFFSET,因为新帖子不断插入,翻页会漏内容或重复。
用游标分页(Cursor-based):
第一页: 返回 Top 20,同时返回最后一条的 score(时间戳)作为 cursor 下一页: ZREVRANGEBYSCORE feed:uid (cursor -inf LIMIT 0 20
第五步:其他细节
在线状态优化: 推模式可以跳过长期不活跃的用户(比如 30 天未登录),等他们上线时再用拉模式补全,节省大量无效写入。
点赞/评论计数: 不能每次都 COUNT(*),用 Redis 计数器:
INCR like_count:{post_id}
异步批量回写 MySQL。
热点帖子缓存: 大 V 的帖子被大量拉取,单独做热点缓存,TTL 短但命中率极高。
面试表达框架
1. 需求 + 估算(3min) → 确认读多写少,规模量级 2. 核心决策(8min) → 推/拉/混合,讲清楚权衡 3. 架构图(5min) → 发帖链路 + 读Feed链路 4. 存储设计(5min) → Redis Sorted Set + PostDB + CDN 5. 深挖(5min) → 大V问题、游标翻页、计数器
面试官最想听的两句话:
“普通用户用推模式保证读性能,大 V 用拉模式避免写风暴,阈值根据实际压测调整。”
“Feed 队列只存 post_id,内容走缓存批量读,把随机读变成批量读,延迟从毫秒降到微秒。”
