Feed 流面试笔记

代码仓库参考https://github.com/CuSO41108/zhiguang_be

1. Feed 流的核心难点

Feed 流相比普通列表页,主要有以下问题:

  1. 首页第一页访问量高,容易形成热点。
  2. 内容更新、发布、删除较频繁,缓存容易产生陈旧数据。
  3. 缓存过期时,大量并发请求可能同时回源,造成击穿。
  4. 返回结果同时包含公共数据和用户个性化数据。

设计目标:

  1. 热点内容尽量少回源。
  2. 缓存过期时间分散,避免雪崩。
  3. 用户态数据不污染公共缓存。
  4. 内容变更和计数变更控制在可接受的一致性窗口内。

2. Feed 缓存分层设计

公共 Feed 采用"本地页缓存 + Redis 骨架缓存 + Redis item 片段缓存"。

L1:本地 Caffeine 页面缓存

缓存完整的 FeedPageResponse,用于承接最热点的页面请求。

特点:

  1. 访问速度最快。
  2. TTL 较短。
  3. 适合抗瞬时高并发。

相关代码:

  • feedPublicCache.getIfPresent(localPageKey)
    KnowPostFeedServiceImpl.java (line 102)

L2:Redis 骨架缓存

骨架缓存只保存当前页的文章 ID 顺序,不保存完整文章内容。

Key 格式:

feed:public:ids:{size}:{hourSlot}:{page}

作用:

  1. 保存一页 Feed 的结构。
  2. 降低整页大 JSON 缓存带来的重复存储。
  3. 配合小时分片,减少大范围失效和回源集中。

相关代码:

  • 构造 idsKey

    KnowPostFeedServiceImpl.java (line 97)

  • 读取 ID 列表

    KnowPostFeedServiceImpl.java (line 250)

L3:Redis item 片段缓存

每篇文章的基础展示信息单独缓存。

Key 格式:

feed:item:{id}

缓存内容包括标题、描述、封面、作者信息、标签等公共字段。

作用:

  1. 多个页面可以复用同一篇文章的 item 片段。
  2. 热点文章可以单独续期。
  3. 页面骨架和内容片段解耦。

相关代码:

  • 构造 item key

    KnowPostFeedServiceImpl.java (line 259)

  • 批量读取 item 片段

    KnowPostFeedServiceImpl.java (line 262)

  • 写入 item 片段

    KnowPostFeedServiceImpl.java (line 361)

3. 公共 Feed 读取流程

公共 Feed 的读取流程:

  1. 先查本地 Caffeine 页面缓存。
  2. 本地未命中,读取 Redis 骨架缓存,拿到文章 ID 列表。
  3. 根据 ID 列表批量读取 feed:item:{id} 片段。
  4. 如果骨架和 item 片段完整,则组装页面。
  5. 组装时从计数系统读取点赞数、收藏数。
  6. 返回前叠加当前用户的 liked/faved 状态。
  7. 如果骨架不存在或任意 item 片段缺失,则回源数据库。
  8. 回源结果写回 Redis 骨架、Redis item 片段和本地页面缓存。

关键代码:

  • Redis 片段组装入口

    KnowPostFeedServiceImpl.java (line 248)

  • item 缺失时返回 null,触发回源

    KnowPostFeedServiceImpl.java (line 268)

  • DB 回源

    KnowPostFeedServiceImpl.java (line 154)

  • 回填缓存

    KnowPostFeedServiceImpl.java (line 170)

4. 用户态信息不进入公共缓存

Feed 返回中包含两类数据:

公共数据:

  1. 标题
  2. 描述
  3. 封面
  4. 作者信息
  5. 标签
  6. 点赞数、收藏数

用户态数据:

  1. 当前用户是否点赞
  2. 当前用户是否收藏

liked/faved 是用户维度字段,不能写入公共缓存。否则 A 用户的点赞状态可能被 B 用户命中,造成串数据。

处理方式:

  1. 公共字段走共享缓存。
  2. liked/faved 在返回前实时查询并覆盖。
  3. 共享缓存中不保存用户态字段。

相关代码:

  • 本地缓存命中后叠加用户态

    KnowPostFeedServiceImpl.java (line 111)

  • Redis 片段组装时叠加用户态

    KnowPostFeedServiceImpl.java (line 292)

5. Hot Key 探测与动态 TTL

公共 Feed 的 hotkey 统计粒度是单篇知文,不是整页 Feed。

统计 key:

knowpost:{id}

流程:

  1. Feed 命中本地页缓存或 Redis 片段缓存后,遍历页面中的 item。
  2. 对每个 item 记录一次 knowpost:{id} 访问。
  3. HotKeyDetector 使用滑动窗口统计热度。
  4. 根据访问量分为 NONE / LOW / MEDIUM / HIGH。
  5. 热度越高,TTL 延长越多。
  6. 热点内容会延长 feed:item:{id} 的 TTL。
  7. 详情页访问也会记录同一篇知文的热度,并延长详情缓存和 Feed item 片段。

相关代码:

  • Feed 中记录单篇内容热度

    KnowPostFeedServiceImpl.java (line 187)

  • 延长 feed:item:{id}

    KnowPostFeedServiceImpl.java (line 196)

  • 详情页记录热度并续期

    KnowPostServiceImpl.java (line 543)

6. 随机抖动抗雪崩

缓存写入时会在基础 TTL 上增加随机抖动。

例如:

baseTtl = 60s jitter = random(0~30) actualTtl = 60s + jitter

作用:

  1. 避免大量缓存同一时刻过期。
  2. 将回源流量打散。
  3. 降低缓存雪崩风险。

相关代码:

  • 公共 Feed 片段 TTL 抖动
    KnowPostFeedServiceImpl.java (line 165)

7. Single-Flight 防并发回源

缓存未命中时,如果大量请求同时回源,会造成数据库压力瞬间升高。

处理方式:

  1. 以同一页的 idsKey 作为 single-flight 粒度。
  2. 同一页同一时刻只允许一个请求回源。
  3. 其他请求进入锁后先重新检查 Redis 缓存。
  4. 如果前一个请求已经回填,则直接使用缓存结果。
  5. 只有缓存仍不存在时才真正查库。

相关代码:

  • single-flight 锁

    KnowPostFeedServiceImpl.java (line 135)

  • 锁内二次检查

    KnowPostFeedServiceImpl.java (line 138)

8. 一致性策略

8.1 内容变更

内容变更包括:

  1. 发布
  2. 编辑标题
  3. 修改描述
  4. 更新封面
  5. 修改可见性
  6. 删除

详情页缓存会做主动失效,删除 Redis 详情缓存和本地详情缓存。

相关代码:

  • 详情缓存失效
    KnowPostServiceImpl.java (line 565)

公共 Feed 侧通过短 TTL、小时分片、片段回源重建来控制陈旧窗口。代码中已经建立了内容到页面的反向索引,可用于后续扩展精准失效。

反向索引 key:

feed:public:index:{eid}:{hour}

相关代码:

  • 写入反向索引
    KnowPostFeedServiceImpl.java (line 356)

8.2 计数变更

点赞、收藏属于高频变更。如果每次变更都删除整页缓存,代价过高。

处理方式:

  1. 点赞/收藏变更后产生计数事件。
  2. Kafka 用于异步聚合计数。
  3. 本地 Spring 事件用于快速修补缓存视图。
  4. Feed 监听计数事件,通过反向索引找到受影响页面。
  5. 对本地 Caffeine 页缓存中的目标 item 修补计数字段。
  6. 权威计数仍来自计数系统。

相关代码:

  • 计数事件发布

    CounterServiceImpl.java (line 125)

  • Feed 监听本地计数事件

    FeedCacheInvalidationListener.java (line 65)

  • 通过反向索引查找受影响页面

    FeedCacheInvalidationListener.java (line 92)

  • 修补本地页面计数

    FeedCacheInvalidationListener.java (line 106)

9. "我的发布"列表

"我的发布"是用户维度列表,缓存策略与公共 Feed 不同。

特点:

  1. 使用本地 Caffeine 页面缓存。
  2. 使用 Redis 页面缓存。
  3. Redis key 包含用户 ID。
  4. TTL 比公共 Feed 更短。
  5. 命中缓存后仍会覆盖 liked/faved,避免用户态陈旧。

Key 格式:

feed:mine:{userId}:{size}:{page}

相关代码:

  • 读取 Redis 页面缓存

    KnowPostFeedServiceImpl.java (line 401)

  • 写入 Redis 页面缓存

    KnowPostFeedServiceImpl.java (line 431)

10. 当前方案的价值

  1. 热点保护

    本地缓存、hotkey 动态 TTL、single-flight 共同降低热点回源压力。

  2. 缓存复用

    Redis 骨架和 item 片段拆分后,单篇内容可以被多个页面复用。

  3. 避免用户态污染

    liked/faved 不进入公共缓存,返回前实时合成。

  4. 一致性窗口可控

    详情缓存主动失效,公共 Feed 通过短 TTL、小时分片和回源重建控制陈旧范围;计数变更通过事件快速修补本地视图。

  5. 抗雪崩和击穿

    随机抖动打散过期时间,single-flight 控制同页并发回源。

11. 面试总结口径

我的 Feed 流采用分层缓存设计。公共 Feed 先查本地 Caffeine 整页缓存,本地未命中后读取 Redis 的 idsKey 骨架缓存,拿到当前页文章 ID,再批量读取 feed:item:{id} 片段组装页面。如果骨架或片段不完整,则通过 single-flight 控制同一页只有一个请求回源数据库,并回填 Redis 骨架、Redis item 片段和本地缓存。

公共缓存只保存公共展示数据,liked/faved 这类用户态字段在返回前实时叠加,避免不同用户之间串数据。热点保护上,公共 Feed 按单篇知文 knowpost:{id} 做滑动窗口热度统计,根据热度动态延长详情缓存和 item 片段缓存 TTL。计数变化通过事件驱动修补本地页面视图,权威计数由计数系统提供。整体上,这套方案重点解决 Feed 的热点访问、缓存击穿、缓存雪崩、用户态污染和一致性窗口控制问题。

相关推荐
智者知已应修善业1 分钟前
【51单片机8位数码管同时倒计时从9999】2024-1-25
c++·经验分享·笔记·算法·51单片机
一只叫煤球的猫3 分钟前
ThreadForge 源码解读二:一个 Task 从 submit 到完成,内部到底发生了什么?
java·后端·面试
洛水水4 分钟前
【力扣100题】86.柱状图中最大的矩形
算法·leetcode·职场和发展
AOwhisky16 分钟前
Redis 学习笔记(第四期):高可用与集群(哨兵 + Cluster + 容器化)
linux·运维·数据库·redis·笔记·学习·缓存
2501_938176881 小时前
924期权赚了2000倍真的吗?
笔记
yzqy_1 小时前
AMD AI 开发者计划学习笔记:从 ROCm 到 Ryzen AI,理解 AMD 的 AI 开发生态
人工智能·笔记·学习·datawhale·amdev
疯狂打码的少年1 小时前
【程序语言与编译】正规式与有限自动机的等价转换
笔记
是上好佳佳佳呀1 小时前
【LangChain|Day03】LangChain 链式调用 Chains 笔记
笔记·langchain
AI人工智能+电脑小能手2 小时前
【大白话说Java面试题 第110题】【并发篇】第10题:CAS 存在哪些问题?
java·开发语言·面试
秋92 小时前
Python工程师面试常问提问和回答(AI工程化方向 · 2026版)
人工智能·python·面试