引言
为什么缓存几乎是"必选项"?
在真实业务系统中,我们往往面临三个典型矛盾:
- 数据访问频繁
- 数据库性能有限
- 用户对响应时间极其敏感
如果所有读请求都直接打到数据库:
- 数据库成为瓶颈
- 延迟高、吞吐低
- 扩容成本高
缓存的本质目标只有一句话:
用空间换时间,用不那么强的一致性,换取极致的性能
一、缓存是一种"设计思想",而不是 Redis 本身
很多人一提缓存就想到 Redis,但实际上:
- Redis 只是缓存的实现
- 缓存是一种系统设计思想
缓存的核心思想包括:
- 数据分层
- 冷热数据分离
- 降低对慢系统的依赖
- 允许短暂不一致,换取整体性能
这也是为什么缓存几乎一定会带来一致性问题。
二、最经典的缓存模式:Cache Aside(旁路缓存)
1. 什么是 Cache Aside?
Cache Aside,也叫 旁路缓存 ,是目前使用最广泛的缓存模式。
特点是:
- 应用代码显式控制
- 缓存与数据库是并列关系
- 缓存不主动写 DB,DB 也不主动更新缓存
2. 一个最常见的业务场景
以「查询商品信息」为例:
getProduct(productId)
三、Cache Aside 的读策略(Read Through 的实现方式)
1. 标准读流程
1. 先查缓存
2. 缓存命中 → 直接返回
3. 缓存未命中 → 查数据库
4. 写入缓存
5. 返回结果
2. 示例代码
Product getProduct(Long id) {
Product product = cache.get(id);
if (product != null) {
return product;
}
product = productMapper.selectById(id);
if (product != null) {
cache.set(id, product, 10, TimeUnit.MINUTES);
}
return product;
}
这是 99% 系统的真实实现方式。
四、Cache Aside 的写策略:为什么"先更新数据库,再删缓存"?
1. 最直觉的写法(错误示范)
1. 更新数据库
2. 更新缓存
2. 这个写法的问题在哪?
看一个并发场景:
时间线示意
T1:线程A 更新数据库(新值)
T2:线程B 读取缓存(旧值)
T3:线程A 更新缓存
T4:线程B 把旧值写回缓存(覆盖)
最终结果:
❌ 缓存中是旧数据,数据库是新数据
这就是典型的 缓存不一致问题。
五、正确的 Cache Aside 写策略
1. 标准做法
1. 更新数据库
2. 删除缓存
2. 为什么是"删缓存"而不是"更新缓存"?
因为:
- 删除是幂等的
- 删除失败可以重试
- 更新缓存会引入并发覆盖风险
3. 示例代码
void updateProduct(Product product) {
productMapper.update(product);
cache.delete(product.getId());
}
六、那"先删缓存,再更新数据库"行不行?
1. 看似合理的流程
1. 删除缓存
2. 更新数据库
2. 潜在问题:脏读
T1:线程A 删除缓存
T2:线程B 读缓存未命中 → 查数据库(旧数据)
T3:线程A 更新数据库
T4:线程B 写缓存(旧数据)
最终:
❌ 缓存再次被写入旧数据
3. 为什么"先更新 DB 再删缓存"概率更低?
因为在现实系统中:
- 数据库写入通常比缓存慢
- 删除缓存的时间窗口更短
- 出现极端并发覆盖的概率更低
所以工程实践中,先更新数据库,再删缓存 是主流选择。
七、Cache Aside 依然存在的问题
1. 写入频繁,缓存命中率下降
在写多读少的场景中:
- 每次写操作都会删缓存
- 缓存不断失效
- 读请求频繁回源数据库
表现为:
- 缓存"形同虚设"
- DB 压力上升
2. 典型场景
- 热点配置频繁修改
- 用户状态高频变更
- 实时性要求极高的数据
八、Cache Aside 的优化方案
1. 延迟双删(Double Delete)
1. 更新数据库
2. 删除缓存
3. 延迟一段时间
4. 再次删除缓存
java
updateDB();
cache.delete(key);
Thread.sleep(500);
cache.delete(key);
目的:
- 清理并发读写带来的脏缓存
2. 写队列 / Binlog 订阅
- 通过 MQ / Canal
- 数据库变更异步通知缓存系统
- 最终一致性
九、读穿 / 写穿 / 写回 策略简介
1. 读穿(Read Through)
- 缓存未命中
- 缓存层负责查 DB
- 应用层无感知
适合:
- 缓存组件能力强
- 应用层简单化
2. 写穿(Write Through)
- 写请求先到缓存
- 缓存同步写 DB
特点:
- 一致性好
- 写延迟高
3. 写回(Write Back)
- 写只写缓存
- 异步刷 DB
适合:
- 对一致性要求不高
- 写多读少
- 日志、统计场景
总结:缓存不是"加了就快"
Cache Aside 的使用法则
-
读多写少:非常适合
-
写多读少:慎用,需配合优化
-
一致性要求极高:考虑不缓存或短 TTL
牢记三句话
缓存不是银弹
一致性是成本
设计永远是权衡