Feed 流系统设计
2021年1月,某社交平台发生了服务雪崩:从晚上8点到10点,Feed流接口响应时间从200ms飙升到30秒,CPU打满,数据库被打挂。
事后复盘,原因很简单:一个头部大V发了一条普通生活动态,引发了5000万粉丝的Feed流更新。
5000万条Feed消息,同时写入粉丝的收件箱。同时触发了缓存失效、数据库写入洪峰、消息队列积压。一个"简单"的内容发布,变成了一场灾难。
这是Feed流系统中最经典的热点问题:大V的发布行为会产生远超普通用户的系统压力。
【架构权衡】
Feed流系统的核心矛盾是写入成本 vs 读取成本。推模式写入成本高、拉模式读取成本高。不同平台根据业务特征选择不同模式:微博用混合模式、Twitter早期用拉模式、今日头条用推模式。理解这个trade-off,是设计Feed流系统的基础。
一、Feed流的核心问题 🔴
1.1 推拉模式对比
Feed流系统的两种基本模式:
【推模式(Push)】
- 发布时:主动推送给所有粉丝
- 优点:读取快,用户请求时直接返回
- 缺点:写入成本高,大V压力大
- 适用:粉丝数差异不大的场景
【拉模式(Pull)】
- 读取时:聚合所有关注对象的新内容
- 优点:写入成本低,发布无压力
- 缺点:读取成本高,大用户读取慢
- 适用:粉丝数差异大的场景
【混合模式】
- 大V用拉,普通用户用推
- 优点:平衡读写成本
- 缺点:实现复杂
- 适用:微博、抖音等头部效应明显的场景
1.2 量化指标
Feed流系统的关键数字:
数据规模:
- 日均发布量:1亿条
- 日均读取量:100亿次
- 用户关注数:平均200人,最多5000人
- 大V粉丝数:500万~1亿
性能要求:
- Feed流接口:P99 < 500ms
- 发布接口:P99 < 1s
- 在线用户:1亿
存储估算:
- 单条Feed:约1KB(文本+媒体URL+元数据)
- 1亿条/天 = 100GB/天
- 保留30天 = 3TB
1.3 面试核心问题
面试官:微博的Feed流是推模式还是拉模式?
候选人:混合模式。
普通用户发微博,推送给粉丝的收件箱——这是推模式。
大V发微博,不主动推送,粉丝读取Feed时实时聚合——这是拉模式。
面试官:为什么大V要用拉模式?
候选人:因为推送成本太高。一个大V有5000万粉丝,如果每次发微博都推送,1亿条/天的写入会变成5000万倍的压力。
但拉模式也有问题:粉丝读取Feed时要实时聚合5000万人的最新微博,性能很差。所以Twitter早期(2010年)用的是纯拉模式,用户抱怨很严重。后来才演变成混合模式。
【面试官心理】
Feed流系统的追问方向通常围绕"推拉模式的trade-off"展开。能回答出混合模式的候选人,说明理解了大V问题的本质;能说出具体阈值(如"粉丝超过100万用拉模式")的候选人,说明有量化分析能力。
二、写入模型设计 🔴
2.1 收件箱设计
推模式:每个用户的收件箱
存储结构:
用户A的收件箱 = [所有A关注的用户发布的Feed]
Redis List实现:
key: inbox:{user_id}
value: List[feed_id, feed_id, feed_id...]
问题:
- 100亿条Feed需要巨大存储
- 按时间排序困难
优化:只存Feed ID,Feed内容单独存储
key: inbox:{user_id}
value: List[feed_id, feed_id, feed_id...]
key: feed:{feed_id}
value: Feed{content, author, time...}
2.2 发布流程
public class FeedService {
public void publish(Long userId, String content) {
// Step 1: 创建Feed内容
Feed feed = new Feed();
feed.setId(snowflakeId);
feed.setUserId(userId);
feed.setContent(content);
feed.setCreateTime(new Date());
feedDAO.insert(feed);
// Step 2: 判断用户类型
User user = userDAO.selectById(userId);
int fanCount = user.getFanCount();
if (fanCount < 10000) {
// 普通用户:推送到粉丝收件箱
pushToFans(userId, feed.getId());
} else {
// 大V:只写自己的Timeline
writeToOwnTimeline(userId, feed.getId());
// 异步更新热点池
asyncUpdateHotPool(userId, feed.getId());
}
}
private void pushToFans(Long userId, Long feedId) {
// 获取粉丝列表
List<Long> fans = fanDAO.selectFans(userId);
// 批量写入收件箱
for (List<Long> batch : Lists.partition(fans, 1000)) {
redisTemplate.opsForList().rightPushAll(
batch.stream().map(fanId -> "inbox:" + fanId).toArray(String[]::new),
feedId
);
}
}
}
2.3 大V处理策略
大V阈值设计:
阈值:粉丝数 > 100万
触发策略:
1. 粉丝数超过阈值后,切换为拉模式
2. 切换过程:灰度切换,先对10%粉丝用拉模式,观察效果
3. 热度衰减:发布超过24小时,切换为推模式
大V拉模式优化:
1. 热门大V的Feed预加载到CDN
2. 粉丝读取时优先查CDN
3. CDN miss时查Redis缓存
4. 缓存miss时查数据库
CDN预热:
- 大V发布后,立即将Feed ID写入CDN
- 缓存TTL设置为24小时
- 过期前主动刷新
三、存储设计 🟡
3.1 多级存储
Feed流存储架构:
第一层:Redis List(热数据,7天内)
- 在线用户的收件箱
- 命中率 > 90%
- TTL = 7天
第二层:MySQL分库分表(全部数据)
- 所有历史Feed
- 按 feed_id 取模分表
- 分8库64表
第三层:HBase/ES(归档数据)
- 超过30天的Feed
- 按时间范围查询
- 支持复杂检索
第四层:OSS(媒体文件)
- 图片、视频等媒体内容
- CDN加速
3.2 拉取优化
拉模式读取流程:
用户读取Feed流:
1. 获取用户关注列表(如200人)
2. 获取每个关注者最近N条Feed
3. 合并并按时间排序
4. 分页返回
优化:
1. 并行拉取:200个关注者的Feed并行查询
2. 缓存热点:热门用户(如大V)的Feed缓存到Redis
3. 限制范围:每个用户最多取最近100条
4. 结果缓存:用户Feed流结果缓存5分钟
3.3 分页设计
public class FeedPullService {
public FeedList pull(Long userId, Long lastFeedId, int pageSize) {
// Step 1: 获取关注列表
List<Long> following = followDAO.selectFollowing(userId);
// Step 2: 并行拉取关注者的Feed
List<Feed> allFeeds = new ArrayList<>();
ExecutorService executor = Executors.newFixedThreadPool(20);
CountDownLatch latch = new CountDownLatch(following.size());
for (Long authorId : following) {
executor.submit(() -> {
try {
List<Feed> feeds = pullAuthorFeed(authorId, lastFeedId, 20);
synchronized (allFeeds) {
allFeeds.addAll(feeds);
}
} finally {
latch.countDown();
}
});
}
latch.await();
executor.shutdown();
// Step 3: 合并排序
allFeeds.sort((a, b) -> b.getCreateTime().compareTo(a.getCreateTime()));
// Step 4: 分页返回
return new FeedList(allFeeds.stream()
.limit(pageSize)
.collect(Collectors.toList()));
}
}
四、热数据处理 🟡
4.1 热点检测
热点Feed判定:
维度1:发布后5分钟内的互动量
- 点赞数 > 10000
- 评论数 > 1000
- 转发数 > 500
维度2:用户画像
- 认证用户
- 粉丝数 > 100万
- 高活跃度账号
维度3:内容特征
- 包含热门话题
- 包含@大V
- 包含热门关键词
触发热点处理:
1. 热点Feed写入热点池(Redis Set)
2. 热点Feed预加载到CDN
3. 热点Feed写入搜索索引
4. 通知推荐系统
4.2 缓存策略
Feed缓存架构:
1. 用户收件箱缓存
- key: inbox:{user_id}
- value: List[feed_id...]
- TTL: 7天
- 更新:发布时写,读取时续期
2. Feed内容缓存
- key: feed:{feed_id}
- value: Feed{content, author, media...}
- TTL: 7天
- 更新:发布时写入,编辑时失效
3. 热点Feed缓存
- key: hot:feed:list
- value: List[feed_id...]
- TTL: 24小时
- 更新:热点检测触发
4. Feed流结果缓存
- key: feed:stream:{user_id}
- value: List[Feed...]
- TTL: 5分钟
- 更新:每次拉取刷新
五、生产避坑 🟡
5.1 Feed流系统的五大坑
坑1:收件箱膨胀
问题:用户关注人数过多,收件箱数据量爆炸
场景:用户关注了5000人,每人每天发10条Feed,收件箱每天增加5万条
影响:Redis内存爆炸,读取性能下降
解决方案:
- 收件箱容量限制:最多保留1000条
- 老旧Feed归档:超过7天的Feed迁移到MySQL
- 拉模式兜底:关注人数超过阈值后,切换到拉模式
坑2:热点大V雪崩
问题:大V发微博时,推送压力导致系统雪崩
场景:粉丝5000万的大V发微博
影响:推送队列积压,数据库被打挂
解决方案:
- 大V用拉模式:超过100万粉丝自动切换
- 异步推送:用MQ削峰,不同步推送
- 灰度切换:先推10%粉丝,观察效果
坑3:Feed重复消费
问题:同一个Feed被多次推送到收件箱
场景:推送过程中用户取消关注又重新关注
影响:用户看到重复Feed
解决方案:
- Feed ID去重:推送前检查是否已存在
- 幂等写入:收件箱用Set而非List
- 定时清理:每天凌晨清理重复Feed
坑4:缓存击穿
问题:热门Feed缓存过期,瞬间大量请求穿透到数据库
场景:热点Feed的缓存TTL=7天,过期时正好是发布后7天
影响:数据库被打挂
解决方案:
- 热点Feed永不过期:主动续期
- 随机TTL:TTL = 7天 + random(0, 1天)
- 互斥锁:只有一个线程去加载缓存
坑5:排序不稳定
问题:用户刷新Feed流,看到的内容顺序不一致
场景:分布式环境下,多个分片返回的数据合并顺序不同
影响:用户体验割裂
解决方案:
- 按时间戳排序:统一使用create_time
- 分页维度统一:last_feed_id + 游标分页
- 结果缓存:用户Feed流结果缓存5分钟
【架构权衡】
Feed流系统的设计哲学是读多写少、热点集中。90%的访问集中在10%的热数据上,所以缓存是关键。但缓存不是万能的——大V的热点问题、收件箱膨胀问题,都需要从架构层面解决,而不是单纯加缓存。
六、真实面试回放 🟡
面试官:设计一个今日头条的Feed流系统,日均发布1亿条,日均读取100亿次,怎么设计?
候选人(架构师老张):先分析特征:
今日头条是推荐Feed,和微博社交Feed不同:
- 推荐Feed:系统决定你看什么,不需要关注关系
- 内容来源:推荐算法+编辑精选+用户互动
- 读取模式:拉模式为主,用户每次刷新都重新计算
架构设计:
-
内容存储:用MySQL分库分表存Feed内容,ElasticSearch存索引
-
用户Profile:用HBase存用户特征向量
-
推荐引擎:读取用户特征,计算最相关的内容
-
缓存层:Redis缓存热门内容、用户特征
-
写入:Feed发布后写MySQL,推送消息到推荐队列
面试官:100亿次读取/天,峰值QPS多少?怎么扛?
老张:峰值因子取5,峰值QPS = 100亿 × 5 / 86400 ≈ 58万
扛法:
-
CDN缓存:热门内容缓存到CDN,命中率80%,回源QPS = 11.6万
-
Redis缓存:CDN miss的内容查Redis,命中率90%,回源QPS = 1.16万
-
分库分表:MySQL分128表,单表扛100 QPS,需要116台...不对,重算
MySQL单表1000 QPS,1.16万需要12台就够了,加上主从和高可用,24台
面试官:推荐算法怎么决定用户看什么?
老张:协同过滤 + 内容特征 + 实时特征
协同过滤:你喜欢的内容,和你相似的人也喜欢
内容特征:你之前看过类似的内容
实时特征:当前热点、所在地区、当天时间
三种特征加权融合,实时更新用户画像
【面试官手记】
老张这场面试的亮点:
-
区分了推荐Feed和社交Feed:不是所有Feed流都是微博模式
-
量化分析到位:峰值QPS计算、存储估算
-
推荐系统基础:协同过滤、内容特征、实时特征
这场面试属于P7级别,能区分不同类型Feed流的候选人,说明有全局视野。
追问方向:会问"怎么冷启动新用户"和"推荐结果怎么评估",这两个问题考验候选人对推荐系统的理解。
Feed流系统设计的核心是推拉模式的trade-off,记住三个要点:
- 推模式:写多读少,适合粉丝数均匀的场景
- 拉模式:写少读多,适合热点集中的场景
- 混合模式:大V用拉、普通用户用推
理解这个trade-off,你就能设计出适合任何场景的Feed流系统。