📖 开场:刷朋友圈
想象你在刷微信朋友圈 📱:
简单实现:
打开朋友圈
↓
查询我关注的100个好友
↓
从每个好友那里拉取最新10条动态
↓
合并 + 排序 + 返回
问题:
- 查询100次数据库 → 太慢!❌
- 好友发动态 → 我看不到(需要我打开才能看到)❌
现实场景:
微博:
- 用户:5亿
- 关注数:平均200人
- 明星粉丝:5000万
挑战:
- 如何实时推送?⏰
- 如何支持高并发?💥
- 明星发动态 → 5000万人收到?😱
这就是Feed流系统的挑战!
🤔 核心概念
Feed流的两种模式
推模式(Write Fanout)📤
用户A发动态
↓
推送给所有粉丝的收件箱
↓
粉丝打开Feed流 → 直接读收件箱
例如:
A发动态 → 推送给粉丝[B, C, D]的收件箱
B打开朋友圈 → 读取自己的收件箱(很快)✅
优点:
- 读快(直接读收件箱)✅
缺点:
- 写慢(明星5000万粉丝,要写5000万次)❌
- 存储大(每个人一个收件箱)❌
拉模式(Read Fanout)📥
用户A发动态
↓
只保存动态,不推送
↓
粉丝B打开Feed流 → 实时拉取关注的人的最新动态
例如:
A发动态 → 保存
B打开朋友圈 → 实时拉取关注的[A, C, D, E]的最新动态
↓
合并 + 排序 + 返回
优点:
- 写快(只写一次)✅
缺点:
- 读慢(需要聚合多个人的动态)❌
推拉结合(最佳实践)⭐⭐⭐
普通用户:推模式
- 粉丝少(<1000)
- 发动态 → 推送给粉丝收件箱
明星用户:拉模式
- 粉丝多(>1000)
- 发动态 → 只保存
- 粉丝打开 → 实时拉取
读取Feed流:
1. 从收件箱读取(推模式的动态)
2. 实时拉取明星用户的最新动态(拉模式)
3. 合并 + 排序 + 返回
优点:
- 普通用户读快 ✅
- 明星用户写快 ✅
- 兼顾性能和体验 ✅
🎯 架构设计
整体架构
┌─────────────────────────────────────────┐
│ 客户端 │
│ - 发布动态 │
│ - 刷新Feed流 │
│ - 点赞、评论 │
└─────────────┬───────────────────────────┘
│
↓
┌─────────────────────────────────────────┐
│ 应用服务器 │
│ │
│ - Feed发布服务 │
│ - Feed读取服务 │
│ - 推拉判断逻辑 │
└───────┬─────────────┬───────────────────┘
│ │
↓ ↓
┌──────────────┐ ┌──────────────────────┐
│ Redis │ │ MySQL │
│ │ │ │
│ - 收件箱 │ │ - 动态表 │
│ - 关注关系 │ │ - 用户表 │
│ - 热点数据 │ │ - 关注关系表 │
└──────────────┘ └──────────────────────┘
数据模型设计
MySQL表设计
-- ⭐ 动态表
CREATE TABLE feed (
id BIGINT PRIMARY KEY,
user_id BIGINT NOT NULL COMMENT '用户ID',
content TEXT COMMENT '动态内容',
images VARCHAR(1000) COMMENT '图片URL(逗号分隔)',
create_time DATETIME NOT NULL,
INDEX idx_user_id_time (user_id, create_time DESC) -- 查询某用户的动态
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- ⭐ 互动表(点赞、评论)
CREATE TABLE feed_interaction (
id BIGINT PRIMARY KEY,
feed_id BIGINT NOT NULL COMMENT '动态ID',
user_id BIGINT NOT NULL COMMENT '用户ID',
type TINYINT NOT NULL COMMENT '类型:1-点赞,2-评论',
content TEXT COMMENT '评论内容',
create_time DATETIME NOT NULL,
INDEX idx_feed_id (feed_id),
INDEX idx_user_id (user_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- ⭐ 关注关系表
CREATE TABLE user_follow (
id BIGINT PRIMARY KEY,
user_id BIGINT NOT NULL COMMENT '用户ID',
follow_id BIGINT NOT NULL COMMENT '关注的用户ID',
create_time DATETIME NOT NULL,
UNIQUE KEY uk_user_follow (user_id, follow_id),
INDEX idx_follow_id (follow_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
Redis数据结构
1. 收件箱(推模式):
key: inbox:{userId}
value: List<feedId>(按时间倒序)
例如:
inbox:1001 → [10005, 10003, 10001]
2. 用户动态(拉模式):
key: user:feed:{userId}
value: List<feedId>
例如:
user:feed:2001 → [20005, 20003, 20001]
3. 关注列表:
key: follow:{userId}
value: Set<followId>
例如:
follow:1001 → {2001, 2002, 2003}
4. 粉丝数:
key: fans:count:{userId}
value: 粉丝数量
例如:
fans:count:2001 → 5000000(500万粉丝)
核心功能实现
1️⃣ 发布动态
@Service
@Slf4j
public class FeedPublishService {
@Autowired
private FeedDao feedDao;
@Autowired
private UserFollowDao followDao;
@Autowired
private StringRedisTemplate redisTemplate;
@Autowired
private FeedPushService pushService;
private static final int STAR_THRESHOLD = 1000; // 明星用户阈值
/**
* ⭐ 发布动态
*/
@Transactional
public Feed publishFeed(Long userId, String content, List<String> images) {
// 1. 保存动态到数据库
Feed feed = new Feed();
feed.setId(idGenerator.generateId());
feed.setUserId(userId);
feed.setContent(content);
feed.setImages(String.join(",", images));
feed.setCreateTime(new Date());
feedDao.insert(feed);
log.info("动态保存成功: feedId={}, userId={}", feed.getId(), userId);
// 2. ⭐ 推送到Redis(用户动态列表)
String userFeedKey = "user:feed:" + userId;
redisTemplate.opsForList().leftPush(userFeedKey, feed.getId().toString());
// 只保留最新100条
redisTemplate.opsForList().trim(userFeedKey, 0, 99);
// 3. ⭐ 判断是否是明星用户
Long fansCount = getFansCount(userId);
if (fansCount < STAR_THRESHOLD) {
// ⭐ 普通用户:推模式
pushToFans(userId, feed.getId());
log.info("推模式推送: userId={}, fansCount={}", userId, fansCount);
} else {
// ⭐ 明星用户:拉模式(不推送)
log.info("明星用户,使用拉模式: userId={}, fansCount={}", userId, fansCount);
}
return feed;
}
/**
* ⭐ 推送到粉丝收件箱(推模式)
*/
private void pushToFans(Long userId, Long feedId) {
// 1. 查询粉丝列表(从Redis)
Set<String> fans = redisTemplate.opsForSet().members("fans:" + userId);
if (fans == null || fans.isEmpty()) {
// Redis中没有,从数据库加载
List<UserFollow> followList = followDao.findFansByUserId(userId);
fans = followList.stream()
.map(f -> f.getUserId().toString())
.collect(Collectors.toSet());
// 缓存到Redis
if (!fans.isEmpty()) {
redisTemplate.opsForSet().add("fans:" + userId, fans.toArray(new String[0]));
}
}
// 2. ⭐ 推送到每个粉丝的收件箱
for (String fanId : fans) {
String inboxKey = "inbox:" + fanId;
// ⭐ 添加到收件箱(List,头部插入)
redisTemplate.opsForList().leftPush(inboxKey, feedId.toString());
// 只保留最新100条
redisTemplate.opsForList().trim(inboxKey, 0, 99);
}
log.info("推送到粉丝收件箱: userId={}, fansCount={}", userId, fans.size());
}
/**
* 获取粉丝数
*/
private Long getFansCount(Long userId) {
String key = "fans:count:" + userId;
String count = redisTemplate.opsForValue().get(key);
if (count != null) {
return Long.parseLong(count);
}
// 从数据库查询
Long fansCount = followDao.countFansByUserId(userId);
// 缓存
redisTemplate.opsForValue().set(key, String.valueOf(fansCount), 1, TimeUnit.HOURS);
return fansCount;
}
}
2️⃣ 读取Feed流
@Service
@Slf4j
public class FeedReadService {
@Autowired
private StringRedisTemplate redisTemplate;
@Autowired
private FeedDao feedDao;
@Autowired
private UserFollowDao followDao;
private static final int STAR_THRESHOLD = 1000;
/**
* ⭐ 读取Feed流(推拉结合)
*/
public List<Feed> getTimeline(Long userId, Long lastFeedId, int size) {
List<Feed> feeds = new ArrayList<>();
// 1. ⭐ 从收件箱读取(推模式的动态)
List<String> inboxFeedIds = getInboxFeeds(userId, lastFeedId, size);
if (!inboxFeedIds.isEmpty()) {
List<Feed> inboxFeeds = feedDao.findByIds(inboxFeedIds);
feeds.addAll(inboxFeeds);
}
// 2. ⭐ 拉取明星用户的动态(拉模式)
List<Long> starFollows = getStarFollows(userId);
if (!starFollows.isEmpty()) {
List<Feed> starFeeds = pullStarFeeds(starFollows, lastFeedId, size);
feeds.addAll(starFeeds);
}
// 3. ⭐ 合并 + 去重 + 排序
List<Feed> result = feeds.stream()
.distinct() // 去重
.sorted(Comparator.comparing(Feed::getCreateTime).reversed()) // 按时间倒序
.limit(size) // 限制数量
.collect(Collectors.toList());
log.info("读取Feed流: userId={}, count={}", userId, result.size());
return result;
}
/**
* ⭐ 从收件箱读取(推模式)
*/
private List<String> getInboxFeeds(Long userId, Long lastFeedId, int size) {
String inboxKey = "inbox:" + userId;
if (lastFeedId == null) {
// 第一页,从头读取
return redisTemplate.opsForList().range(inboxKey, 0, size - 1);
} else {
// 分页,从lastFeedId之后读取
Long index = redisTemplate.opsForList().indexOf(inboxKey, lastFeedId.toString());
if (index != null && index >= 0) {
return redisTemplate.opsForList().range(inboxKey, index + 1, index + size);
}
}
return Collections.emptyList();
}
/**
* ⭐ 获取明星用户列表
*/
private List<Long> getStarFollows(Long userId) {
// 查询我关注的人
Set<String> follows = redisTemplate.opsForSet().members("follow:" + userId);
if (follows == null || follows.isEmpty()) {
return Collections.emptyList();
}
// 过滤出明星用户(粉丝数>1000)
List<Long> starFollows = new ArrayList<>();
for (String followId : follows) {
Long fansCount = getFansCount(Long.parseLong(followId));
if (fansCount >= STAR_THRESHOLD) {
starFollows.add(Long.parseLong(followId));
}
}
return starFollows;
}
/**
* ⭐ 拉取明星用户的动态(拉模式)
*/
private List<Feed> pullStarFeeds(List<Long> starUserIds, Long lastFeedId, int size) {
List<Feed> starFeeds = new ArrayList<>();
for (Long starUserId : starUserIds) {
// 从Redis读取明星用户的最新动态
String userFeedKey = "user:feed:" + starUserId;
List<String> feedIds = redisTemplate.opsForList().range(userFeedKey, 0, size - 1);
if (feedIds != null && !feedIds.isEmpty()) {
List<Feed> feeds = feedDao.findByIds(feedIds);
starFeeds.addAll(feeds);
}
}
return starFeeds;
}
private Long getFansCount(Long userId) {
String key = "fans:count:" + userId;
String count = redisTemplate.opsForValue().get(key);
return count != null ? Long.parseLong(count) : 0L;
}
}
3️⃣ 点赞和评论
@Service
@Slf4j
public class FeedInteractionService {
@Autowired
private FeedInteractionDao interactionDao;
@Autowired
private StringRedisTemplate redisTemplate;
/**
* ⭐ 点赞
*/
public boolean like(Long feedId, Long userId) {
// 1. 检查是否已点赞
String likeKey = "like:" + feedId;
Boolean isMember = redisTemplate.opsForSet().isMember(likeKey, userId.toString());
if (Boolean.TRUE.equals(isMember)) {
log.warn("已经点赞过了: feedId={}, userId={}", feedId, userId);
return false;
}
// 2. 保存到数据库
FeedInteraction interaction = new FeedInteraction();
interaction.setId(idGenerator.generateId());
interaction.setFeedId(feedId);
interaction.setUserId(userId);
interaction.setType(1); // 1-点赞
interaction.setCreateTime(new Date());
interactionDao.insert(interaction);
// 3. ⭐ 添加到Redis Set
redisTemplate.opsForSet().add(likeKey, userId.toString());
// 4. ⭐ 点赞数+1
String countKey = "like:count:" + feedId;
redisTemplate.opsForValue().increment(countKey);
log.info("点赞成功: feedId={}, userId={}", feedId, userId);
return true;
}
/**
* ⭐ 取消点赞
*/
public boolean unlike(Long feedId, Long userId) {
// 1. 检查是否已点赞
String likeKey = "like:" + feedId;
Boolean isMember = redisTemplate.opsForSet().isMember(likeKey, userId.toString());
if (Boolean.FALSE.equals(isMember)) {
log.warn("未点赞: feedId={}, userId={}", feedId, userId);
return false;
}
// 2. 删除数据库记录
interactionDao.deleteByFeedIdAndUserIdAndType(feedId, userId, 1);
// 3. ⭐ 从Redis Set移除
redisTemplate.opsForSet().remove(likeKey, userId.toString());
// 4. ⭐ 点赞数-1
String countKey = "like:count:" + feedId;
redisTemplate.opsForValue().decrement(countKey);
log.info("取消点赞成功: feedId={}, userId={}", feedId, userId);
return true;
}
/**
* ⭐ 评论
*/
public FeedInteraction comment(Long feedId, Long userId, String content) {
// 1. 保存到数据库
FeedInteraction interaction = new FeedInteraction();
interaction.setId(idGenerator.generateId());
interaction.setFeedId(feedId);
interaction.setUserId(userId);
interaction.setType(2); // 2-评论
interaction.setContent(content);
interaction.setCreateTime(new Date());
interactionDao.insert(interaction);
// 2. ⭐ 评论数+1
String countKey = "comment:count:" + feedId;
redisTemplate.opsForValue().increment(countKey);
log.info("评论成功: feedId={}, userId={}", feedId, userId);
return interaction;
}
/**
* 查询点赞列表
*/
public List<Long> getLikeUsers(Long feedId) {
String likeKey = "like:" + feedId;
Set<String> userIds = redisTemplate.opsForSet().members(likeKey);
if (userIds == null || userIds.isEmpty()) {
return Collections.emptyList();
}
return userIds.stream()
.map(Long::parseLong)
.collect(Collectors.toList());
}
/**
* 查询评论列表
*/
public List<FeedInteraction> getComments(Long feedId, int page, int size) {
int offset = (page - 1) * size;
return interactionDao.findByFeedIdAndType(feedId, 2, offset, size);
}
}
🎯 高级功能
1️⃣ 热点数据缓存
@Service
public class FeedCacheService {
@Autowired
private StringRedisTemplate redisTemplate;
/**
* ⭐ 缓存热点动态
*/
public void cacheHotFeed(Feed feed) {
String key = "feed:hot:" + feed.getId();
// 转换为JSON
String json = JSON.toJSONString(feed);
// 缓存1小时
redisTemplate.opsForValue().set(key, json, 1, TimeUnit.HOURS);
}
/**
* 获取热点动态
*/
public Feed getHotFeed(Long feedId) {
String key = "feed:hot:" + feedId;
String json = redisTemplate.opsForValue().get(key);
if (json != null) {
return JSON.parseObject(json, Feed.class);
}
return null;
}
}
2️⃣ 消息扇出优化
@Service
public class FeedFanoutService {
@Autowired
private ThreadPoolExecutor executor;
/**
* ⭐ 异步扇出(提高性能)
*/
public void asyncFanout(Long userId, Long feedId, List<Long> fanIds) {
// 分批处理(每批1000个)
int batchSize = 1000;
for (int i = 0; i < fanIds.size(); i += batchSize) {
int end = Math.min(i + batchSize, fanIds.size());
List<Long> batch = fanIds.subList(i, end);
// ⭐ 异步推送
executor.execute(() -> {
pushToBatch(feedId, batch);
});
}
}
private void pushToBatch(Long feedId, List<Long> fanIds) {
for (Long fanId : fanIds) {
String inboxKey = "inbox:" + fanId;
redisTemplate.opsForList().leftPush(inboxKey, feedId.toString());
}
}
}
3️⃣ Feed流分页
/**
* ⭐ 基于lastFeedId的分页
*/
public List<Feed> getTimelinePage(Long userId, Long lastFeedId, int size) {
List<Feed> feeds = getTimeline(userId, lastFeedId, size);
// 返回结果中最后一个feedId作为下一页的lastFeedId
if (!feeds.isEmpty()) {
Feed lastFeed = feeds.get(feeds.size() - 1);
log.info("下一页lastFeedId: {}", lastFeed.getId());
}
return feeds;
}
📊 架构总结
Feed流系统架构(推拉结合)
发布动态:
┌─────────────────────────────────────┐
│ 用户发动态 │
│ ↓ │
│ 判断是否是明星用户 │
│ ├─ 普通用户(粉丝<1000)→ 推模式 │
│ │ └─ 推送到粉丝收件箱 │
│ └─ 明星用户(粉丝>1000)→ 拉模式 │
│ └─ 只保存,不推送 │
└─────────────────────────────────────┘
读取Feed流:
┌─────────────────────────────────────┐
│ 用户刷Feed流 │
│ ↓ │
│ 1. 从收件箱读取(推模式的动态) │
│ 2. 实时拉取明星用户的动态(拉模式)│
│ 3. 合并 + 去重 + 排序 │
│ 4. 返回Top N │
└─────────────────────────────────────┘
兼顾性能和体验!✅
🎓 面试题速答
Q1: Feed流的推模式和拉模式有什么区别?
A: 推模式(Write Fanout):
- 发动态时,推送到所有粉丝收件箱
- 读快,写慢
- 适合粉丝少的普通用户
拉模式(Read Fanout):
- 发动态时,只保存
- 读取时,实时拉取关注的人的动态
- 写快,读慢
- 适合粉丝多的明星用户
推拉结合(最佳):
- 普通用户推,明星用户拉
- 兼顾性能和体验
Q2: 如何处理明星用户的动态?
A: 拉模式 + 缓存:
1. 发动态:只保存,不推送
2. 缓存到Redis:
key: user:feed:{userId}
value: List<feedId>(最新100条)
3. 粉丝读取:
- 实时拉取明星用户的最新动态
- 从Redis读取(很快)
优点:
- 发动态快(不需要推送5000万次)
- 读取也快(Redis缓存)
Q3: 如何实现Feed流的分页?
A: 基于lastFeedId的分页:
// 第一页
List<Feed> page1 = getTimeline(userId, null, 10);
Long lastFeedId = page1.get(9).getId(); // 第10条的ID
// 第二页
List<Feed> page2 = getTimeline(userId, lastFeedId, 10);
原理:
- 记录上一页最后一条的feedId
- 下一页从这个feedId之后开始读取
优点:
- 不会漏数据(有人发新动态)
- 不会重复(精确定位)
Q4: 如何保证Feed流的一致性?
A: 三种策略:
-
最终一致性:
- 推送到收件箱可能有延迟
- 用户刷新后能看到
-
双写:
- 同时写MySQL和Redis
- Redis过期后从MySQL加载
-
异步补偿:
- 推送失败的,异步重试
- 消息队列保证可靠性
Q5: 如何优化Feed流的性能?
A: 五种优化:
-
推拉结合:
- 普通用户推,明星用户拉
- 兼顾性能
-
异步扇出:
- 推送到粉丝收件箱异步执行
- 不阻塞发布
-
热点缓存:
- 热门动态缓存到Redis
- 减少数据库查询
-
分批推送:
- 每批1000个粉丝
- 并发推送
-
CDN加速:
- 图片、视频使用CDN
- 全球加速
Q6: 微博和朋友圈的Feed流有什么区别?
A: 微博(公开):
- 任何人都能看到
- 推拉结合(明星拉,普通用户推)
- 热点数据缓存
朋友圈(私密):
- 只有好友能看到
- 推模式为主(好友少)
- 三天可见(减少存储)
推荐:
- 根据业务特点选择
- 大部分场景用推拉结合
🎬 总结
Feed流系统核心架构
┌────────────────────────────────────┐
│ 推模式(Write Fanout) │
│ - 发动态 → 推送到粉丝收件箱 │
│ - 读快,写慢 │
│ - 适合普通用户 │
└────────────────────────────────────┘
┌────────────────────────────────────┐
│ 拉模式(Read Fanout) │
│ - 发动态 → 只保存 │
│ - 写快,读慢 │
│ - 适合明星用户 │
└────────────────────────────────────┘
┌────────────────────────────────────┐
│ 推拉结合(最佳实践)⭐⭐⭐ │
│ - 普通用户推,明星用户拉 │
│ - 兼顾性能和体验 │
└────────────────────────────────────┘
推拉结合是最佳方案!✅
🎉 恭喜你!
你已经完全掌握了Feed流系统的设计!🎊
核心要点:
- 推拉结合:普通用户推,明星用户拉
- 收件箱:Redis List存储
- 异步扇出:提高推送性能
- 热点缓存:减少数据库压力
下次面试,这样回答:
"Feed流系统采用推拉结合的方案。普通用户(粉丝<1000)使用推模式,发动态时推送到粉丝的Redis收件箱;明星用户(粉丝>1000)使用拉模式,发动态时只保存,粉丝读取时实时拉取。
读取Feed流时,先从收件箱读取推模式的动态,再实时拉取明星用户的动态,最后合并、去重、排序后返回。
性能优化方面,推送采用异步扇出,分批处理(每批1000个),并发推送。热门动态缓存到Redis,减少数据库压力。
我们项目的动态系统采用这套方案,支持千万用户,平均响应时间100ms,推送延迟1秒以内。"
面试官:👍 "很好!你对Feed流系统的设计理解很深刻!"
本文完 🎬
上一篇: 203-设计一个搜索引擎系统.md
下一篇: 205-设计一个分布式延迟任务调度系统.md
作者注:写完这篇,我都想去腾讯做微信朋友圈了!📰
如果这篇文章对你有帮助,请给我一个Star⭐!