HiHuo
首页
博客
手册
工具
关于
首页
博客
手册
工具
关于
  • 系统设计实战

    • 系统设计面试教程
    • 系统设计方法论
    • 01-短链系统设计
    • 02 - 秒杀系统设计
    • 03 - IM 即时通讯系统设计
    • 04 - Feed 流系统设计
    • 05 - 分布式 ID 生成器设计
    • 06 - 限流系统设计
    • 第7章:搜索引擎设计
    • 08 - 推荐系统设计
    • 09 - 支付系统设计
    • 10 - 电商系统设计
    • 11 - 直播系统设计
    • 第12章:缓存系统设计
    • 第13章:消息队列设计
    • 第14章:分布式事务
    • 15 - 监控系统设计

04 - Feed 流系统设计

面试频率: 难度等级: 推荐时长: 45-60 分钟

目录

  • 需求分析与澄清
  • 容量估算
  • API 设计
  • 数据模型设计
  • 架构设计
  • 核心算法与实现
  • 优化方案
  • 监控告警
  • 面试问答

需求分析与澄清

业务场景

Feed 流(信息流/时间线)是社交网络的核心功能,用于展示用户关注的内容更新。典型应用:

  • 微博/Twitter 时间线
  • 朋友圈/Facebook Timeline
  • Instagram 照片流
  • 知乎关注动态
  • 抖音推荐流

核心挑战

Feed 流系统的四大挑战:
┌─────────────────────────────────────────────────────────┐
│ 1. 推拉模式选择                                          │
│    - Push(推模式):写扩散,读取快                      │
│    - Pull(拉模式):读扩散,写入快                      │
│    - Push+Pull(混合模式):平衡性能                     │
│                                                          │
│ 2. 大 V 问题                                             │
│    - 大 V 发布内容,需要推送给百万粉丝                   │
│    - 写扩散成本极高                                      │
│    - 需要特殊处理策略                                    │
│                                                          │
│ 3. Feed 聚合与排序                                       │
│    - 时间排序 vs 智能排序                                │
│    - 多路归并(关注多人的内容)                          │
│    - 去重、过滤                                          │
│                                                          │
│ 4. 海量数据存储                                          │
│    - 亿级用户,千亿级 Feed                               │
│    - 读写比例:1:100(读多写少)                         │
│    - 冷热数据分离                                        │
└─────────────────────────────────────────────────────────┘

功能性需求

面试官视角的关键问题

面试官: "设计一个类似微博的 Feed 流系统。"

你需要主动澄清以下需求:

Q1: Feed 流类型?

 关注流(Follow Feed): 展示关注的人发布的内容
 推荐流(Recommend Feed): 基于算法推荐感兴趣的内容
 不包含复杂的推荐算法(机器学习超出范围)

Q2: 用户规模?

DAU: 1 亿
日发布 Feed 数: 1 亿条(平均每人 1 条)
平均关注数: 100 人
平均粉丝数: 100 人
大 V 粉丝数: 1000 万
日 Feed 浏览量: 100 亿次(每人刷 100 条)

Q3: 排序方式?

时间排序: 按发布时间倒序(最新在前)
热度排序: 按点赞、评论、转发等热度指标
个性化排序: 基于用户兴趣(需要推荐算法)

【本次设计】
- 主要支持时间排序
- 预留热度排序接口

Q4: 关注关系?

单向关注: 用户 A 关注 B,B 不一定关注 A(微博模式)
双向关注: 互相关注(微信朋友圈模式)

【本次设计】
- 支持单向关注(更通用)

Q5: Feed 内容类型?

 文本、图片、视频
 点赞、评论、转发
 @ 提及、话题标签
 直播、投票等复杂功能

非功能性需求

需求类型具体要求优先级
低延迟Feed 拉取 < 500ms (P99)P0
高可用99.9% 可用性P0
最终一致性Feed 延迟可接受(1-2秒)P0
扩展性支持亿级用户P0
可观测性监控、日志、链路追踪P1

容量估算

用户与内容规模

【用户数据】
总用户数: 10 亿
DAU: 1 亿 (10%)
日活跃发布者: 2000 万 (20%)

【Feed 发布】
日发布 Feed 数: 1 亿条
平均每条 Feed 大小: 1 KB(不含图片视频)
日新增存储: 1亿 * 1KB = 100 GB

一年存储: 100GB * 365 = 36.5 TB
三年存储: ≈ 110 TB(纯文本)

【关注关系】
总关注关系: 10亿用户 * 100 = 1000 亿条
单条关系: 16 Bytes(follower_id + followee_id)
总存储: 1000亿 * 16B = 1.6 TB

读写 QPS 估算

【写入 QPS】
发布 Feed: 1亿 / 86400 ≈ 1200 QPS
峰值: 1200 * 3 = 3600 QPS

【读取 QPS】
日浏览量: 100 亿次
平均 QPS: 100亿 / 86400 ≈ 11.5 万 QPS
峰值 QPS: 11.5万 * 3 = 35 万 QPS

【读写比例】
读:写 = 100:1(典型的读多写少场景)

Push 模式存储估算

【写扩散(Push 模式)】
假设:用户发布 Feed 时,推送给所有粉丝

用户发布 1 条 Feed:
  - 平均粉丝数: 100 人
  - 需要写入: 100 条记录

日发布 1 亿条 Feed:
  - 总写入量: 1亿 * 100 = 100 亿条
  - 每条收件箱记录: 24 Bytes(feed_id + user_id + timestamp)
  - 日新增: 100亿 * 24B = 240 GB

一年存储: 240GB * 365 ≈ 87.6 TB
三年存储: ≈ 260 TB

【大 V 问题】
大 V(1000 万粉丝)发布 1 条:
  - 写入量: 1000 万条
  - 写入时间: 假设 10万 TPS,需要 100 秒
  - 成本极高!

Pull 模式计算估算

【读扩散(Pull 模式)】
用户刷新 Feed:
  - 查询所有关注的人(100 人)
  - 每人取最新 10 条
  - 合并排序,返回 20 条

计算成本:
  - 100 次 DB 查询(可缓存)
  - 1000 条数据归并排序
  - QPS: 11.5 万

优化后:
  - Redis 缓存每个用户最新 100 条 Feed
  - 只需 100 次 Redis 查询(批量获取)
  - 响应时间: < 100ms

混合模式权衡

【Push + Pull 混合】
普通用户(粉丝 < 10 万): Push 模式
  - 发布时推送给所有粉丝
  - 读取快(直接从收件箱读)

大 V(粉丝 > 10 万): Pull 模式
  - 只写自己的发件箱
  - 粉丝读取时实时拉取

存储成本:
  - 普通用户: 1.95亿 * 100 * 24B = 468 GB/天
  - 大 V: 500 万 * 1 * 1KB = 5 GB/天
  - 总计: 473 GB/天 ≈ 170 TB/年

缓存容量

【Redis 缓存】
用户收件箱缓存:
  - 1 亿 DAU * 100 条 * 24 Bytes = 240 GB

Feed 内容缓存:
  - 热点 Feed(1000 万条)* 1 KB = 10 GB

关注关系缓存:
  - 1 亿 DAU * 100 人 * 16 Bytes = 160 GB

总计: 约 500 GB
实际分配: 2 TB(考虑冗余)

带宽估算

【下行带宽】
QPS: 35 万
每次请求返回: 20 条 * 1 KB = 20 KB
带宽: 35万 * 20KB = 7 GB/s = 56 Gbps

【上行带宽】
QPS: 3600
每次上传: 10 KB(含图片缩略图)
带宽: 3600 * 10KB = 36 MB/s = 0.3 Gbps

建议带宽: 100 Gbps(考虑 CDN 卸载)

API 设计

RESTful API

1. Feed 发布

# 发布 Feed
POST /api/v1/feeds

Headers:
  Authorization: Bearer {token}

Request Body:
{
  "content": "今天天气真好!#美好的一天",
  "images": ["url1", "url2"],
  "video": null,
  "location": {
    "lat": 39.9042,
    "lng": 116.4074,
    "name": "北京市朝阳区"
  },
  "mentions": [10001, 10002],  // @ 提及的用户
  "visibility": "public"       // public, private, friends
}

Response 200:
{
  "code": 0,
  "data": {
    "feed_id": "1234567890",
    "user_id": 10001,
    "content": "今天天气真好!#美好的一天",
    "created_at": 1699804800000
  }
}

2. Feed 拉取(时间线)

# 拉取关注的人的 Feed(时间线)
GET /api/v1/feeds/timeline?cursor=&limit=20

Headers:
  Authorization: Bearer {token}

Response 200:
{
  "code": 0,
  "data": {
    "feeds": [
      {
        "feed_id": "1234567890",
        "user": {
          "user_id": 10001,
          "username": "alice",
          "avatar": "https://cdn.example.com/avatar/alice.jpg"
        },
        "content": "今天天气真好!#美好的一天",
        "images": ["url1", "url2"],
        "video": null,
        "created_at": 1699804800000,
        "stats": {
          "likes": 100,
          "comments": 20,
          "shares": 5
        },
        "is_liked": false,
        "is_bookmarked": false
      }
    ],
    "next_cursor": "1699804700000_1234567889",  // 用于分页
    "has_more": true
  }
}

【Cursor 分页】
- cursor 格式: {timestamp}_{feed_id}
- 优点: 避免漏数据(新 Feed 插入导致偏移)
- 缺点: 不支持跳页

3. 用户 Feed 列表

# 查看某用户发布的所有 Feed
GET /api/v1/users/{user_id}/feeds?cursor=&limit=20

Response 200:
{
  "code": 0,
  "data": {
    "feeds": [ /* ... */ ],
    "next_cursor": "...",
    "has_more": true
  }
}

4. Feed 详情

# 查看单条 Feed 详情
GET /api/v1/feeds/{feed_id}

Response 200:
{
  "code": 0,
  "data": {
    "feed_id": "1234567890",
    "user": { /* ... */ },
    "content": "...",
    "images": [],
    "created_at": 1699804800000,
    "stats": {
      "likes": 100,
      "comments": 20,
      "shares": 5,
      "views": 1000
    }
  }
}

5. 点赞/取消点赞

# 点赞
POST /api/v1/feeds/{feed_id}/like

Response 200:
{
  "code": 0,
  "data": {
    "feed_id": "1234567890",
    "likes": 101  // 点赞后的总数
  }
}

# 取消点赞
DELETE /api/v1/feeds/{feed_id}/like

Response 200:
{
  "code": 0,
  "data": {
    "feed_id": "1234567890",
    "likes": 100
  }
}

6. 评论

# 发表评论
POST /api/v1/feeds/{feed_id}/comments

Request Body:
{
  "content": "说得好!",
  "reply_to": null  // 回复某条评论的 ID
}

Response 200:
{
  "code": 0,
  "data": {
    "comment_id": "comment_123",
    "feed_id": "1234567890",
    "user_id": 10001,
    "content": "说得好!",
    "created_at": 1699804800000
  }
}

# 查看评论列表
GET /api/v1/feeds/{feed_id}/comments?page=1&size=20

Response 200:
{
  "code": 0,
  "data": {
    "comments": [
      {
        "comment_id": "comment_123",
        "user": { /* ... */ },
        "content": "说得好!",
        "likes": 10,
        "reply_to": null,
        "created_at": 1699804800000
      }
    ],
    "total": 20
  }
}

7. 关注/取消关注

# 关注用户
POST /api/v1/users/{user_id}/follow

Response 200:
{
  "code": 0,
  "data": {
    "user_id": 10002,
    "is_following": true
  }
}

# 取消关注
DELETE /api/v1/users/{user_id}/follow

# 查看关注列表
GET /api/v1/users/{user_id}/following?page=1&size=20

# 查看粉丝列表
GET /api/v1/users/{user_id}/followers?page=1&size=20

数据模型设计

核心表结构

1. Feed 表 (feed)

CREATE TABLE feed (
    id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY COMMENT '自增 ID',
    feed_id VARCHAR(64) NOT NULL COMMENT 'Feed 唯一标识',
    user_id BIGINT UNSIGNED NOT NULL COMMENT '发布者 UID',
    content TEXT NOT NULL COMMENT 'Feed 内容',
    media_type TINYINT UNSIGNED DEFAULT 0 COMMENT '媒体类型: 0-纯文本 1-图片 2-视频',
    media_urls JSON COMMENT '媒体 URL 列表',
    location JSON COMMENT '位置信息',
    visibility TINYINT UNSIGNED DEFAULT 0 COMMENT '可见性: 0-公开 1-私密 2-好友',
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '发布时间',
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,

    UNIQUE KEY uk_feed_id (feed_id),
    INDEX idx_user_id_created (user_id, created_at DESC) COMMENT '用户 Feed 列表',
    INDEX idx_created_at (created_at DESC) COMMENT '时间排序'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='Feed 表';

-- 分表策略: 按 user_id 分 256 张表(用户维度)
-- 或按时间分表(每月一张表)

2. Feed 收件箱表 (feed_inbox) - Push 模式

CREATE TABLE feed_inbox (
    id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY COMMENT '自增 ID',
    user_id BIGINT UNSIGNED NOT NULL COMMENT '收件人 UID',
    feed_id VARCHAR(64) NOT NULL COMMENT 'Feed ID',
    author_id BIGINT UNSIGNED NOT NULL COMMENT '发布者 UID',
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '推送时间',

    UNIQUE KEY uk_user_feed (user_id, feed_id),
    INDEX idx_user_created (user_id, created_at DESC) COMMENT '用户时间线',
    INDEX idx_author_id (author_id) COMMENT '删除 Feed 时清理收件箱'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='Feed 收件箱(写扩散)';

-- 分表策略: 按 user_id 分 256 张表
-- 定期清理旧数据(只保留最近 3 个月)

3. 关注关系表 (follow_relation)

CREATE TABLE follow_relation (
    id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY COMMENT '自增 ID',
    follower_id BIGINT UNSIGNED NOT NULL COMMENT '粉丝 UID',
    followee_id BIGINT UNSIGNED NOT NULL COMMENT '被关注者 UID',
    status TINYINT UNSIGNED DEFAULT 1 COMMENT '状态: 0-取消 1-正常',
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '关注时间',
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,

    UNIQUE KEY uk_follower_followee (follower_id, followee_id),
    INDEX idx_followee_id (followee_id) COMMENT '查询粉丝列表',
    INDEX idx_follower_id (follower_id) COMMENT '查询关注列表'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='关注关系表';

-- 分表策略: 按 follower_id 分 256 张表

4. Feed 统计表 (feed_stats)

CREATE TABLE feed_stats (
    feed_id VARCHAR(64) PRIMARY KEY COMMENT 'Feed ID',
    likes_count INT UNSIGNED DEFAULT 0 COMMENT '点赞数',
    comments_count INT UNSIGNED DEFAULT 0 COMMENT '评论数',
    shares_count INT UNSIGNED DEFAULT 0 COMMENT '转发数',
    views_count INT UNSIGNED DEFAULT 0 COMMENT '浏览数',
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,

    INDEX idx_likes_count (likes_count DESC) COMMENT '热门 Feed',
    INDEX idx_created_at (updated_at DESC)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='Feed 统计表';

-- 说明:
-- 点赞、评论数等高频更新的数据单独存储
-- 避免更新 feed 表导致的锁竞争
-- 定期同步到 Redis 缓存

5. 点赞表 (feed_like)

CREATE TABLE feed_like (
    id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY COMMENT '自增 ID',
    feed_id VARCHAR(64) NOT NULL COMMENT 'Feed ID',
    user_id BIGINT UNSIGNED NOT NULL COMMENT '点赞用户 UID',
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '点赞时间',

    UNIQUE KEY uk_feed_user (feed_id, user_id),
    INDEX idx_user_id (user_id) COMMENT '用户点赞列表',
    INDEX idx_feed_id (feed_id) COMMENT '查询点赞用户'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='点赞表';

-- 分表策略: 按 feed_id 分表(数据量大)

6. 评论表 (feed_comment)

CREATE TABLE feed_comment (
    id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY COMMENT '自增 ID',
    comment_id VARCHAR(64) NOT NULL COMMENT '评论 ID',
    feed_id VARCHAR(64) NOT NULL COMMENT 'Feed ID',
    user_id BIGINT UNSIGNED NOT NULL COMMENT '评论用户 UID',
    content TEXT NOT NULL COMMENT '评论内容',
    reply_to VARCHAR(64) DEFAULT NULL COMMENT '回复的评论 ID',
    likes_count INT UNSIGNED DEFAULT 0 COMMENT '点赞数',
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '评论时间',

    UNIQUE KEY uk_comment_id (comment_id),
    INDEX idx_feed_id_created (feed_id, created_at DESC) COMMENT '查询 Feed 评论',
    INDEX idx_user_id (user_id) COMMENT '用户评论列表'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='评论表';

-- 分表策略: 按 feed_id 分表

Redis 数据结构设计

1. 用户收件箱(Sorted Set)

【Sorted Set 类型】
Key: feed:inbox:{user_id}
Score: 时间戳(毫秒)
Value: feed_id

示例:
ZADD feed:inbox:10001 1699804800000 "feed_123"
ZADD feed:inbox:10001 1699804700000 "feed_456"

# 查询最新 20 条(按时间倒序)
ZREVRANGE feed:inbox:10001 0 19 WITHSCORES

# 查询某个时间之前的 20 条(分页)
ZREVRANGEBYSCORE feed:inbox:10001 1699804700000 -inf LIMIT 0 20

# 只保留最新 1000 条(定期清理)
ZREMRANGEBYRANK feed:inbox:10001 0 -1001

2. 用户发件箱(Sorted Set)

【Sorted Set 类型】
Key: feed:outbox:{user_id}
Score: 时间戳
Value: feed_id

示例:
ZADD feed:outbox:10001 1699804800000 "feed_123"
ZREVRANGE feed:outbox:10001 0 19  # 最新 20 条

3. Feed 内容缓存(String)

【String 类型】
Key: feed:content:{feed_id}
Value: JSON 序列化的 Feed 详情
TTL: 1 小时(热点数据)

示例:
SET feed:content:feed_123 '{"feed_id":"feed_123","user_id":10001,...}' EX 3600
GET feed:content:feed_123

4. 关注列表缓存(Set)

【Set 类型】
Key: user:following:{user_id}
Value: followee_id 集合

示例:
SADD user:following:10001 10002 10003 10004
SMEMBERS user:following:10001
SISMEMBER user:following:10001 10002  # 检查是否关注
SCARD user:following:10001  # 关注数

5. 粉丝列表缓存(Set)

【Set 类型】
Key: user:followers:{user_id}
Value: follower_id 集合

示例:
SADD user:followers:10002 10001 10003
SMEMBERS user:followers:10002
SCARD user:followers:10002  # 粉丝数

6. Feed 统计缓存(Hash)

【Hash 类型】
Key: feed:stats:{feed_id}
Field: likes, comments, shares, views
Value: 统计数

示例:
HSET feed:stats:feed_123 likes 100 comments 20 shares 5 views 1000
HINCRBY feed:stats:feed_123 likes 1  # 点赞数 +1
HGETALL feed:stats:feed_123

7. 用户点赞记录(Set)

【Set 类型】
Key: user:liked:{user_id}
Value: feed_id 集合

示例:
SADD user:liked:10001 "feed_123" "feed_456"
SISMEMBER user:liked:10001 "feed_123"  # 检查是否点赞

架构设计

V1 版本:Push 模式(写扩散)

架构图

客户端
    ↓
┌────────────────────────────────────────┐
│         Nginx 负载均衡                  │
└────────────────────────────────────────┘
    ↓
┌────────────────────────────────────────┐
│      Feed 服务集群(50 台)             │
│  ┌──────────────────────────────────┐  │
│  │  Feed 发布                       │  │
│  │  1. 保存到 feed 表                │  │
│  │  2. 推送到所有粉丝收件箱          │  │
│  └──────────────────────────────────┘  │
│  ┌──────────────────────────────────┐  │
│  │  Feed 读取                       │  │
│  │  1. 查询收件箱 (feed_inbox)       │  │
│  │  2. 批量查询 Feed 详情            │  │
│  └──────────────────────────────────┘  │
└────────────────────────────────────────┘
    ↓
┌────────────────────────────────────────┐
│         MySQL 主从                      │
│  - feed 表(发件箱)                    │
│  - feed_inbox 表(收件箱,核心)        │
│  - follow_relation 表                   │
└────────────────────────────────────────┘

核心代码实现

Feed 发布(Push 模式)

package service

import (
    "database/sql"
)

type FeedService struct {
    db *sql.DB
}

// PublishFeed 发布 Feed(Push 模式)
func (s *FeedService) PublishFeed(userID int64, content string, mediaURLs []string) (string, error) {
    // 1. 生成 Feed ID
    feedID := generateFeedID()

    // 2. 保存 Feed 到 feed 表
    insertFeedSQL := `
        INSERT INTO feed (feed_id, user_id, content, media_urls, created_at)
        VALUES (?, ?, ?, ?, NOW())
    `
    mediaJSON, _ := json.Marshal(mediaURLs)
    if _, err := s.db.Exec(insertFeedSQL, feedID, userID, content, mediaJSON); err != nil {
        return "", err
    }

    // 3. 查询粉丝列表
    followers, err := s.getFollowers(userID)
    if err != nil {
        return "", err
    }

    // 4. 推送到所有粉丝的收件箱(写扩散)
    if err := s.pushToInbox(feedID, userID, followers); err != nil {
        log.Errorf("推送到收件箱失败: %v", err)
        // 不影响 Feed 发布成功,可异步重试
    }

    return feedID, nil
}

// getFollowers 查询粉丝列表
func (s *FeedService) getFollowers(userID int64) ([]int64, error) {
    query := "SELECT follower_id FROM follow_relation WHERE followee_id = ? AND status = 1"
    rows, err := s.db.Query(query, userID)
    if err != nil {
        return nil, err
    }
    defer rows.Close()

    var followers []int64
    for rows.Next() {
        var followerID int64
        rows.Scan(&followerID)
        followers = append(followers, followerID)
    }

    return followers, nil
}

// pushToInbox 推送到粉丝收件箱
func (s *FeedService) pushToInbox(feedID string, authorID int64, followers []int64) error {
    if len(followers) == 0 {
        return nil
    }

    // 批量插入(每次 1000 条)
    batchSize := 1000
    for i := 0; i < len(followers); i += batchSize {
        end := i + batchSize
        if end > len(followers) {
            end = len(followers)
        }

        batch := followers[i:end]
        if err := s.batchInsertInbox(feedID, authorID, batch); err != nil {
            return err
        }
    }

    return nil
}

// batchInsertInbox 批量插入收件箱
func (s *FeedService) batchInsertInbox(feedID string, authorID int64, followers []int64) error {
    // 构造批量插入 SQL
    values := make([]string, len(followers))
    args := make([]interface{}, 0, len(followers)*3)

    for i, followerID := range followers {
        values[i] = "(?, ?, ?)"
        args = append(args, followerID, feedID, authorID)
    }

    sql := fmt.Sprintf(`
        INSERT INTO feed_inbox (user_id, feed_id, author_id, created_at)
        VALUES %s
    `, strings.Join(values, ","))

    // 每条记录的 created_at 使用 NOW()
    sql = strings.ReplaceAll(sql, "?)", "?, NOW())")

    _, err := s.db.Exec(sql, args...)
    return err
}

Feed 读取(从收件箱)

// GetTimeline 获取用户时间线
func (s *FeedService) GetTimeline(userID int64, cursor string, limit int) ([]*Feed, string, error) {
    // 1. 解析 cursor(格式: timestamp_feedID)
    var lastTimestamp int64
    var lastFeedID string
    if cursor != "" {
        parts := strings.Split(cursor, "_")
        lastTimestamp, _ = strconv.ParseInt(parts[0], 10, 64)
        lastFeedID = parts[1]
    }

    // 2. 查询收件箱
    query := `
        SELECT feed_id, author_id, created_at
        FROM feed_inbox
        WHERE user_id = ?
    `
    args := []interface{}{userID}

    if cursor != "" {
        // 游标分页
        query += " AND (created_at < ? OR (created_at = ? AND feed_id < ?))"
        args = append(args, lastTimestamp, lastTimestamp, lastFeedID)
    }

    query += " ORDER BY created_at DESC, feed_id DESC LIMIT ?"
    args = append(args, limit+1)  // 多查 1 条判断是否还有更多

    rows, err := s.db.Query(query, args...)
    if err != nil {
        return nil, "", err
    }
    defer rows.Close()

    var feedIDs []string
    var inboxItems []struct {
        FeedID    string
        AuthorID  int64
        CreatedAt int64
    }

    for rows.Next() {
        var item struct {
            FeedID    string
            AuthorID  int64
            CreatedAt int64
        }
        rows.Scan(&item.FeedID, &item.AuthorID, &item.CreatedAt)
        feedIDs = append(feedIDs, item.FeedID)
        inboxItems = append(inboxItems, item)
    }

    // 3. 判断是否还有更多
    hasMore := false
    if len(feedIDs) > limit {
        hasMore = true
        feedIDs = feedIDs[:limit]
        inboxItems = inboxItems[:limit]
    }

    // 4. 批量查询 Feed 详情
    feeds, err := s.batchGetFeeds(feedIDs)
    if err != nil {
        return nil, "", err
    }

    // 5. 生成下一页 cursor
    var nextCursor string
    if hasMore && len(inboxItems) > 0 {
        last := inboxItems[len(inboxItems)-1]
        nextCursor = fmt.Sprintf("%d_%s", last.CreatedAt, last.FeedID)
    }

    return feeds, nextCursor, nil
}

// batchGetFeeds 批量查询 Feed 详情
func (s *FeedService) batchGetFeeds(feedIDs []string) ([]*Feed, error) {
    if len(feedIDs) == 0 {
        return []*Feed{}, nil
    }

    placeholders := strings.Repeat("?,", len(feedIDs))
    placeholders = placeholders[:len(placeholders)-1]

    query := fmt.Sprintf(`
        SELECT feed_id, user_id, content, media_urls, created_at
        FROM feed
        WHERE feed_id IN (%s)
    `, placeholders)

    args := make([]interface{}, len(feedIDs))
    for i, id := range feedIDs {
        args[i] = id
    }

    rows, err := s.db.Query(query, args...)
    if err != nil {
        return nil, err
    }
    defer rows.Close()

    feeds := make([]*Feed, 0, len(feedIDs))
    for rows.Next() {
        var feed Feed
        var mediaJSON []byte
        rows.Scan(&feed.FeedID, &feed.UserID, &feed.Content, &mediaJSON, &feed.CreatedAt)
        json.Unmarshal(mediaJSON, &feed.MediaURLs)
        feeds = append(feeds, &feed)
    }

    return feeds, nil
}

V1 架构的问题

问题分析:
┌─────────────────────────────────────────────┐
│ 1. 大 V 写入慢                               │
│    - 1000 万粉丝,需要插入 1000 万条         │
│    - 写入时间长(分钟级)                    │
│                                              │
│ 2. 存储成本高                                │
│    - 每个用户收件箱存储 N 条                 │
│    - 总存储 = 用户数 * 关注数 * Feed 数      │
│                                              │
│ 3. 冷启动问题                                │
│    - 新用户关注大 V,收件箱为空              │
│    - 需要回填历史 Feed                       │
│                                              │
│ 4. 无法个性化排序                            │
│    - 只能按时间排序                          │
│    - 无法插入推荐内容                        │
└─────────────────────────────────────────────┘

V2 版本:混合模式(Push + Pull)

架构图

客户端
    ↓
┌────────────────────────────────────────┐
│         Nginx 负载均衡                  │
└────────────────────────────────────────┘
    ↓
┌────────────────────────────────────────┐
│      Feed 服务集群(100 台)            │
│  ┌──────────────────────────────────┐  │
│  │  Feed 发布                       │  │
│  │  - 普通用户: Push 模式           │  │
│  │  - 大 V: 只写发件箱              │  │
│  └──────────────────────────────────┘  │
│  ┌──────────────────────────────────┐  │
│  │  Feed 读取                       │  │
│  │  1. 读收件箱(普通用户 Feed)     │  │
│  │  2. 拉取大 V Feed(实时)         │  │
│  │  3. 合并排序                      │  │
│  └──────────────────────────────────┘  │
└────────────────────────────────────────┘
    ↓
┌─────────┐          ┌──────────────────┐
│  Redis  │          │  MySQL 分库分表   │
│  集群   │          │  - 16 库 * 256 表 │
└─────────┘          └──────────────────┘

核心代码实现

智能 Push/Pull 策略

package service

const (
    BIG_V_THRESHOLD = 100000  // 粉丝数超过 10 万为大 V
)

// PublishFeed 发布 Feed(智能选择 Push/Pull)
func (s *FeedService) PublishFeed(userID int64, content string, mediaURLs []string) (string, error) {
    // 1. 保存 Feed
    feedID := generateFeedID()
    if err := s.saveFeed(feedID, userID, content, mediaURLs); err != nil {
        return "", err
    }

    // 2. 判断是否为大 V
    followersCount, _ := s.getFollowersCount(userID)

    if followersCount < BIG_V_THRESHOLD {
        // 普通用户: Push 模式
        go s.pushToFollowers(feedID, userID)
    } else {
        // 大 V: 不推送,粉丝读取时实时拉取
        log.Infof("大 V 用户 %d 发布 Feed %s,不推送", userID, feedID)

        // 只推送给活跃粉丝(最近 7 天活跃)
        go s.pushToActiveFollowers(feedID, userID)
    }

    // 3. 缓存到 Redis(发件箱)
    go s.cacheToRedis(userID, feedID)

    return feedID, nil
}

// pushToActiveFollowers 推送给活跃粉丝
func (s *FeedService) pushToActiveFollowers(feedID string, userID int64) {
    // 查询活跃粉丝(最近 7 天有登录)
    query := `
        SELECT fr.follower_id
        FROM follow_relation fr
        JOIN user_activity ua ON fr.follower_id = ua.user_id
        WHERE fr.followee_id = ?
          AND fr.status = 1
          AND ua.last_login > DATE_SUB(NOW(), INTERVAL 7 DAY)
    `
    rows, _ := s.db.Query(query, userID)
    defer rows.Close()

    var activeFollowers []int64
    for rows.Next() {
        var followerID int64
        rows.Scan(&followerID)
        activeFollowers = append(activeFollowers, followerID)
    }

    log.Infof("大 V %d 有 %d 个活跃粉丝", userID, len(activeFollowers))

    // 推送给活跃粉丝
    s.pushToInbox(feedID, userID, activeFollowers)
}

// cacheToRedis 缓存到 Redis 发件箱
func (s *FeedService) cacheToRedis(userID int64, feedID string) {
    key := fmt.Sprintf("feed:outbox:%d", userID)
    timestamp := time.Now().UnixMilli()

    // 添加到 Sorted Set
    s.rdb.ZAdd(context.Background(), key, &redis.Z{
        Score:  float64(timestamp),
        Member: feedID,
    })

    // 只保留最新 100 条
    s.rdb.ZRemRangeByRank(context.Background(), key, 0, -101)
}

混合读取(收件箱 + 实时拉取)

// GetTimelineHybrid 混合模式获取时间线
func (s *FeedService) GetTimelineHybrid(userID int64, cursor string, limit int) ([]*Feed, string, error) {
    // 1. 查询用户关注列表
    following, err := s.getFollowing(userID)
    if err != nil {
        return nil, "", err
    }

    // 2. 区分普通用户和大 V
    normalUsers := []int64{}
    bigVs := []int64{}

    for _, followeeID := range following {
        if s.isBigV(followeeID) {
            bigVs = append(bigVs, followeeID)
        } else {
            normalUsers = append(normalUsers, followeeID)
        }
    }

    // 3. 读取收件箱(普通用户的 Feed)
    inboxFeeds, _ := s.getInboxFeeds(userID, limit)

    // 4. 实时拉取大 V 的 Feed
    bigVFeeds, _ := s.pullBigVFeeds(bigVs, limit)

    // 5. 合并排序(多路归并)
    allFeeds := s.mergeFeeds(inboxFeeds, bigVFeeds, limit)

    // 6. 批量查询 Feed 详情
    feeds, err := s.batchGetFeedsWithDetails(allFeeds)

    return feeds, "", nil
}

// pullBigVFeeds 拉取大 V 的最新 Feed
func (s *FeedService) pullBigVFeeds(bigVs []int64, limit int) ([]string, error) {
    if len(bigVs) == 0 {
        return []string{}, nil
    }

    var allFeeds []struct {
        FeedID    string
        CreatedAt int64
    }

    // 并发拉取每个大 V 的最新 Feed
    var wg sync.WaitGroup
    var mu sync.Mutex
    feedChan := make(chan []struct {
        FeedID    string
        CreatedAt int64
    }, len(bigVs))

    for _, bigVID := range bigVs {
        wg.Add(1)
        go func(userID int64) {
            defer wg.Done()

            // 从 Redis 读取大 V 的发件箱(最新 10 条)
            key := fmt.Sprintf("feed:outbox:%d", userID)
            result, err := s.rdb.ZRevRangeWithScores(context.Background(), key, 0, 9).Result()
            if err != nil {
                return
            }

            feeds := make([]struct {
                FeedID    string
                CreatedAt int64
            }, len(result))

            for i, z := range result {
                feeds[i] = struct {
                    FeedID    string
                    CreatedAt int64
                }{
                    FeedID:    z.Member.(string),
                    CreatedAt: int64(z.Score),
                }
            }

            feedChan <- feeds
        }(bigVID)
    }

    go func() {
        wg.Wait()
        close(feedChan)
    }()

    // 收集所有大 V 的 Feed
    for feeds := range feedChan {
        mu.Lock()
        allFeeds = append(allFeeds, feeds...)
        mu.Unlock()
    }

    // 按时间排序
    sort.Slice(allFeeds, func(i, j int) bool {
        return allFeeds[i].CreatedAt > allFeeds[j].CreatedAt
    })

    // 取前 N 条
    if len(allFeeds) > limit {
        allFeeds = allFeeds[:limit]
    }

    feedIDs := make([]string, len(allFeeds))
    for i, feed := range allFeeds {
        feedIDs[i] = feed.FeedID
    }

    return feedIDs, nil
}

// mergeFeeds 合并排序(多路归并)
func (s *FeedService) mergeFeeds(inbox, bigV []string, limit int) []string {
    // 简化版:直接合并并去重
    feedMap := make(map[string]bool)
    result := []string{}

    for _, feedID := range inbox {
        if !feedMap[feedID] {
            result = append(result, feedID)
            feedMap[feedID] = true
        }
    }

    for _, feedID := range bigV {
        if !feedMap[feedID] {
            result = append(result, feedID)
            feedMap[feedID] = true
        }
    }

    if len(result) > limit {
        result = result[:limit]
    }

    return result
}

// isBigV 判断是否为大 V(缓存粉丝数)
func (s *FeedService) isBigV(userID int64) bool {
    key := fmt.Sprintf("user:followers_count:%d", userID)
    count, err := s.rdb.Get(context.Background(), key).Int()
    if err != nil {
        // Redis 未命中,查询 DB
        count, _ = s.getFollowersCount(userID)
        s.rdb.Set(context.Background(), key, count, 1*time.Hour)
    }

    return count >= BIG_V_THRESHOLD
}

V2 性能提升

性能对比:
┌────────────────────────────────────────────────┐
│ 指标              V1 (Pure Push)  V2 (Hybrid)  │
├────────────────────────────────────────────────┤
│ 大 V 发布延迟     分钟级          < 1 秒       │
│ 读取延迟 P99      100ms           150ms        │
│ 存储成本          260 TB/年       170 TB/年    │
│ 支持个性化排序    否              是            │
└────────────────────────────────────────────────┘

优化效果:
 大 V 发布快(不推送全部粉丝)
 存储成本降低 35%
 支持实时拉取大 V 内容
 预留个性化排序接口

V3 版本:高可用 + 智能推荐

架构图

客户端
    ↓
┌────────────────────────────────────────────┐
│         CDN(静态资源)                     │
└────────────────────────────────────────────┘
    ↓
┌────────────────────────────────────────────┐
│         负载均衡(LVS + Nginx)             │
└────────────────────────────────────────────┘
    ↓
┌────────────────────────────────────────────┐
│      Feed 服务集群(200 台)                │
│  - 发布服务                                 │
│  - 读取服务                                 │
│  - 排序服务                                 │
└────────────────────────────────────────────┘
    ↓                     ↓                   ↓
┌─────────┐    ┌──────────────┐    ┌──────────────┐
│  Redis  │    │  Kafka MQ    │    │  推荐服务     │
│  集群   │    │  - 异步推送   │    │  - 个性化     │
└─────────┘    └──────────────┘    └──────────────┘
         ↓              ↓                   ↓
┌────────────────────────────────────────────┐
│     MySQL 分库分表(16 库 * 256 表)        │
└────────────────────────────────────────────┘
         ↓
┌────────────────────────────────────────────┐
│         对象存储 + CDN                      │
│     - 图片、视频                            │
└────────────────────────────────────────────┘

核心优化点

1. 异步推送(Kafka)

// PublishFeed 异步推送
func (s *FeedService) PublishFeed(userID int64, content string) (string, error) {
    // 1. 保存 Feed
    feedID, err := s.saveFeed(userID, content)
    if err != nil {
        return "", err
    }

    // 2. 发送到 Kafka(异步推送)
    event := FeedPublishEvent{
        FeedID:  feedID,
        UserID:  userID,
        Content: content,
    }

    data, _ := json.Marshal(event)
    s.kafka.Publish("feed-publish", data)

    return feedID, nil
}

// Kafka 消费者:推送到粉丝收件箱
type FeedPushConsumer struct {
    db *sql.DB
}

func (c *FeedPushConsumer) Consume(msg *kafka.Message) error {
    var event FeedPublishEvent
    json.Unmarshal(msg.Value, &event)

    // 查询粉丝并推送
    followers, _ := c.getFollowers(event.UserID)

    if len(followers) < BIG_V_THRESHOLD {
        c.pushToInbox(event.FeedID, event.UserID, followers)
    }

    return nil
}

2. 个性化排序(预留接口)

// GetPersonalizedTimeline 个性化时间线
func (s *FeedService) GetPersonalizedTimeline(userID int64, limit int) ([]*Feed, error) {
    // 1. 获取候选 Feed(收件箱 + 大 V 拉取)
    candidateFeeds, _ := s.getCandidateFeeds(userID, limit*3)

    // 2. 调用推荐服务打分排序
    rankedFeeds, _ := s.recommendService.Rank(userID, candidateFeeds)

    // 3. 返回 Top N
    if len(rankedFeeds) > limit {
        rankedFeeds = rankedFeeds[:limit]
    }

    return rankedFeeds, nil
}

// 推荐服务(简化版)
type RecommendService struct{}

func (r *RecommendService) Rank(userID int64, feeds []*Feed) ([]*Feed, error) {
    // 特征提取
    features := r.extractFeatures(userID, feeds)

    // 模型预测(离线训练的模型)
    scores := r.model.Predict(features)

    // 排序
    for i, feed := range feeds {
        feed.Score = scores[i]
    }

    sort.Slice(feeds, func(i, j int) bool {
        return feeds[i].Score > feeds[j].Score
    })

    return feeds, nil
}

func (r *RecommendService) extractFeatures(userID int64, feeds []*Feed) [][]float64 {
    // 特征工程(示例):
    // - 用户特征: 年龄、性别、兴趣标签
    // - Feed 特征: 作者粉丝数、点赞数、发布时间
    // - 交互特征: 用户是否关注作者、历史互动
    // ...

    features := make([][]float64, len(feeds))
    for i, feed := range feeds {
        features[i] = []float64{
            float64(feed.LikesCount),
            float64(feed.CommentsCount),
            float64(time.Since(feed.CreatedAt).Hours()),
            // ...
        }
    }

    return features
}

3. 热点 Feed 缓存

// 热点 Feed 检测
func (s *FeedService) detectHotFeed(feedID string) {
    key := fmt.Sprintf("feed:views:%s", feedID)

    // 浏览数 +1
    count, _ := s.rdb.Incr(context.Background(), key).Result()

    // 如果 5 分钟内浏览超过 1000 次,视为热点
    if count == 1 {
        s.rdb.Expire(context.Background(), key, 5*time.Minute)
    }

    if count > 1000 {
        // 标记为热点 Feed
        s.markAsHot(feedID)

        // 缓存到本地内存(CDN 预热)
        s.localCache.Set(feedID, feed, 10*time.Minute)

        log.Infof("检测到热点 Feed: %s, 浏览数: %d", feedID, count)
    }
}

4. 冷热数据分离

【数据分层】
L1: Redis (热数据)
    - 最近 3 天的 Feed
    - 热点 Feed
    - 用户收件箱(最新 1000 条)

L2: MySQL (温数据)
    - 最近 3 个月的 Feed
    - SSD 存储

L3: HBase/对象存储 (冷数据)
    - 3 个月以前的 Feed
    - HDD 存储
    - 压缩归档
// 读取 Feed(冷热分离)
func (s *FeedService) GetFeed(feedID string) (*Feed, error) {
    // 1. 尝试从 Redis 读取
    cacheKey := fmt.Sprintf("feed:content:%s", feedID)
    cached, err := s.rdb.Get(context.Background(), cacheKey).Result()
    if err == nil {
        var feed Feed
        json.Unmarshal([]byte(cached), &feed)
        return &feed, nil
    }

    // 2. 从 MySQL 读取
    feed, err := s.getFeedFromMySQL(feedID)
    if err == nil {
        // 回写缓存
        data, _ := json.Marshal(feed)
        s.rdb.Set(context.Background(), cacheKey, data, 1*time.Hour)
        return feed, nil
    }

    // 3. 从冷存储读取(HBase)
    feed, err = s.getFeedFromHBase(feedID)
    if err != nil {
        return nil, errors.New("Feed 不存在")
    }

    return feed, nil
}

核心算法与实现

1. 多路归并排序

package merge

import (
    "container/heap"
)

// FeedWithSource Feed 来源
type FeedWithSource struct {
    Feed     *Feed
    SourceID int  // 来源 ID(用于区分不同关注者)
}

// MinHeap 最小堆(按时间倒序)
type MinHeap []FeedWithSource

func (h MinHeap) Len() int           { return len(h) }
func (h MinHeap) Less(i, j int) bool { return h[i].Feed.CreatedAt > h[j].Feed.CreatedAt }
func (h MinHeap) Swap(i, j int)      { h[i], h[j] = h[j], h[i] }

func (h *MinHeap) Push(x interface{}) {
    *h = append(*h, x.(FeedWithSource))
}

func (h *MinHeap) Pop() interface{} {
    old := *h
    n := len(old)
    x := old[n-1]
    *h = old[0 : n-1]
    return x
}

// MergeSortedFeeds 多路归并排序
func MergeSortedFeeds(sources [][]FeedWithSource, limit int) []*Feed {
    h := &MinHeap{}
    heap.Init(h)

    // 初始化堆(每个来源的第一个元素)
    indices := make([]int, len(sources))
    for i, source := range sources {
        if len(source) > 0 {
            heap.Push(h, source[0])
            indices[i] = 1
        }
    }

    // 依次取出最大的元素
    result := make([]*Feed, 0, limit)
    for h.Len() > 0 && len(result) < limit {
        top := heap.Pop(h).(FeedWithSource)
        result = append(result, top.Feed)

        // 从同一来源取下一个元素
        sourceID := top.SourceID
        if indices[sourceID] < len(sources[sourceID]) {
            next := sources[sourceID][indices[sourceID]]
            indices[sourceID]++
            heap.Push(h, next)
        }
    }

    return result
}

2. 布隆过滤器(Feed 去重)

package filter

import (
    "github.com/bits-and-blooms/bloom/v3"
)

type FeedFilter struct {
    bloom *bloom.BloomFilter
}

func NewFeedFilter(expectedFeeds uint) *FeedFilter {
    return &FeedFilter{
        bloom: bloom.NewWithEstimates(expectedFeeds, 0.01),
    }
}

// IsSeen 检查 Feed 是否已看过
func (f *FeedFilter) IsSeen(feedID string) bool {
    return f.bloom.TestString(feedID)
}

// MarkAsSeen 标记 Feed 为已看过
func (f *FeedFilter) MarkAsSeen(feedID string) {
    f.bloom.AddString(feedID)
}

3. 限流(防刷)

// 发布 Feed 限流(用户维度)
func (s *FeedService) PublishFeed(userID int64, content string) (string, error) {
    // 限流检查:每分钟最多发布 5 条
    key := fmt.Sprintf("ratelimit:publish:%d", userID)
    count, _ := s.rdb.Incr(context.Background(), key).Result()

    if count == 1 {
        s.rdb.Expire(context.Background(), key, 1*time.Minute)
    }

    if count > 5 {
        return "", errors.New("发布过于频繁,请稍后再试")
    }

    // 发布 Feed...
}

优化方案

1. Feed 预加载

// 客户端预加载下一页
function preloadNextPage() {
    const lastFeed = document.querySelector('.feed-item:last-child');

    const observer = new IntersectionObserver((entries) => {
        if (entries[0].isIntersecting) {
            // 用户滚动到倒数第三条时,预加载下一页
            loadMoreFeeds();
        }
    });

    // 监听倒数第三条 Feed
    const thirdLastFeed = document.querySelector('.feed-item:nth-last-child(3)');
    observer.observe(thirdLastFeed);
}

2. 图片懒加载

<img data-src="https://cdn.example.com/image.jpg" class="lazy-load" />

<script>
const lazyLoadImages = () => {
    const images = document.querySelectorAll('img.lazy-load');

    const observer = new IntersectionObserver((entries) => {
        entries.forEach(entry => {
            if (entry.isIntersecting) {
                const img = entry.target;
                img.src = img.dataset.src;
                img.classList.remove('lazy-load');
                observer.unobserve(img);
            }
        });
    });

    images.forEach(img => observer.observe(img));
};
</script>

3. Feed 聚合批量查询

// 批量查询 Feed 详情(包含用户信息、统计数据)
func (s *FeedService) BatchGetFeedDetails(feedIDs []string) ([]*FeedDetail, error) {
    var wg sync.WaitGroup
    wg.Add(3)

    var feeds []*Feed
    var users map[int64]*User
    var stats map[string]*FeedStats

    // 并发查询
    go func() {
        defer wg.Done()
        feeds, _ = s.batchGetFeeds(feedIDs)
    }()

    go func() {
        defer wg.Done()
        userIDs := s.extractUserIDs(feeds)
        users, _ = s.batchGetUsers(userIDs)
    }()

    go func() {
        defer wg.Done()
        stats, _ = s.batchGetStats(feedIDs)
    }()

    wg.Wait()

    // 组装数据
    details := make([]*FeedDetail, len(feeds))
    for i, feed := range feeds {
        details[i] = &FeedDetail{
            Feed:  feed,
            User:  users[feed.UserID],
            Stats: stats[feed.FeedID],
        }
    }

    return details, nil
}

4. 数据库索引优化

-- 收件箱查询优化
-- 查询: SELECT feed_id FROM feed_inbox WHERE user_id = ? ORDER BY created_at DESC LIMIT 20
CREATE INDEX idx_user_created ON feed_inbox (user_id, created_at DESC);

-- 覆盖索引(避免回表)
CREATE INDEX idx_user_created_feed ON feed_inbox (user_id, created_at DESC, feed_id);

-- Feed 表查询优化
-- 查询: SELECT * FROM feed WHERE user_id = ? ORDER BY created_at DESC LIMIT 20
CREATE INDEX idx_user_created ON feed (user_id, created_at DESC);

-- 关注关系查询
-- 查询粉丝: SELECT follower_id FROM follow_relation WHERE followee_id = ?
CREATE INDEX idx_followee_id ON follow_relation (followee_id, status);

-- 查询关注: SELECT followee_id FROM follow_relation WHERE follower_id = ?
CREATE INDEX idx_follower_id ON follow_relation (follower_id, status);

监控告警

核心监控指标

【业务指标】
# Feed 发布 QPS
feed_publish_qps

# Feed 拉取 QPS
feed_timeline_qps

# 响应时间 P99
feed_timeline_latency_p99

# 推送成功率
feed_push_success_rate

# 大 V 发布延迟
feed_big_v_publish_latency

【系统指标】
# Redis 缓存命中率
redis_cache_hit_rate

# MySQL 慢查询数
mysql_slow_queries_count

# Kafka 消费延迟
kafka_consumer_lag

Grafana 看板

┌────────────────────────────────────────────┐
│  Feed 流系统实时监控                        │
├────────────────────────────────────────────┤
│  [发布QPS]  [拉取QPS]  [P99延迟]  [命中率] │
│   3600      35万       150ms     95%       │
├────────────────────────────────────────────┤
│  [QPS 曲线(24 小时)]                      │
│  ▁▂▃▅▇█████▇▅▃▂▁                           │
├────────────────────────────────────────────┤
│  [推送模式分布]                             │
│  Push: 80%    Pull: 20%                    │
└────────────────────────────────────────────┘

面试问答

Push 模式 vs Pull 模式,如何选择?

【Push 模式(写扩散)】
优点:
  - 读取快(直接从收件箱读)
  - 实时性好(发布即推送)

缺点:
  - 写入慢(需要写 N 次)
  - 存储成本高
  - 大 V 发布慢

适用:
  - 普通用户(粉丝 < 10 万)
  - 读多写少场景

【Pull 模式(读扩散)】
优点:
  - 写入快(只写一次)
  - 存储成本低
  - 大 V 友好

缺点:
  - 读取慢(需要实时拉取)
  - 实时性稍差

适用:
  - 大 V(粉丝 > 10 万)
  - 写多读少场景

【混合模式(Push + Pull)】
- 普通用户: Push
- 大 V: Pull
- 活跃粉丝: Push(大 V 也推送)
- 兼顾性能和成本

如何解决大 V 问题?

【策略组合】
1. 不推送全部粉丝
   - 只推送给活跃粉丝(最近 7 天登录)
   - 其他粉丝读取时实时拉取

2. 异步推送
   - Kafka 消息队列
   - 分批推送(每批 1000 人)
   - 避免阻塞

3. 分级推送
   - 核心粉丝(经常互动): 立即推送
   - 普通粉丝: 延迟推送
   - 僵尸粉: 不推送

4. 缓存发件箱
   - Redis Sorted Set 缓存大 V 最新 100 条
   - 读取时从缓存拉取

5. Pull 模式兜底
   - 大 V 不推送,粉丝主动拉取

如何保证 Feed 有序?

【时间排序】
1. 发布时记录时间戳(毫秒级)
2. 收件箱按时间戳倒序存储
3. Sorted Set (Redis) 或 ORDER BY (MySQL)

【Cursor 分页】
- Cursor 格式: {timestamp}_{feed_id}
- 避免新 Feed 插入导致漏数据
- 比 offset 分页更可靠

【多路归并】
- 同时拉取多个用户的 Feed
- 按时间戳归并排序(最小堆)
- 取 Top N

【个性化排序】
- 先按时间召回候选 Feed
- 再用模型打分排序
- 兼顾时效性和相关性

如何实现 Feed 去重?

【去重场景】
1. 用户关注 A 和 B
2. B 转发了 A 的 Feed
3. 用户看到 2 次相同内容

【去重策略】
1. 客户端去重
   - 维护已看过的 Feed ID 集合
   - 收到重复 Feed 不展示

2. 服务端去重
   - 布隆过滤器快速判断
   - Redis Set 存储已推送的 Feed ID

3. 数据库约束
   - UNIQUE KEY (user_id, feed_id)
   - 重复插入自动忽略

4. 转发标记
   - 转发的 Feed 显示"转发自 XX"
   - 不算重复

如何实现热度排序?

【热度算法】
热度分数 = 点赞数 * 0.5 + 评论数 * 1.0 + 转发数 * 2.0 + 时间衰减

时间衰减: score / (1 + (当前时间 - 发布时间) / 衰减因子)

【实现】
1. 定期计算热度分数(每分钟)
2. 存储到 Redis Sorted Set
   Key: hot_feeds
   Score: 热度分数
   Value: feed_id

3. 查询热门 Feed
   ZREVRANGE hot_feeds 0 19  # Top 20

【优化】
- 只计算最近 24 小时的 Feed
- 增量更新(只更新有新互动的 Feed)
- 多级缓存(本地 + Redis)

如何防止刷量?

【防刷策略】
1. 限流
   - 用户维度: 每分钟最多发布 5 条
   - IP 维度: 每分钟最多 100 条
   - 设备维度: 设备指纹

2. 风控检测
   - 短时间大量点赞 → 疑似机器人
   - 新注册用户立即大量关注 → 疑似营销号
   - 内容重复度高 → 疑似复制粘贴

3. 验证码
   - 高频操作触发验证码
   - 滑动验证、图形验证

4. 实名认证
   - 发布 Feed 需要手机号验证
   - 一人一号

5. 人工审核
   - 疑似违规内容进入审核队列
   - 人工判断是否封禁

如何实现 @ 提及功能?

【实现步骤】
1. 发布 Feed 时解析 @
   content = "你好 @Alice @Bob"
   mentions = extract_mentions(content)  // [Alice, Bob]

2. 存储 @ 关系
   INSERT INTO feed_mentions (feed_id, mentioned_user_id)

3. 推送通知
   - 发送站内信给 Alice 和 Bob
   - "XX 在 Feed 中提到了你"

4. 渲染时高亮
   - 前端解析 @ 符号
   - 渲染为链接

【数据模型】
CREATE TABLE feed_mentions (
    feed_id VARCHAR(64),
    mentioned_user_id BIGINT,
    INDEX idx_user (mentioned_user_id)
);

如何实现 Feed 删除?

【软删除 vs 硬删除】
软删除(推荐):
  - 标记 status = 0(已删除)
  - 数据仍保留(可恢复)
  - 查询时过滤已删除

硬删除:
  - 物理删除数据
  - 不可恢复
  - 需要清理收件箱

【删除流程】
1. 标记 Feed 为已删除
   UPDATE feed SET status = 0 WHERE feed_id = ?

2. 删除收件箱记录
   DELETE FROM feed_inbox WHERE feed_id = ?
   (异步执行,避免阻塞)

3. 删除缓存
   DEL feed:content:feed_123
   ZREM feed:outbox:10001 feed_123

4. 通知粉丝(可选)
   - 已读该 Feed 的用户,刷新时自动消失

如何实现草稿箱?

【草稿箱表】
CREATE TABLE feed_draft (
    draft_id VARCHAR(64) PRIMARY KEY,
    user_id BIGINT,
    content TEXT,
    media_urls JSON,
    created_at TIMESTAMP,
    updated_at TIMESTAMP
);

【操作】
1. 保存草稿
   - 自动保存(每 30 秒)
   - 手动保存(点击保存按钮)

2. 发布草稿
   - 从草稿箱读取
   - 调用发布接口
   - 删除草稿

3. 删除草稿
   - 软删除或硬删除

从 0 到 1 设计 Feed 流系统,步骤是什么?

【设计步骤】
1. 需求分析(5 分钟)
   - 规模: 1 亿 DAU,35 万 QPS
   - 功能: 发布、读取、点赞、评论
   - 排序: 时间排序为主

2. 容量估算(5 分钟)
   - 日发布 1 亿条
   - 存储 170 TB/年(混合模式)
   - 带宽 100 Gbps

3. API 设计(5 分钟)
   - POST /feeds (发布)
   - GET /feeds/timeline (时间线)
   - POST /feeds/{id}/like (点赞)

4. 数据模型(5 分钟)
   - feed 表(发件箱)
   - feed_inbox 表(收件箱)
   - follow_relation 表(关注关系)

5. 架构设计(20 分钟)
   - V1: Push 模式(写扩散)
   - V2: 混合模式(Push + Pull)
   - V3: 高可用 + 个性化推荐

6. 核心难点(15 分钟)
   - Push vs Pull 选择: 混合模式
   - 大 V 问题: 不推送全部粉丝
   - Feed 有序: 时间戳 + Cursor 分页
   - 多路归并: 最小堆排序

7. 优化方案(5 分钟)
   - 异步推送(Kafka)
   - 冷热分离
   - 缓存优化

【加分项】
- 画架构图
- 写核心代码(Push/Pull)
- 分析性能瓶颈
- 提出监控方案

Prev
03 - IM 即时通讯系统设计
Next
05 - 分布式 ID 生成器设计