Cache Aside 模式是什么?读写缓存时有哪些坑?
简化版
Cache Aside 是最常见的旁路缓存模式:读时先查缓存,未命中再查数据库并回填;写时先更新数据库,再删除缓存。它简单通用,但要处理缓存击穿、并发回填、删除失败、延迟双删、缓存雪崩和脏数据窗口。
详细版
Cache Aside 的读流程是:
读请求 -> 查缓存
命中 -> 返回
未命中 -> 查数据库 -> 写缓存 -> 返回
写流程常见是:
写请求 -> 更新数据库 -> 删除缓存
为什么不是更新缓存?因为缓存可能有复杂聚合、并发写覆盖、部分字段更新等问题,删除后下次读再回填更简单。
主要风险包括缓存未命中并发打 DB、缓存删除失败、读写并发导致短暂旧值、热点 key 失效、缓存无 TTL 或 TTL 集中到期。面试要说明这是最终一致方案,不是强一致事务。
完整版教学
一、Cache Aside 解决什么问题
Cache Aside 又叫旁路缓存,应用代码同时访问缓存和数据库。缓存不主动和数据库绑定,应用决定什么时候读缓存、回填缓存、删除缓存。
它适合读多写少的业务。缓存承担高频读,数据库保存权威数据。这样既能降低数据库压力,又保持数据源清晰。
记忆钩子:Cache Aside 的核心是“读旁路回填,写数据库删缓存”。
二、读流程为什么要回填
读请求先查 Redis,如果命中直接返回;如果没命中,再查数据库,把结果写入 Redis,然后返回给用户。
GET cache
|
命中 -> 返回
|
未命中 -> SELECT DB -> SET cache TTL -> 返回
回填时要设置 TTL,避免缓存永久占用内存,也能让异常情况下的数据最终自然过期。对于不存在的数据,也可以缓存空值短 TTL,防止缓存穿透。
三、写流程为什么常用“更新 DB 后删缓存”
写入时常见做法是先更新数据库,再删除缓存。下次读缓存未命中,会从数据库加载新值。
如果写缓存而不是删缓存,会遇到更多问题:缓存结构可能不是数据库单行,可能是聚合结果;并发写时旧请求可能覆盖新缓存;写数据库成功但写缓存失败也会造成不一致。
| 写法 | 风险 |
|---|---|
| 更新 DB 后更新缓存 | 并发覆盖、聚合难维护 |
| 更新 DB 后删除缓存 | 短暂不一致但更简单 |
| 先删缓存后更新 DB | 并发读可能回填旧值 |
四、读写并发下为什么仍可能脏
即使采用“更新 DB 后删缓存”,也存在短暂窗口。比如读请求先查缓存未命中,随后查到旧 DB 值;写请求更新 DB 并删除缓存;读请求最后把旧值回填进缓存。
T1 读:缓存未命中
T2 读:查询 DB 旧值
T3 写:更新 DB 新值
T4 写:删除缓存
T5 读:把旧值写回缓存
这个场景概率不高但真实存在。可以用延迟双删、版本号、较短 TTL、互斥回填、消息队列重删等方式降低风险。
五、删除失败怎么办
更新数据库成功后,如果删除缓存失败,就会留下旧缓存。工程上不能只写一行删除命令就结束,要有重试和补偿。
常见方案包括同步重试、异步重试、消息队列、订阅 binlog 删除缓存。强一致要求越高,方案越复杂。
更新 DB 成功
|
删除缓存失败
|
记录重试任务 / MQ / binlog 消费
|
最终删除缓存
如果业务能接受短暂不一致,TTL 也是兜底;如果不能接受,要重新评估是否适合缓存。
六、并发回填和击穿问题
热点 key 过期后,大量请求同时未命中,会一起打到数据库,这就是击穿。Cache Aside 必须配合互斥锁、singleflight、逻辑过期或热点预热。
假设热点 key 每秒 5000 QPS,缓存过期瞬间如果 1000 个请求同时查 DB,数据库可能被压垮。加互斥回填后,只有一个请求查 DB,其他请求等待或返回旧值。
| 方案 | 适用 |
|---|---|
| 互斥锁回填 | 强一致读较重要 |
| 逻辑过期 | 可返回短暂旧值 |
| 热点预热 | 明确热点数据 |
七、TTL 设计别太机械
TTL 不能所有 key 都设置同一时间,否则容易集中失效形成雪崩。可以给 TTL 加随机抖动,比如基础 30 分钟,加随机 0 到 5 分钟。
TTL = 1800 + random(0, 300)
缓存空值也要设置较短 TTL,避免不存在 key 被反复打到数据库。不同业务数据要按变化频率、热点程度和一致性要求设置不同 TTL。
八、常见误区与追问
- 误区:Cache Aside 能保证强一致。 它通常是最终一致方案,存在短暂不一致窗口。
- 误区:写数据库后更新缓存一定更好。 并发覆盖和聚合缓存维护会让更新缓存更容易出错。
- 误区:删除缓存失败靠 TTL 就够了。 TTL 是兜底,核心数据仍要有重试或补偿。
- 追问:为什么不先删缓存再写数据库? 并发读可能在 DB 更新前回填旧值,导致旧缓存存活到 TTL。
- 追问:延迟双删解决什么? 降低读写并发下旧值回填缓存的概率。
- 追问:热点 key 过期怎么处理? 互斥回填、逻辑过期、预热和 TTL 抖动组合使用。
九、加强记忆
记住“读缓存,不中查库回填;写数据库,成功后删缓存”。这是 Cache Aside 的骨架。
真正能拉开差距的是补上四个坑:并发回填、删除失败、短暂脏读、TTL 集中失效。能讲到这四个点,答案就不像模板。