代码仓库参考https://github.com/CuSO41108/zhiguang_be
1. Feed 流的核心难点
Feed 流相比普通列表页,主要有以下问题:
- 首页第一页访问量高,容易形成热点。
- 内容更新、发布、删除较频繁,缓存容易产生陈旧数据。
- 缓存过期时,大量并发请求可能同时回源,造成击穿。
- 返回结果同时包含公共数据和用户个性化数据。
设计目标:
- 热点内容尽量少回源。
- 缓存过期时间分散,避免雪崩。
- 用户态数据不污染公共缓存。
- 内容变更和计数变更控制在可接受的一致性窗口内。
2. Feed 缓存分层设计
公共 Feed 采用"本地页缓存 + Redis 骨架缓存 + Redis item 片段缓存"。
L1:本地 Caffeine 页面缓存
缓存完整的 FeedPageResponse,用于承接最热点的页面请求。
特点:
- 访问速度最快。
- TTL 较短。
- 适合抗瞬时高并发。
相关代码:
- feedPublicCache.getIfPresent(localPageKey)
KnowPostFeedServiceImpl.java (line 102)
L2:Redis 骨架缓存
骨架缓存只保存当前页的文章 ID 顺序,不保存完整文章内容。
Key 格式:
feed:public:ids:{size}:{hourSlot}:{page}
作用:
- 保存一页 Feed 的结构。
- 降低整页大 JSON 缓存带来的重复存储。
- 配合小时分片,减少大范围失效和回源集中。
相关代码:
-
构造 idsKey
KnowPostFeedServiceImpl.java (line 97)
-
读取 ID 列表
KnowPostFeedServiceImpl.java (line 250)
L3:Redis item 片段缓存
每篇文章的基础展示信息单独缓存。
Key 格式:
feed:item:{id}
缓存内容包括标题、描述、封面、作者信息、标签等公共字段。
作用:
- 多个页面可以复用同一篇文章的 item 片段。
- 热点文章可以单独续期。
- 页面骨架和内容片段解耦。
相关代码:
-
构造 item key
KnowPostFeedServiceImpl.java (line 259)
-
批量读取 item 片段
KnowPostFeedServiceImpl.java (line 262)
-
写入 item 片段
KnowPostFeedServiceImpl.java (line 361)
3. 公共 Feed 读取流程
公共 Feed 的读取流程:
- 先查本地 Caffeine 页面缓存。
- 本地未命中,读取 Redis 骨架缓存,拿到文章 ID 列表。
- 根据 ID 列表批量读取 feed:item:{id} 片段。
- 如果骨架和 item 片段完整,则组装页面。
- 组装时从计数系统读取点赞数、收藏数。
- 返回前叠加当前用户的 liked/faved 状态。
- 如果骨架不存在或任意 item 片段缺失,则回源数据库。
- 回源结果写回 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 返回中包含两类数据:
公共数据:
- 标题
- 描述
- 封面
- 作者信息
- 标签
- 点赞数、收藏数
用户态数据:
- 当前用户是否点赞
- 当前用户是否收藏
liked/faved 是用户维度字段,不能写入公共缓存。否则 A 用户的点赞状态可能被 B 用户命中,造成串数据。
处理方式:
- 公共字段走共享缓存。
- liked/faved 在返回前实时查询并覆盖。
- 共享缓存中不保存用户态字段。
相关代码:
-
本地缓存命中后叠加用户态
KnowPostFeedServiceImpl.java (line 111)
-
Redis 片段组装时叠加用户态
KnowPostFeedServiceImpl.java (line 292)
5. Hot Key 探测与动态 TTL
公共 Feed 的 hotkey 统计粒度是单篇知文,不是整页 Feed。
统计 key:
knowpost:{id}
流程:
- Feed 命中本地页缓存或 Redis 片段缓存后,遍历页面中的 item。
- 对每个 item 记录一次 knowpost:{id} 访问。
- HotKeyDetector 使用滑动窗口统计热度。
- 根据访问量分为 NONE / LOW / MEDIUM / HIGH。
- 热度越高,TTL 延长越多。
- 热点内容会延长 feed:item:{id} 的 TTL。
- 详情页访问也会记录同一篇知文的热度,并延长详情缓存和 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
作用:
- 避免大量缓存同一时刻过期。
- 将回源流量打散。
- 降低缓存雪崩风险。
相关代码:
- 公共 Feed 片段 TTL 抖动
KnowPostFeedServiceImpl.java (line 165)
7. Single-Flight 防并发回源
缓存未命中时,如果大量请求同时回源,会造成数据库压力瞬间升高。
处理方式:
- 以同一页的 idsKey 作为 single-flight 粒度。
- 同一页同一时刻只允许一个请求回源。
- 其他请求进入锁后先重新检查 Redis 缓存。
- 如果前一个请求已经回填,则直接使用缓存结果。
- 只有缓存仍不存在时才真正查库。
相关代码:
-
single-flight 锁
KnowPostFeedServiceImpl.java (line 135)
-
锁内二次检查
KnowPostFeedServiceImpl.java (line 138)
8. 一致性策略
8.1 内容变更
内容变更包括:
- 发布
- 编辑标题
- 修改描述
- 更新封面
- 修改可见性
- 删除
详情页缓存会做主动失效,删除 Redis 详情缓存和本地详情缓存。
相关代码:
- 详情缓存失效
KnowPostServiceImpl.java (line 565)
公共 Feed 侧通过短 TTL、小时分片、片段回源重建来控制陈旧窗口。代码中已经建立了内容到页面的反向索引,可用于后续扩展精准失效。
反向索引 key:
feed:public:index:{eid}:{hour}
相关代码:
- 写入反向索引
KnowPostFeedServiceImpl.java (line 356)
8.2 计数变更
点赞、收藏属于高频变更。如果每次变更都删除整页缓存,代价过高。
处理方式:
- 点赞/收藏变更后产生计数事件。
- Kafka 用于异步聚合计数。
- 本地 Spring 事件用于快速修补缓存视图。
- Feed 监听计数事件,通过反向索引找到受影响页面。
- 对本地 Caffeine 页缓存中的目标 item 修补计数字段。
- 权威计数仍来自计数系统。
相关代码:
-
计数事件发布
CounterServiceImpl.java (line 125)
-
Feed 监听本地计数事件
FeedCacheInvalidationListener.java (line 65)
-
通过反向索引查找受影响页面
FeedCacheInvalidationListener.java (line 92)
-
修补本地页面计数
FeedCacheInvalidationListener.java (line 106)
9. "我的发布"列表
"我的发布"是用户维度列表,缓存策略与公共 Feed 不同。
特点:
- 使用本地 Caffeine 页面缓存。
- 使用 Redis 页面缓存。
- Redis key 包含用户 ID。
- TTL 比公共 Feed 更短。
- 命中缓存后仍会覆盖 liked/faved,避免用户态陈旧。
Key 格式:
feed:mine:{userId}:{size}:{page}
相关代码:
-
读取 Redis 页面缓存
KnowPostFeedServiceImpl.java (line 401)
-
写入 Redis 页面缓存
KnowPostFeedServiceImpl.java (line 431)
10. 当前方案的价值
-
热点保护
本地缓存、hotkey 动态 TTL、single-flight 共同降低热点回源压力。
-
缓存复用
Redis 骨架和 item 片段拆分后,单篇内容可以被多个页面复用。
-
避免用户态污染
liked/faved 不进入公共缓存,返回前实时合成。
-
一致性窗口可控
详情缓存主动失效,公共 Feed 通过短 TTL、小时分片和回源重建控制陈旧范围;计数变更通过事件快速修补本地视图。
-
抗雪崩和击穿
随机抖动打散过期时间,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 的热点访问、缓存击穿、缓存雪崩、用户态污染和一致性窗口控制问题。