时间线(Timeline / Feed)是微型博客的「首页命脉」——用户打开 App 看到的每一帧内容,都来自「关注关系」与「时间线生成」这两套系统。Twitter 早期 1 亿用户时,时间线系统就是其最核心的技术壁垒。
本文系统讲解:关注关系建模、推模式 Fanout、拉模式、混合架构、热搜/榜单时间线、分页与一致性。
一、关注关系数据模型
1.1 关注表
CREATE TABLE follows (
follower_id BIGINT NOT NULL, -- 关注者
followee_id BIGINT NOT NULL, -- 被关注者
created_at TIMESTAMPTZ DEFAULT now(),
PRIMARY KEY (follower_id, followee_id)
);
CREATE INDEX idx_follows_followee ON follows(followee_id, created_at DESC);
1.2 核心查询
-- 我关注了谁(关注列表)
SELECT followee_id FROM follows WHERE follower_id = :uid ORDER BY created_at DESC;
-- 谁关注了我(粉丝列表)
SELECT follower_id FROM follows WHERE followee_id = :uid ORDER BY created_at DESC;
-- 判断是否互关
SELECT count(*) FROM follows
WHERE follower_id = :a AND followee_id = :b;
-- 关注人数/粉丝数(冗余计数)
UPDATE users SET following_count = following_count + 1 WHERE id = :follower;
UPDATE users SET follower_count = follower_count + 1 WHERE id = :followee;
1.3 双向关系与缓存
- 关注动作:写一行 follows + 更新两个计数
- 缓存:Redis Set 缓存关注/粉丝列表(超大账号例外)
following:{uid} → Set[followee ids]
followers:{uid} → Set[follower ids]
一句话总结:关注关系的本质是「一张有向边表」——点赞/取关即增删一行,计数用冗余字段保证读 O(1)。
二、推模式:Fanout on Write
2.1 原理
发布时推送:作者发帖 → 系统把帖子副本写入每个粉丝的「个人时间线」列表。
用户 A 发帖
→ 查出 A 的粉丝列表 [B, C, D]
→ 向 timeline:B, timeline:C, timeline:D 各写入一条帖子引用
→ B/C/D 刷新时间线时直接读自己的列表(零计算)
2.2 优点与代价
| 优点 | 代价 |
|---|---|
| 读时间线极快(O(1) 读列表) | 写放大:粉丝越多,写入越多 |
| 天然支持取关回退 | 大 V 发帖 = 千万级写入风暴 |
| 实现简单 | 存储冗余(同帖多副本) |
2.3 存储
Redis List / SortedSet:
timeline:{uid} = 帖子 id 列表(按时间倒序)
新帖写入:LPUSH timeline:{B} {post_id}
容量问题:长期积累无限增长
裁剪:只保留最近 N 条(如 500 条)
越界 → 从粉丝时间线移除 → 靠拉模式兜底
一句话总结:推模式用「写放大换读简化」——粉丝时间线是预先生成的快照,读起来零计算,但大 V 的粉丝量决定了写成本的上限。
三、拉模式:Fanout on Read
3.1 原理
读取时合并:用户刷新时间线 → 系统实时查询「我关注的所有人的最新帖子」并合并排序。
用户 B 刷新时间线
→ 查出 B 的关注列表 [A, E, F]
→ 并行查询各人最新帖子
→ 合并 → 排序 → 返回前 N 条
3.2 优点与代价
| 优点 | 代价 |
|---|---|
| 无写放大(发帖零副本) | 读开销大(多次查询 + 合并) |
| 适合低活跃用户 | 关注多时查询放大 |
| 存储省 | 冷启动/刷新慢 |
3.3 合并查询
-- 拉模式核心查询(多关注合并)
SELECT * FROM posts
WHERE author_id IN (SELECT followee_id FROM follows WHERE follower_id = :uid)
ORDER BY created_at DESC
LIMIT 20;
性能优化:
- 按关注列表分片并行查各作者最新帖
- 各作者仅取前几篇,再内存合并
- 结果缓存短 TTL(如 30s)
一句话总结:拉模式用「读合并换写零副本」——发帖即存一篇,读取时实时聚合,适合粉丝少、关注多的普通用户。
四、混合架构:大 V 例外
4.1 为什么需要混合
纯推模式:大 V(千万粉丝)发一条帖 = 千万次写入,瞬时打爆 Redis。
纯拉模式:普通用户每次刷新都要查询几十上百个作者,慢且费。
混合方案:按账号「热度」分流——大 V 用拉模式,普通用户用推模式。
4.2 混合时间线
用户 B 刷新时间线:
1. 拉取「普通关注者」的推模式时间线(timeline:{B})
2. 拉取「大 V 关注者」的最新帖子(实时查询,数量少)
3. 合并排序 → 分页返回
大 V 判定:粉丝数 > 阈值(如 10 万)→ 走拉模式
粉丝数 ≤ 阈值 → 走推模式
4.3 时间线生成伪代码
func GetTimeline(ctx, uid, page) []Post {
// 1. 推模式部分
pushed := redis.LRange(ctx, "timeline:"+uid, 0, 500)
// 2. 拉模式部分(大 V)
bigVs := GetBigVFollowees(ctx, uid) // 粉丝超阈值的大 V
pulled := []Post{}
for _, v := range bigVs {
posts := QueryRecentPosts(ctx, v, 5) // 每大 V 取 5 条
pulled = append(pulled, posts...)
}
// 3. 合并排序(按时间倒序)
merged := mergeSort(pushed, pulled)
return paginate(merged, page)
}
4.4 阈值与自适应
- 静态阈值:固定粉丝数(如 10 万)
- 动态阈值:按系统写入负载调整
- 触达补偿:大 V 发帖后,活跃粉丝的 Timeline 主动补写(增量 push)
一句话总结:混合架构的本质是「推拉分治」——按粉丝量把作者分流,普通用户靠推、大 V 靠拉,合并时各取所需。
五、热搜与榜单时间线
5.1 热搜榜
独立于关注关系,基于全站行为:
热度分 = 互动量(赞/转/评)× 权重 - 时间衰减
计算:实时流聚合 → 定期重算榜单
Redis ZSet:hot_posts(score = 热度分)→ 取 Top N
5.2 榜单时间线
推荐时间线(非关注驱动):
- 基础召回:热搜、热门作者、同兴趣内容
- 粗排/精排:CTR 预估模型
- 去重与多样性:已看内容过滤、作者去重
5.3 与关注时间线的差异
| 维度 | 关注时间线 | 热搜/推荐时间线 |
|---|---|---|
| 驱动 | 关注关系 | 全站行为/算法 |
| 更新 | 新帖即新 | 按热度重算 |
| 实时性 | 强 | 中(榜单周期性刷新) |
| 生成 | 推拉混合 | 流聚合 + 排序 |
一句话总结:热搜与推荐时间线是「内容发现」的另一条腿——它们不靠关注关系,而靠热度与算法把好内容推给「还没关注它的人」。
六、分页与一致性
6.1 时间线分页
游标分页(推荐):
SELECT ... WHERE created_at < :cursor ORDER BY created_at DESC LIMIT 20
传入上一页最后一条的 created_at
避免 offset 分页的「翻页时新增导致的重复/跳页」
6.2 一致性挑战
- 拉取时新增帖子 → 游标分页天然处理
- 删除帖子 → 时间线里出现「已删除」占位 → 客户端过滤
- 取关后 → 推模式需从时间线移除该作者帖子(异步清理)
- 缓存与 DB 偏差 → 短 TTL + 主动失效
6.3 降级策略
Redis 故障 → 拉模式兜底(直接查 DB)
大 V 写放大 → 转拉模式/批量异步写入
时间线超长 → 裁剪 + 提示「查看更早请进主页」
一句话总结:时间线的健壮性在于「分页正确 + 降级兜底」——游标分页解决翻页问题,拉模式兜底解决缓存故障,裁剪解决无限增长。
七、总结
微型博客时间线系统的演进路径可以概括为:先建模、再选模式、后混合、终治理。关注关系用「有向边表 + 冗余计数」支撑;时间线生成在「推模式的写放大」与「拉模式的读放大」之间权衡;混合架构按粉丝量分流,让大 V 与普通用户各得其所;热搜榜单用流聚合与热度分补充「发现」能力;分页与降级保证体验稳定。
时间线是微型博客性能与体验的双核心——读得快,用户觉得流畅;写得稳,系统扛得住。把推拉混合做对,就掌握了社交平台最核心的架构能力。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。