【高并发Feed流系统速学-面试高频题实战项目-系统设计题实战项目】 www.bilibili.com/video/BV1ZU…
这是一个社交媒体Feed流系统的完整实现,模拟了类似微博、Twitter等社交平台的核心功能,特别适合新手学习分布式系统设计和高并发处理。
第一部分:业务场景理解
1.1 什么是Feed流系统?
Feed流是社交媒体平台的核心功能,用户可以看到关注的人发布的最新动态。想象一下:
- 你关注了100个朋友
- 他们每天发布各种动态(文字、图片、视频)
- 你的首页会按时间顺序显示这些动态
- 这就是Feed流系统
1.2 核心业务功能
用户管理:
- 用户注册:创建账号
- 关注关系:A关注B,A就能看到B的动态
内容发布:
- 发帖:用户发布文字内容
- 实时性:发帖后立即在粉丝的Feed流中显示
Feed流展示:
- 个人Feed流:显示关注者的最新动态
- 分页浏览:支持翻页查看历史动态
1.3 业务挑战
高并发挑战:
- 大量用户同时发帖
- 大量用户同时浏览Feed流
- 需要快速响应,不能卡顿
数据一致性:
- 发帖后要立即在粉丝Feed流中显示
- 不能出现数据丢失或重复
性能优化:
- 响应时间要快(毫秒级)
- 支持大量用户同时使用
第二部分:技术架构设计
2.1 整体架构图
┌─────────────────────────────────────────────────────────────┐
│ 用户请求层 │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ 发帖API │ │ 关注API │ │ Feed流API │ │
│ └─────────────┘ └─────────────┘ └─────────────┘ │
└─────────────────────────────────────────────────────────────┘
│
┌─────────────────────────────────────────────────────────────┐
│ Spring Boot应用层 │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ Controller │ │ Service │ │ Repository │ │
│ └─────────────┘ └─────────────┘ └─────────────┘ │
└─────────────────────────────────────────────────────────────┘
│
┌─────────────────────────────────────────────────────────────┐
│ 缓存层 │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ Caffeine │ │ Redis │ │ MySQL │ │
│ │ 本地缓存 │ │ 分布式缓存 │ │ 数据库 │ │
│ └─────────────┘ └─────────────┘ └─────────────┘ │
└─────────────────────────────────────────────────────────────┘
│
┌─────────────────────────────────────────────────────────────┐
│ 消息队列层 │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ Kafka │ │ Zookeeper │ │ 消费者服务 │ │
│ │ 消息队列 │ │ 协调服务 │ │ 异步处理 │ │
│ └─────────────┘ └─────────────┘ └─────────────┘ │
└─────────────────────────────────────────────────────────────┘
2.2 核心技术栈
后端框架:
- Spring Boot 3.3.3:主框架,提供Web服务、依赖注入、自动配置
- Java 17:现代Java版本,支持新特性
数据存储:
- MySQL 8.0:主数据库,存储用户、帖子、关注关系等核心数据
- MyBatis 3.0.3:ORM框架,负责数据库操作
缓存系统:
- Redis 7:分布式缓存,存储Feed流列表和帖子对象
- Caffeine:本地缓存,JVM内高速缓存
消息队列:
- Apache Kafka 3.6:异步消息处理,实现发帖的削峰填谷
- Zookeeper 3.9:Kafka的协调服务
监控体系:
- Prometheus:指标收集和存储
- Grafana:可视化监控面板
- Micrometer:应用指标暴露
第三部分:核心设计模式
3.1 推拉混合模式(Push-Pull Hybrid)
这是Feed流系统的核心设计模式,根据用户粉丝数智能选择策略:
小号推模式(粉丝数 < 10,000):
用户发帖 → 立即推送到所有粉丝的Feed流 → 粉丝浏览时直接读取
- 优势:读取速度快,用户体验好
- 劣势:写入压力大,粉丝越多写入越慢
大V拉模式(粉丝数 ≥ 10,000):
用户发帖 → 只存储帖子 → 粉丝浏览时实时聚合生成Feed流
- 优势:写入压力小,支持大量粉丝
- 劣势:读取时需要聚合,响应稍慢
代码实现:
if (author.getFollowersCount() < BIG_V_THRESHOLD) {
// 小号推模式:发送Kafka消息,异步写入粉丝Feed流
kafkaPublishService.publishPost(saved);
} else {
// 大V拉模式:仅写帖子,读时聚合
log.info("大V拉模式,读时聚合");
}
3.2 多级缓存架构
三级缓存设计:
用户请求 → Caffeine本地缓存 → Redis分布式缓存 → MySQL数据库
第一级:Caffeine本地缓存
- 响应时间:< 1ms
- 容量:10,000条记录
- TTL:1秒
- 适用场景:热点数据,频繁访问
第二级:Redis分布式缓存
- 响应时间:1-5ms
- 数据结构:List(Feed流)+ String(帖子对象)
- TTL:60秒
- 适用场景:跨实例共享,用户Feed流
第三级:MySQL数据库
- 响应时间:10-100ms
- 适用场景:持久化存储,冷数据
缓存更新策略:
// Cache-Aside模式
// 读操作:先查缓存,未命中回源
List<Post> cachedPosts = postCacheService.multiGetPosts(postIds);
if (cachedPosts.isEmpty()) {
posts = postRepository.findByIds(postIds); // 回源
postCacheService.cachePosts(posts); // 回填
}
// 写操作:先写数据库,再更新缓存
postRepository.updateContent(postId, newContent);
postCacheService.cachePost(updatedPost); // 刷新缓存
3.3 异步处理模式
发帖异步处理流程:
1. 用户发帖 → API立即返回成功
2. 发送Kafka消息 → 后台消费者处理
3. 消费者写入粉丝Feed流 → 更新缓存
优势:
- 用户体验:发帖API立即返回,不等待
- 系统稳定性:削峰填谷,避免高并发冲击
- 可扩展性:可以增加消费者实例处理更多消息
第四部分:数据库设计详解
4.1 表结构设计
用户表(users):
CREATE TABLE users (
id BIGINT PRIMARY KEY AUTO_INCREMENT, -- 主键
username VARCHAR(191) NOT NULL UNIQUE, -- 用户名(191避免索引超长)
followers_count INT NOT NULL DEFAULT 0 -- 粉丝数(用于推拉阈值判定)
);
帖子表(posts):
CREATE TABLE posts (
id BIGINT PRIMARY KEY AUTO_INCREMENT, -- 帖子ID
author_id BIGINT NOT NULL, -- 作者ID
content VARCHAR(2000) NOT NULL, -- 正文
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, -- 创建时间
INDEX idx_posts_author_created(author_id, created_at DESC), -- 复合索引
INDEX idx_posts_created(created_at DESC) -- 时间索引
);
关注关系表(follows):
CREATE TABLE follows (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
follower_id BIGINT NOT NULL, -- 关注者ID
followee_id BIGINT NOT NULL, -- 被关注者ID
UNIQUE KEY uk_follow (follower_id, followee_id), -- 防止重复关注
INDEX idx_follow_followee(followee_id), -- 查询某人的粉丝
INDEX idx_follow_follower(follower_id) -- 查询某人的关注列表
);
Feed流表(timeline_entries):
CREATE TABLE timeline_entries (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
user_id BIGINT NOT NULL, -- Feed流归属用户
post_id BIGINT NOT NULL, -- 帖子ID
author_id BIGINT NOT NULL, -- 作者ID(冗余字段)
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
UNIQUE KEY uk_timeline_user_post(user_id, post_id), -- 幂等性保证
INDEX idx_timeline_user_created(user_id, created_at DESC) -- Feed流查询索引
);
4.2 索引优化策略
复合索引设计:
-- 支持查询:SELECT * FROM posts WHERE author_id IN (...) ORDER BY created_at DESC
INDEX idx_posts_author_created(author_id, created_at DESC)
-- 支持查询:SELECT post_id FROM timeline_entries WHERE user_id = ? ORDER BY created_at DESC
INDEX idx_timeline_user_created(user_id, created_at DESC)
索引优势:
- 覆盖索引:避免回表查询,提升性能
- 排序优化:索引顺序与查询顺序一致
- 范围查询:支持时间范围查询
第五部分:Redis缓存设计
5.1 数据结构选择
Feed流列表缓存(List结构):
// Redis Key: timeline:{userId}
// 数据结构: List<String> 存储postId列表
String redisKey = "timeline:" + userId;
List<String> postIdStrs = stringRedisTemplate.opsForList().range(redisKey, start, end);
帖子对象缓存(String结构):
// Redis Key: post:{postId}
// 数据结构: String 存储JSON格式的帖子详情
String postKey = "post:" + postId;
String json = redis.opsForValue().get(postKey);
预聚合数据缓存(ZSet结构):
// Redis Key: preagg:bigv:{userId}
// 数据结构: ZSet 存储postId,score为时间戳
String preAggKey = "preagg:bigv:" + userId;
redisTemplate.opsForZSet().add(preAggKey, String.valueOf(post.getId()), score);
5.2 Lua脚本原子操作
Feed流更新Lua脚本:
local key = KEYS[1];
local value = ARGV[1];
redis.call('LPUSH', key, value); -- 推入新帖子ID
redis.call('LTRIM', key, 0, 999); -- 保持列表长度不超过1000
return 1;
脚本优势:
- 原子性:整个操作在Redis服务器端原子执行
- 性能优化:避免客户端多次网络往返
- 数据一致性:防止并发条件下的数据不一致
第六部分:Kafka消息处理
6.1 生产者配置
Exactly-Once语义配置:
configProps.put(ProducerConfig.ACKS_CONFIG, "all"); // 等待所有副本确认
configProps.put(ProducerConfig.ENABLE_IDEMPOTENCE_CONFIG, true); // 启用幂等性
configProps.put(ProducerConfig.RETRIES_CONFIG, 3); // 重试3次
configProps.put(ProducerConfig.MAX_IN_FLIGHT_REQUESTS_PER_CONNECTION, 5); // 最大在途请求数
自定义分区器:
@Override
public int partition(String topic, Object key, byte[] keyBytes,
Object value, byte[] valueBytes, Cluster cluster) {
int partitionCount = cluster.partitionCountForTopic(topic);
String authorId = (String) key;
// 计算分区:hash(authorId) % partitionCount
int partition = Math.abs(authorId.hashCode()) % partitionCount;
return partition;
}
分区策略优势:
- 消息有序性:同一作者的消息发送到同一分区,保证顺序
- 负载均衡:不同作者的消息分布到不同分区
- 扩展性:分区数可动态调整
6.2 消费者处理
消费者配置:
kafka:
consumer:
group-id: feed-consumers # 消费者组ID
properties:
max.poll.records: 200 # 单次拉取最大记录数
isolation.level: read_committed # 只读已提交消息
消息处理流程:
@KafkaListener(topics = KafkaConfig.TOPIC_POST_PUBLISH)
public void onPostPublish(String message) {
// 1. 解析消息:postId,authorId
String[] arr = message.split(",");
Long postId = Long.valueOf(arr[0]);
Long authorId = Long.valueOf(arr[1]);
// 2. 查询帖子详情并缓存
Post post = postRepository.findById(postId);
postCacheService.cachePost(post);
// 3. 查询作者的所有粉丝
List<Follow> fans = followRepository.findByFolloweeId(authorId);
// 4. 为每个粉丝写入Feed流
for (Follow fan : fans) {
TimelineEntry e = new TimelineEntry();
e.setUserId(fan.getFollowerId());
e.setPostId(post.getId());
e.setAuthorId(authorId);
timelineEntryRepository.insert(e);
// 5. 更新RedisFeed流列表
String redisKey = "timeline:" + fan.getFollowerId();
redisLuaService.lpushTimelineTrim(redisKey, String.valueOf(post.getId()));
}
}
第七部分:核心业务代码解析
7.1 发帖流程详解
TimelineService.createPost方法:
@Transactional
public Post createPost(Long authorId, String content) {
// 1. 记录发帖请求日志
log.info("[post] 发帖请求 | authorId={} | 内容预览={}",
authorId, content.substring(0, Math.min(32, content.length())));
// 2. 创建Post对象并插入数据库
Post post = new Post();
post.setAuthorId(authorId);
post.setContent(content);
postRepository.insert(post);
Post saved = postRepository.findById(post.getId());
// 3. 查询作者信息,判断推拉模式
User author = userRepository.findById(authorId);
if (author != null) {
if (author.getFollowersCount() < BIG_V_THRESHOLD) {
// 4. 小号推模式:发送Kafka消息
log.info("[post] 小号推模式 | 粉丝数={}", author.getFollowersCount());
kafkaPublishService.publishPost(saved);
} else {
// 5. 大V拉模式:仅写帖子,读时聚合
log.info("[post] 大V拉模式 | 粉丝数={}", author.getFollowersCount());
}
}
return saved;
}
7.2 Feed流读取流程详解
TimelineService.readTimelineItems方法:
@Cacheable(cacheNames = "timeline", key = "#userId + ':' + #page + ':' + #size")
@Transactional(readOnly = true)
public List<FeedItem> readTimelineItems(Long userId, int page, int size) {
// 1. 验证用户存在性
User u = userRepository.findById(userId);
if (u == null) throw new IllegalArgumentException("user not found");
// 2. 检查RedisFeed流列表
String redisKey = "timeline:" + userId;
Long listLen = stringRedisTemplate.opsForList().size(redisKey);
if (listLen != null && listLen > 0) {
// 3. 从Redis List获取postId列表
List<String> postIdStrs = stringRedisTemplate.opsForList().range(redisKey, page * size, page * size + size - 1);
if (postIdStrs != null && !postIdStrs.isEmpty()) {
// 4. 解析postId并批量获取帖子对象
ArrayList<Long> postIds = new ArrayList<>();
for (String postIdStr : postIdStrs) {
try {
postIds.add(Long.valueOf(postIdStr));
} catch (NumberFormatException e) {
log.debug("[redis-list] 无效的postId格式 | postIdStr={}", postIdStr);
}
}
// 5. 批量获取Post对象缓存
List<Post> cachedPosts = postCacheService.multiGetPosts(postIds);
// 6. 构建FeedItem列表
ArrayList<FeedItem> items = new ArrayList<>();
for (int i = 0; i < postIds.size(); i++) {
Long postId = postIds.get(i);
FeedItem item = new FeedItem();
// 7. 查找对应的缓存Post
Post cachedPost = cachedPosts.stream()
.filter(post -> post.getId().equals(postId))
.findFirst()
.orElse(null);
if (cachedPost != null) {
// 8. 从缓存Post构建FeedItem
item.setPostId(cachedPost.getId());
item.setAuthorId(cachedPost.getAuthorId());
item.setCreatedAt(cachedPost.getCreatedAt());
item.setContent(cachedPost.getContent());
} else {
// 9. 对象缓存未命中,设置占位符
item.setPostId(postId);
item.setAuthorId(0L);
item.setContent("(缓存未命中)");
}
items.add(item);
}
return items;
}
}
// 10. Redis未命中,执行读时聚合
int offset = page * size;
List<Long> followees = followRepository.findFolloweeIdsByFollower(userId);
if (followees == null || followees.isEmpty()) {
return Collections.emptyList();
}
// 11. 尝试使用预聚合数据
List<Post> posts = tryPreAggregatedPosts(followees, size, offset);
if (posts != null && !posts.isEmpty()) {
markReadSource("pre-agg");
} else {
// 12. 回源MySQL查询
markReadSource("db");
posts = postRepository.findRecentByAuthorIds(followees, null, size, offset);
}
// 13. 构建结果并回填缓存
ArrayList<FeedItem> items = new ArrayList<>();
for (Post p : posts) {
FeedItem item = new FeedItem();
item.setPostId(p.getId());
item.setAuthorId(p.getAuthorId());
item.setCreatedAt(p.getCreatedAt());
item.setContent(p.getContent());
items.add(item);
// 14. 缓存帖子对象
postCacheService.cachePost(p);
// 15. 回填RedisFeed流列表
String postIdStr = String.valueOf(p.getId());
redisLuaService.lpushTimelineTrim(redisKey, postIdStr);
}
// 16. 设置TTL
stringRedisTemplate.expire(redisKey, Duration.ofSeconds(60));
return items;
}
第八部分:监控和性能优化
8.1 监控体系设计
Prometheus + Grafana监控栈:
应用指标 → Micrometer → Prometheus → Grafana Dashboard
核心性能指标:
// 缓存命中率指标
Gauge.builder("feed.cache.hit.rate")
.tag("cache_type", "local")
.tag("cache_name", "caffeine")
.register(meterRegistry);
// API性能指标
Timer.Sample sample = feedMetricsService.startTimelineApiTimer();
feedMetricsService.recordTimelineApiDuration(sample);
AOP切面监控:
@Around("execution(* com.example.feeddemo.service.TimelineService.readTimelineItems(..))")
public Object aroundReadTimelineItems(ProceedingJoinPoint joinPoint) {
// 统计缓存命中情况
boolean localCacheHit = checkLocalCache(cacheKey);
boolean redisListHit = checkRedisListCache(userId, page, size);
// 记录指标
if (localCacheHit) localCacheHits.incrementAndGet();
else localCacheMisses.incrementAndGet();
}
8.2 性能优化策略
缓存优化:
// 批量操作优化
public List<Post> multiGetPosts(List<Long> postIds) {
List<String> keys = postIds.stream()
.map(this::buildKey)
.collect(Collectors.toList());
List<String> values = redis.opsForValue().multiGet(keys);
return serializationService.deserializePosts(values);
}
数据库优化:
-- 复合索引优化
INDEX idx_posts_author_created(author_id, created_at DESC)
-- 批量查询优化
SELECT * FROM posts
WHERE author_id IN (1,2,3,4,5)
ORDER BY created_at DESC
LIMIT 10 OFFSET 0;
Kafka优化:
// 批量发送配置
configProps.put(ProducerConfig.LINGER_MS_CONFIG, 5); // 批量延迟5ms
configProps.put(ProducerConfig.BATCH_SIZE_CONFIG, 16384); // 批量大小16KB
// 消费者批量处理
properties.put("max.poll.records", 200); // 单次拉取200条
第九部分:部署和运维
9.1 Docker部署配置
Docker Compose服务配置:
version: '3.8'
services:
mysql:
image: mysql:8.0
environment:
- MYSQL_ROOT_PASSWORD=root
- MYSQL_DATABASE=feeddemo
ports:
- "3306:3306"
redis:
image: redis:7
ports:
- "6379:6379"
kafka:
image: bitnami/kafka:3.6
depends_on:
- zookeeper
ports:
- "9092:9092"
environment:
- KAFKA_CFG_ZOOKEEPER_CONNECT=zookeeper:2181
- KAFKA_CFG_LISTENERS=PLAINTEXT://:9092
- KAFKA_CFG_ADVERTISED_LISTENERS=PLAINTEXT://localhost:9092
prometheus:
image: prom/prometheus:v2.52.0
ports:
- "9090:9090"
grafana:
image: grafana/grafana:10.4.3
ports:
- "3000:3000"
environment:
- GF_SECURITY_ADMIN_PASSWORD=admin123
9.2 一键启动
启动所有服务:
# 启动基础设施服务
docker-compose up -d
# 构建并启动应用
docker-compose build app && docker-compose up -d app
服务访问地址:
- 应用服务:http://localhost:8080
- Prometheus:http://localhost:9090
- Grafana:http://localhost:3000 (admin/admin123)
- MySQL:localhost:3306 (root/root)
- Redis:localhost:6379
- Kafka:localhost:9092