04 - Feed 流系统设计
面试频率: 难度等级: 推荐时长: 45-60 分钟
目录
需求分析与澄清
业务场景
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)
- 分析性能瓶颈
- 提出监控方案