CodeRoadMap
首页路线题库资源库文章社区
CodeRoadMap

程序员的学习成长路线图。内容免费开放,登录后可同步路线图与课时进度。

由 程序员柏风 独立维护 · 关于作者

学习

  • 路线图
  • 课程
  • 面试题
  • 文章
  • 我

更多

  • 资源
  • 社区
  • 关于作者
  • 内容说明

© 2026 CodeRoadMap

津ICP备2026012044号-1·津公网安备12011002026155号·coderoadmap@126.com
Redis 缓存入门/课时/缓存模式
RawBlame
课时目录12 节

Redis 缓存入门

  1. 01Redis 是什么
  2. 02CLI 与键命名
  3. 03String 与计数
  4. 04Hash / List / Set / ZSet
  5. 05过期与淘汰
  6. 06缓存模式
  7. 07Spring Data Redis
  8. 08会话与 Sa-Token
  9. 09Docker 运行 Redis
  10. 10Pub/Sub 入门
  11. 11测试与监控
  12. 12综合练习

缓存模式

Cache-Aside、读写路径、一致性直觉。

30 分钟

Cache-Aside:本站缓存的读写路径

这一课你将能做到

  1. 画出 Cache-Aside 的完整读路径和写路径,说清每一步「谁先谁后、为什么」
  2. 解释「为什么删缓存而不是更新缓存」,并能讲出并发下的两种竞态窗口和兜底手段
  3. 读懂本站现成的 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

TrackService.listAll() 上有:

@Cacheable(cacheNames = CacheConfig.TRACKS, unless = "#result == null || #result.isEmpty()")
public List<Track> listAll() { ... }

deleteTrack 上有 @CacheEvict(cacheNames = ..., allEntries = true)——这就是写路径失效,只不过是本地层的。

步骤 6:两层缓存的分工图

请求 → Caffeine(进程内,30s,扛最热的读,零网络开销)
      → miss → Redis(跨实例共享,5 分钟级 TTL,扛第二波)
        → miss → MySQL

本站单机开发时只有 Caffeine 生效;引入 Redis 后,多实例各自的 Caffeine 失效不同步的问题由「30 秒短 TTL + Redis 兜底」消化。跨实例主动广播失效方案见第 10 课 Pub/Sub。

讲清楚要点

  1. Cache-Aside 由应用代码自己维护缓存(区别于 Redis 的 read-through 模式),好处是逻辑完全可控,坏处是每个读接口都要写样板代码——所以第 12 课会把它封装成 TrackCacheService。
  2. 写路径铁律:先 MySQL 后 DEL 缓存——顺序反了,MySQL 写失败时缓存已被删(无害),但「缓存更新成功而 MySQL 失败」就是脏缓存。
  3. 删缓存失败要有兜底:TTL 保证最终一致;更严格的场景加重试或订阅 binlog(进阶)。
  4. 失效要成组:改一条 track 影响列表 + 详情两个 key,漏删一个就是一个不一致 bug。
  5. 两层缓存的价值:Caffeine 挡住进程内重复读(0 网络成本),Redis 挡住跨实例重复读(1 次网络),MySQL 只承担真正的回源。
  6. 一致性强度是权衡出来的:本站用「短 TTL + 写时失效」达到最终一致,秒级窗口,对内容型站点足够;交易系统要求强一致就别把这类数据放缓存。

你可能会懵的地方

  • 为什么是先更新 MySQL 再删缓存,而不是反过来? 反过来时:删了缓存 → MySQL 还没写完 → 并发读 miss 回源读了旧库值并写回缓存 → MySQL 写完 → 缓存留下脏数据直到 TTL。先写库后删,最坏是短暂 miss,不会留下脏数据。

  • @CacheEvict(allEntries=true) 会不会太暴力? 暴力但正确:track 数量少(几十个量级),全清的 miss 风暴可忽略;比精确清更不会漏。

  • 为什么不直接只用 Caffeine? 多实例下各进程各一份,更新后 A 实例清了,B 实例还持有最多 30 秒旧数据;Redis 提供跨实例一致视图和更大容量。

  • 「延迟双删」本站用了吗? 没有。它解决「回源线程把旧值写回」的窗口,本站用 TTL 兜底 + 数据低频变更,没必要上复杂度——工具尺寸匹配问题规模。

  • 写路径删缓存失败了怎么办? 本站依靠 TTL 到期自然重建(最长 5 分钟旧数据);更严格系统用重试队列或 Canal 订阅 binlog 删缓存。

错法对照

错法问题正解
写路径只更新 MySQL 不动缓存长期旧数据更新库后 DEL 相关 key
删了详情忘了删列表列表里旧摘要失效成组:list + detail
先删缓存再写 MySQL回源旧值写回脏缓存先写库后删缓存
写路径直接 SET 新值进缓存并发乱序脏写、白算聚合写路径只删不更新
缓存里存写路径的中间态缓存变成权威源缓存只放只读副本
只用 Caffeine 且无 TTL 上限多实例长期不一致expireAfterWrite 30s(本站现状)

卡点急救

  1. 改了数据接口还是旧值:逐层查——Caffeine(≤30s 自己消失)→ Redis(DEL 对应 key)→ MySQL(确认写入成功)。
  2. DEL 了的 key 又出现旧值:并发回源写回,属 TTL 兜底窗口,等下一轮或加互斥锁。
  3. @Cacheable 方法内部调用不生效:同类内部方法调用不走代理,改从外部 Bean 调用或注入自身代理。
  4. 缓存对象反序列化报错:改了 DTO 结构,旧 JSON 字段对不上——DEL 全部相关 key 重建。
  5. Redis 挂了读路径全变慢:回源风暴。代码里 Redis 异常要 catch 降级直查 MySQL(第 12 课的封装会做)。

面试常问

  • Redis 和数据库的一致性怎么保证? Cache-Aside:读时填充、写时先更新 MySQL 再 DEL 缓存,靠 TTL 最终一致。要讲清楚「为什么删不更新」和并发窗口,可提延迟双删/binlog 订阅作为进阶方案。

  • 为什么删缓存而不是更新缓存? 并发写乱序会覆盖出脏数据;更新可能白做(没人读);删除让下次读自然回源,逻辑最简单、最不易错。

  • 说说你们的多级缓存。 Caffeine 30 秒/200 条本地层(@Cacheable/@CacheEvict)+ Redis 共享层(Cache-Aside,5 分钟±抖动)+ MySQL 权威层;单机开发可整体降级。兼顾性能与跨实例一致性。

自测三问

  1. 完整画出读路径和写路径的每一步顺序,标出哪一步失败会有什么后果。
  2. 先删缓存后写 MySQL 会出什么问题?推演一遍时序。
  3. 本站两层缓存各层的 TTL 和职责是什么?为什么 Caffeine 在前、Redis 在后?

小结一句话

Cache-Aside = 读时填充、写时先库后删、失效成组、TTL 兜底最终一致;本站是 Caffeine 30 秒 + Redis 5 分钟的两层实践。

延伸

  • Cache-Aside 模式(微软云设计模式)
  • 下一课:Spring Data Redis 接入——把今天的模式翻译成 Java:依赖、配置、RedisConfig 逐行读。
加载本节练习…
← 上一课过期与淘汰下一课 →Spring Data Redis