缓存模式
Cache-Aside、读写路径、一致性直觉。
Cache-Aside:本站缓存的读写路径
这一课你将能做到
- 画出 Cache-Aside 的完整读路径和写路径,说清每一步「谁先谁后、为什么」
- 解释「为什么删缓存而不是更新缓存」,并能讲出并发下的两种竞态窗口和兜底手段
- 读懂本站现成的 Caffeine 30 秒本地缓存(CacheConfig),理解它和 Redis 组成的「两层缓存」架构
为什么要学
前几课你掌握了命令和 TTL,但真正写业务时要回答一个系统设计问题:MySQL 和 Redis 之间的数据怎么保持一致?
本站例子:课程数据在 MySQL(cf_track 表),首页的 tracks 列表想用 Redis 顶住读流量。什么时候写 Redis?什么时候删?MySQL 更新后缓存里的旧数据怎么办?回答错一步就是「改了课程标题,前端还是旧的」这类线上问题。
Cache-Aside(旁路缓存)是工业界最常用的答案,也是面试「Redis 和 MySQL 一致性」的标准考点。本站已经用 Caffeine 做了第一层(30 秒本地缓存),Redis 是第二层——这一课把两层串起来。
先建立图像
Cache-Aside 的两条路径:
【读路径】
请求 → 查 Redis
├─ 命中 → 直接返回(大多数请求止步于此)
└─ miss → 查 MySQL → 写回 Redis(带 TTL)→ 返回
【写路径】(管理端改课程)
请求 → 更新 MySQL(权威,必须成功)
→ DEL Redis 的相关 key(列表 + 详情,成组失效)
(下次读走 miss,自然回源重建)
关键词:读时填充,写时失效。Redis 里永远只有「读出来的副本」,写请求不直接更新它。
跟着做
步骤 1:手工演练读路径
模拟缓存 miss 和填充(此 key 在第 12 课才真正接入,本步先理解流程):
DEL coderoadmap:tracks:list # 制造 miss
curl -s localhost:8080/api/tracks | head -c 200
docker exec redis-dev redis-cli EXISTS coderoadmap:tracks:list # 接入后应为 1
步骤 2:手工演练写路径
管理端更新课程后,必须删除两类 key:
DEL coderoadmap:tracks:list # 列表缓存(含该课程的聚合结果)
DEL coderoadmap:track:java-fundamentals # 详情缓存
为什么删列表 key:列表里嵌着课程摘要,改了标题不清列表缓存,首页还是旧标题——这就是「失效要成组」。
步骤 3:想清楚「删 vs 更新」
写路径为什么 DEL 而不是 SET 新值:
更新缓存的三个问题:
1. 并发写 A、B 两次,缓存可能被后到的旧写覆盖(AB 乱序)→ 脏数据
2. 更新的值如果还要算(聚合、关联查询),写路径变重
3. 可能根本没人读 → 白更新
删除的问题:只多一次回源 miss(成本可控,TTL 兜底)
步骤 4:并发 miss 的竞态与兜底
线程1 miss ──查MySQL───写缓存(旧值)──┐
线程2 miss ──查MySQL──写缓存──────┴─ 最后写的赢
回源期间 MySQL 数据又变了 → 缓存短暂不一致。解法(按成本递增):
- TTL 兜底:不一致窗口 ≤ TTL,本站可接受
- 短互斥锁:
SET mutex NX EX 5让重建串行(第 5 课的击穿方案) - 延迟双删(进阶):写后延迟几百毫秒再删一次
步骤 5:读本站第一层缓存 CacheConfig
打开 backend/src/main/java/com/codeforge/config/CacheConfig.java:
@EnableCaching
CaffeineCacheManager
expireAfterWrite 30 秒
maximumSize 200 条
cacheNames: roadmaps / tracks / questionsSummary / resources / articles / search