旁路缓存模式(Cache-Aside)深度解析
一、核心思想与命名由来
1. 为什么叫"旁路"缓存?
- 字面含义:缓存系统(如Redis)像一条"旁路"支线,不直接参与主业务流程
- 类比理解:类似高速公路的应急车道,只在需要时使用
- 关键特征:应用程序主动管理缓存,而非依赖缓存系统自动处理
2. 核心流程记忆口诀
读流程:缓存有→直接取,缓存无→查库→回填
写流程:先改库→再删缓存
(可用谐音记忆:"读有取,无查填;写先库,后删缓")
二、读策略(Read Policy)
1. 标准流程
sequenceDiagram
participant App as 应用程序
participant Cache as 缓存
participant DB as 数据库
App->>Cache: 1. 查询缓存
alt 缓存命中
Cache-->>App: 返回数据
else 缓存未命中
App->>DB: 2. 查询数据库
DB-->>App: 返回数据
App->>Cache: 3. 回填缓存
end
2. 关键设计要点
| 设计选择 | 原因 | 风险控制 |
|---|
| 缓存未命中时回填 | 避免每次穿透到DB | 需加分布式锁防止并发回填 |
| 不强制读穿透 | 保持业务逻辑控制权 | 需处理回填失败场景 |
| 设置TTL | 兜底最终一致性 | TTL时间需根据业务特点调整 |
三、写策略(Write Policy)
1. 标准流程
sequenceDiagram
participant App as 应用程序
participant Cache as 缓存
participant DB as 数据库
App->>DB: 1. 更新数据库
App->>Cache: 2. 删除缓存
2. 删除而非更新的原因
| 操作 | 优点 | 缺点 | 适用场景 |
|---|
| 删除缓存 | 避免并发写导致脏数据 | 下次读取需回填 | 常规场景 |
| 更新缓存 | 减少一次回填查询 | 可能覆盖其他线程的中间状态 | 写多读极少场景 |
四、与其他模式的对比
1. 写穿模式(Write-Through)
graph LR
App[应用] --> Cache[缓存]
Cache --> DB[数据库]
- 特点:所有写操作先更新缓存,由缓存系统同步写DB
- 示例:Guava Cache的
CacheLoader.write实现
- 适用场景:需要强一致性的配置类数据
2. 读穿模式(Read-Through)
graph LR
App[应用] --> Cache[缓存]
Cache --> DB[数据库]
- 特点:应用直接读缓存,缓存系统负责未命中时的DB查询和回填
- 示例:Spring Cache注解的实现
- 适用场景:希望完全解耦业务代码与缓存逻辑
3. 写回模式(Write-Back)
graph LR
App[应用] --> Cache[缓存]
Cache --异步批量--> DB[数据库]
- 特点:写操作只更新缓存,异步批量刷盘
- 风险:数据丢失风险(如宕机)
- 适用场景:写吞吐量极高的场景(如点击流日志)
五、业务场景选型指南
1. 旁路缓存适用场景
| 场景特征 | 示例业务 | 原因 |
|---|
| 读多写少 | 商品详情页 | 减少DB压力 |
| 允许短暂不一致 | 用户昵称展示 | 最终一致可接受 |
| 缓存Key分散 | 个性化推荐 | 避免大Key问题 |
2. 不适用场景
| 场景特征 | 问题 | 替代方案 |
|---|
| 写密集型(如秒杀库存) | 频繁删缓存导致穿透 | 写穿透+本地缓存 |
| 强一致性要求(如账户余额) | 无法接受不一致窗口 | 分布式事务 |
| 数据关联复杂(如社交关系) | 维护成本过高 | 图数据库 |
六、经典问题解决方案
1. 缓存击穿(单个热点Key失效)
解决方案:
public Object getData(String key) {
Object value = redis.get(key);
if (value == null) {
if (redis.setnx(key + ":lock", "1")) {
try {
value = db.query(key);
redis.setex(key, 300, value);
} finally {
redis.del(key + ":lock");
}
} else {
Thread.sleep(100);
return getData(key);
}
}
return value;
}
2. 缓存雪崩(大量Key同时失效)
防御措施:
- 随机TTL:在基础过期时间上增加随机值(如300±60秒)
- 预热机制:在低峰期主动加载热点数据
- 熔断降级:当缓存失效比例超过阈值时直接走DB
七、设计本质总结
| 维度 | 旁路缓存 | 写穿/读穿 | 写回 |
|---|
| 控制权 | 应用层控制 | 缓存系统控制 | 混合控制 |
| 一致性 | 最终一致 | 强一致 | 弱一致 |
| 复杂度 | 中 | 低 | 高 |
| 吞吐量 | 高 | 中 | 极高 |
记忆要点:
- 旁路缓存像"手动挡"汽车,需要开发者精细控制
- 写穿/读穿像"自动挡"汽车,由缓存系统自动处理
- 写回像"赛车模式",追求极致性能但风险更高