缓存与数据库双写一致性如何保证?¶
一、问题¶
更新数据库后,缓存还是旧值,下一次读到脏数据。
二、常见策略¶
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);
}
为什么是删缓存而不是更新缓存?
- 更新缓存可能有并发写问题,删缓存更简单。
- 缓存是计算出来的结果(多表 join),更新缓存成本高。
- 懒加载,读时再回填。
2. 为什么先更 DB 再删缓存¶
如果先删缓存再更 DB:
- 线程 A 删缓存。
- 线程 B 读 DB(旧值),回填缓存。
- 线程 A 更新 DB。
- 结果:缓存里是旧值。
先更 DB 再删缓存,窗口小得多。
3. 删除失败怎么办¶
删缓存失败,下次读还是旧值。解决:
- 重试机制:删除失败发 MQ 重试。
- 订阅 Binlog:Canal 监听 DB 变更,异步删缓存。这是最可靠的方案。
三、延迟双删¶
不推荐,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: 消费删除缓存
面试标准答案
- 写:先更 DB,再删缓存。
- 删失败:MQ 重试。
- 兜底:订阅 Binlog + TTL。
- 高一致场景:加分布式锁或直接读 DB。