i007.cc

i007.cc

优先队列-降维打击

05.价值资料

设计新闻 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 时:

  1. 拿到 post_id 列表
  2. 批量 MGET 帖子内容(也在 Redis 缓存)
  3. 一次返回
帖子内容存储
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,内容走缓存批量读,把随机读变成批量读,延迟从毫秒降到微秒。”

发表回复