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);
}
存储设计
| 数据 | 存储 | 说明 |
|---|---|---|
| 帖子内容 | MySQL | Post 表,帖子主体数据 |
| 收件箱 | Redis ZSet | inbox:{userId} → 帖子 ID 按时间排序 |
| 发件箱 | Redis ZSet | outbox:{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)降级到数据库查询