跳转至

缓存与数据库双写一致性如何保证?

一、问题

更新数据库后,缓存还是旧值,下一次读到脏数据。

二、常见策略

1. Cache Aside(旁路缓存)

最常用:

  • :先读缓存,miss 再读 DB,回填缓存。
  • :先更新 DB,再删除缓存。
// 写
db.update(newValue);
redis.del(key);

// 读
Object v = redis.get(key);
if (v == null) {
    v = db.get(key);
    redis.set(key, v);
}

为什么是删缓存而不是更新缓存?

  1. 更新缓存可能有并发写问题,删缓存更简单。
  2. 缓存是计算出来的结果(多表 join),更新缓存成本高。
  3. 懒加载,读时再回填。

2. 为什么先更 DB 再删缓存

如果先删缓存再更 DB:

  • 线程 A 删缓存。
  • 线程 B 读 DB(旧值),回填缓存。
  • 线程 A 更新 DB。
  • 结果:缓存里是旧值。

先更 DB 再删缓存,窗口小得多。

3. 删除失败怎么办

删缓存失败,下次读还是旧值。解决:

  • 重试机制:删除失败发 MQ 重试。
  • 订阅 Binlog:Canal 监听 DB 变更,异步删缓存。这是最可靠的方案。

三、延迟双删

redis.del(key);
db.update(newValue);
Thread.sleep(500);   // 等读请求把旧值写回
redis.del(key);

不推荐,sleep 时间不好定。

四、强一致需求

  • 分布式锁:写请求加锁,串行化。
  • 读写穿透:读也走缓存,缓存 miss 时加锁回源。
  • 严格一致场景:直接读 DB,不要缓存。

五、TTL 兜底

不管用哪种策略,都给缓存加 TTL,作为最终一致的兜底。即使中间出问题,最多 TTL 时间后恢复。

六、流程图

sequenceDiagram
    participant W as 写请求
    participant DB
    participant Redis
    participant Canal
    participant MQ

    W->>DB: 更新数据
    W->>Redis: 删除缓存
    Redis-->>W: OK / FAIL
    alt 删除失败
        W->>MQ: 发重试消息
        MQ-->>Redis: 消费时再次删除
    end
    DB->>Canal: Binlog 变更
    Canal->>MQ: 推变更事件
    MQ-->>Redis: 消费删除缓存

面试标准答案

  1. 写:先更 DB,再删缓存。
  2. 删失败:MQ 重试。
  3. 兜底:订阅 Binlog + TTL。
  4. 高一致场景:加分布式锁或直接读 DB。