高并发Feed流系统完整学习指南,面试高频题实战项目,系统设计题实战项目,附实战代码

83 阅读10分钟

【高并发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

服务访问地址: