缓存与数据库的交互策略
聊完了缓存淘汰和内存管理,咱们得把目光转向一个更实际的问题——缓存怎么跟数据库配合。
说白了,缓存不是孤岛。它必须和数据库协同工作,才能既快又准。我见过太多项目,缓存策略没选好,上线就出大问题。要么数据不一致,用户看到脏数据;要么数据库被打爆,直接雪崩。
这一块,核心就围绕几种经典模式。我把它们整理了一下,你看看。
旁路缓存模式
这是最常用的模式。我个人习惯叫它“懒加载”模式。为什么?因为数据只有在被访问时,才会加载到缓存。
流程上很简单:读请求先查缓存,有就直接返回;没有,就去数据库查,然后写入缓存。写请求呢?先写数据库,然后删除缓存。
核心原则:先写数据库,再删缓存。顺序不能乱。
为什么要删缓存,而不是更新缓存?你想想看,更新缓存需要额外计算,而且容易引发并发问题。删除缓存更简单,下次读的时候自然就加载新的了。
嗯,这里有个坑。我曾经处理过一个线上问题——并发写操作导致缓存删除失败。A线程写数据库,删缓存;B线程在A删除缓存前,又写了一次数据库。这时候缓存里存的还是A的数据。我当时的解决方案是加一个延迟双删:先删缓存,等几百毫秒再删一次。虽然不完美,但在大多数场景下够用。
// 伪代码示例:旁路缓存写操作
void writeData(String key, Data value) {
// 1. 先更新数据库
database.update(key, value);
// 2. 删除缓存
cache.delete(key);
// 3. 延迟双删(可选,用于应对并发)
Thread.sleep(500);
cache.delete(key);
}
什么时候用旁路缓存? 读多写少,数据更新不频繁的场景。比如用户信息、配置数据。
读写穿透模式
这种模式下,缓存不再是个“被动”的角色。它变成一个代理层,所有请求都先经过它。
读请求:缓存没有数据,缓存自己会去查数据库,然后返回。应用层根本不知道数据库的存在。
写请求:缓存先写自己,再同步写数据库。如果是分布式环境,还需要考虑同步的可靠性。
说白了,读写穿透是对应用更友好的模式。我曾经在一个金融项目中用过,因为业务逻辑复杂,我们不想让业务代码直接操作数据库。缓存层帮我们做了封装,代码干净了很多。
注意: 缓存如果挂了,所有请求会直接打到数据库。必须要有降级和熔断机制。我见过有人在这上面栽跟头,服务直接雪崩。
// 伪代码示例:读写穿透 - 缓存层实现
class CacheProxy {
Data get(String key) {
Data data = cache.get(key);
if (data == null) {
// 缓存自己查数据库
data = database.get(key);
// 写入缓存
cache.put(key, data);
}
return data;
}
void write(String key, Data value) {
// 先写缓存
cache.put(key, value);
// 再写数据库
database.update(key, value);
}
}
两种模式,一个直接,一个封装。选哪个?看你的团队对侵入性的容忍度。
策略选择与对比
到底用哪个?我自己的经验是,80%的场景用旁路缓存就够了。读写穿透适合那些对数据一致性要求极高,或者想屏蔽数据库细节的场景。
| 对比维度 | 旁路缓存 | 读写穿透 |
|---|---|---|
| 实现复杂度 | 低 | 高(需要缓存层封装) |
| 数据一致性 | 较好(删除策略) | 高(同步写入) |
| 应用侵入性 | 高(需要主动管理缓存) | 低(业务无感知) |
| 适用场景 | 读多写少 | 写操作频繁且一致性要求高 |
| 缓存崩溃影响 | 数据库压力增大 | 直接雪崩风险 |
你看这个表。旁路缓存适合你主动管理缓存的场景。读写穿透适合你不想让业务代码操心缓存的场景。没有的好,只有合不合适。
避坑指南
这部分我踩过不少坑。给你列几个典型的:
- 缓存穿透: 查询一个压根不存在的数据。缓存没有,数据库也没有。每次请求都直接打数据库。我建议你加布隆过滤器,或者把空结果也缓存起来(注意设置短过期时间)。
- 缓存击穿: 热点数据的缓存过期,大量请求同时打到数据库。加锁,让一个线程去加载数据,其他线程等待。
- 缓存雪崩: 大量缓存同时过期,或者缓存节点挂了。给过期时间加个随机值,避免同时过期。或者做高可用缓存集群。
我曾经遇到过缓存击穿的问题。双11活动,一个爆款商品的缓存过期了,瞬间几十万请求打到数据库。数据库直接卡死。后来我给这个热点数据加了互斥锁,只让一个请求去查数据库,其他请求等这个请求加载完缓存再返回。问题解决了。
核心流程总结
为了让这些策略更直观,我画了一张图。你看完应该就明白了。
看到没?左侧的旁路缓存,应用层直接面对缓存和数据库。右侧的读写穿透,应用层只和缓存代理打交道。这两种模式,是你在实际项目中必须掌握的。