跳到主要内容

Feed 信息流系统设计

问题

如何设计微博、朋友圈这样的 Feed 信息流系统?

答案

核心需求

  • 用户发布内容 → 粉丝看到(写扩散/读扩散)
  • Timeline 按时间排序展示
  • 高性能读取,支持下拉刷新和无限加载

推拉模式对比

模式写入时机读取时机适用场景
推模式发帖时写入所有粉丝收件箱直接读收件箱粉丝少(如朋友圈)
拉模式不做额外写入实时聚合关注人内容关注少、粉丝多
推拉结合普通用户推,大V不推读时再拉大V内容合并微博等社交平台

推拉结合方案(生产推荐)

发帖 —— 推模式(普通用户)+ 拉模式(大V)
public void publish(Post post) {
// 1. 存储帖子到 Post 表
postMapper.insert(post);

// 2. 判断是否为大V(粉丝 > 10万)
long followerCount = userService.getFollowerCount(post.getUserId());

if (followerCount > 100_000) {
// 大V:不扩散,只更新大V发件箱(拉模式)
redisTemplate.opsForZSet().add(
"outbox:" + post.getUserId(),
String.valueOf(post.getId()),
post.getCreateTime()
);
} else {
// 普通用户:写入所有粉丝收件箱(推模式)
List<Long> followerIds = userService.getFollowerIds(post.getUserId());
for (Long followerId : followerIds) {
redisTemplate.opsForZSet().add(
"inbox:" + followerId,
String.valueOf(post.getId()),
post.getCreateTime()
);
}
}
}
读 Feed —— 合并收件箱 + 大V发件箱
public List<Post> getFeed(long userId, long cursor, int pageSize) {
// 1. 从收件箱拿推送过来的帖子 ID(ZSet 按时间倒序)
Set<String> inboxIds = redisTemplate.opsForZSet()
.reverseRangeByScore("inbox:" + userId, 0, cursor, 0, pageSize);

// 2. 拉取关注的大V最新内容
List<Long> followedBigVs = userService.getFollowedBigVs(userId);
Set<String> bigVIds = new TreeSet<>();
for (Long bigVId : followedBigVs) {
bigVIds.addAll(redisTemplate.opsForZSet()
.reverseRangeByScore("outbox:" + bigVId, 0, cursor, 0, pageSize));
}

// 3. 合并、排序、截取
List<Long> allPostIds = mergeAndSort(inboxIds, bigVIds, pageSize);

// 4. 批量查询帖子详情
return postMapper.findByIds(allPostIds);
}

存储设计

数据存储说明
帖子内容MySQLPost 表,帖子主体数据
收件箱Redis ZSetinbox:{userId} → 帖子 ID 按时间排序
发件箱Redis ZSetoutbox:{userId} → 大V帖子,读时拉取
关注关系MySQL + Redis关注列表、粉丝列表
收件箱膨胀

每个用户的收件箱只保留最近 N 条(如 1000),超出的从数据库回捞。定期用 ZREMRANGEBYRANK 清理。


常见面试问题

Q1: 大V发帖粉丝太多怎么办?

答案

大V不走推模式,改为拉模式。粉丝刷 Feed 时实时拉取大V发件箱合并。阈值通常设为粉丝数 > 10 万。

Q2: 如何实现 Feed 分页?

答案

游标分页(Cursor-based),不用 OFFSET

  • 客户端传上一页最后一条的时间戳作为 cursor
  • ZREVRANGEBYSCORE inbox:userId cursor-1 0 LIMIT 0 pageSize
  • 避免了 OFFSET 跳过大量数据的性能问题

Q3: 用户取关后 Feed 怎么处理?

答案

  • 如果是推模式:异步从粉丝收件箱删除该用户的帖子
  • 也可以不删,读取 Feed 时过滤掉已取关用户的帖子(软过滤,更简单)

Q4: Feed 流的缓存策略?

答案

  • 收件箱用 Redis ZSet,天然有序,热数据常驻内存
  • 帖子详情用 Redis Hash 或本地缓存,减少 MySQL 查询
  • 冷数据(如一周前的 Feed)降级到数据库查询

相关链接