缓存与数据库的交互策略

聊完了缓存淘汰和内存管理,咱们得把目光转向一个更实际的问题——缓存怎么跟数据库配合。

说白了,缓存不是孤岛。它必须和数据库协同工作,才能既快又准。我见过太多项目,缓存策略没选好,上线就出大问题。要么数据不一致,用户看到脏数据;要么数据库被打爆,直接雪崩。

这一块,核心就围绕几种经典模式。我把它们整理了一下,你看看。

旁路缓存模式

这是最常用的模式。我个人习惯叫它“懒加载”模式。为什么?因为数据只有在被访问时,才会加载到缓存。

流程上很简单:读请求先查缓存,有就直接返回;没有,就去数据库查,然后写入缓存。写请求呢?先写数据库,然后删除缓存。

核心原则:先写数据库,再删缓存。顺序不能乱。

为什么要删缓存,而不是更新缓存?你想想看,更新缓存需要额外计算,而且容易引发并发问题。删除缓存更简单,下次读的时候自然就加载新的了。

嗯,这里有个坑。我曾经处理过一个线上问题——并发写操作导致缓存删除失败。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活动,一个爆款商品的缓存过期了,瞬间几十万请求打到数据库。数据库直接卡死。后来我给这个热点数据加了互斥锁,只让一个请求去查数据库,其他请求等这个请求加载完缓存再返回。问题解决了。

核心流程总结

为了让这些策略更直观,我画了一张图。你看完应该就明白了。

缓存与数据库交互策略对比 缓存与数据库交互策略核心流程 旁路缓存模式 应用层 读:查缓存 读:查DB 写:写DB 写:删缓存 读写穿透模式 应用层 缓存代理层 缓存触发读取DB 缓存同步写入DB 选择策略:根据一致性需求和团队维护能力

看到没?左侧的旁路缓存,应用层直接面对缓存和数据库。右侧的读写穿透,应用层只和缓存代理打交道。这两种模式,是你在实际项目中必须掌握的。