面试知识库

05 - Feed 流与 Timeline#

知识图谱#

推模式实现#

发布:Fan-out 到粉丝收件箱#

文件: src/main/java/com/hmdp/service/impl/BlogServiceImpl.java

读取:游标分页#

ScrollResult 游标算法详解#

关键点:offset 不是传统的页码偏移,而是”上一批结果中最小 score 出现的次数”。这样即使有多个相同时间戳的博客,也不会漏掉或重复。

博客点赞(ZSet)#


面试 Q&A#

Q1: 推模式和拉模式有什么区别?为什么选推模式?#

维度推模式(写扩散)拉模式(读扩散)
写入发布时推给所有粉丝 O(粉丝数)只写自己的发件箱 O(1)
读取直接读自己的收件箱 O(1)聚合所有关注人的发件箱 O(关注数)
延迟读取无延迟读取需要实时聚合排序
存储每个粉丝一份副本,冗余大无冗余

选推模式的原因:

  1. 本项目是点评平台,不是微博——用户粉丝数中等(几百到几千),写扩散成本可控
  2. Feed 读取频率远高于发布频率(读多写少),优化读端更重要
  3. 实现简单,不需要复杂的聚合排序逻辑

追问:如果一个大 V 有 100 万粉丝,推模式还行吗?#

不行。100 万次 ZADD 操作耗时可能达到秒级,阻塞发布接口。

解决方案:推拉结合

  • 粉丝数 < 阈值(如 10000):推模式
  • 粉丝数 > 阈值(大V):粉丝主动拉取
  • 或者发布时异步推送(放入消息队列分批推送)

再追问:为什么不用消息队列异步推送?#

当前的同步推送 for (Follow follow : follows) 确实会阻塞发布接口。可以改为:

  1. 发布时只写 DB
  2. 发 Kafka 消息
  3. 消费者分批推送到粉丝的 Feed ZSet

项目中秒杀已经有了 Kafka 基础设施,复用即可。

Q2: 为什么用 ZREVRANGEBYSCORE 而不是 ZREVRANGE?#

ZREVRANGE 按下标分页(类似 LIMIT offset, count),但 Feed 流是动态数据——两次请求之间可能有新博客推送进来,导致下标偏移:

第一次: ZREVRANGE feed 0 1 → [blog10, blog9]
此时新推送了 blog11
第二次: ZREVRANGE feed 2 3 → [blog9, blog8] ← blog9 重复了!
plaintext

ZREVRANGEBYSCORE 按 score(时间戳)范围查询,不受新元素插入影响:

第一次: ZREVRANGEBYSCORE feed +inf 0 LIMIT 0 2 → [blog10(1000), blog9(900)]
此时新推送了 blog11(1100)
第二次: ZREVRANGEBYSCORE feed 900 0 LIMIT 1 2 → [blog8(800), blog7(700)]
plaintext

追问:offset 参数的作用是什么?#

处理同分数元素。如果多个博客恰好在同一毫秒发布(score 相同),LIMIT 的 offset 可以跳过上一批已经返回过的同分数元素。

具体逻辑:

  • 每次查询后,记录最小 score (minTime) 和该 score 出现的次数 (os)
  • 下次查询时,max = minTime, offset = os
  • 这样跳过已经返回过的同分数元素,不重复不遗漏

Q3: Feed 流的数据会无限增长吗?#

是的,当前实现没有清理机制。每个用户的 feed:{userId} ZSet 会随时间无限增长。

改进方案:

  1. 定时清理:定期 ZREMRANGEBYSCORE 删除 N 天前的数据
  2. 限制大小:保持 ZSet 最多 1000 条,ZREMRANGEBYRANK 删除最旧的
  3. 冷热分离:近期 Feed 在 Redis,历史 Feed 查 DB

踩坑点#

踩坑点说明面试官考察意图
同步推送阻塞saveBlog 中 for 循环同步 ZADD大V场景性能
Feed 无限增长没有清理策略内存管理
页大小硬编码为 2offset, 2 写死在代码中可配置性
System.currentTimeMillis() 精度毫秒级,高并发下可能多个博客同 score时间精度
ORDER BY FIELD 拼接直接拼接 id 到 SQL,理论上有注入风险安全意识

加分回答#

  • 对比 Twitter 的 Fanout Service:推模式 + 异步 + 优先级队列
  • 提到 Feed 流的三种模型及其适用场景(推/拉/推拉结合)
  • 分析 ZSet 的内存占用:每个元素 score(8 bytes) + member(变长),大规模场景需要估算
  • 提到可以用 Redis Stream 替代 ZSet 做 Feed 流(原生支持消费组和游标)
  • 讨论 isBlogLiked 在未登录时的处理(UserHolder.getUser() 可能为 null)

关联文档#