我们做后端开发,尤其是做电商、交易这类对数据敏感的系统,缓存和数据库的一致性几乎是绕不开的话题。
面试时也经常被问到:缓存和数据库双写不一致怎么办?
很多人第一反应是:先删缓存,再更新数据库。或者,先更新数据库,再删缓存。
但真正到了高并发场景,你会发现这两种做法其实都不够稳。那么今天我们就把这个问题拆开讲清楚。
一、为什么会不一致?
假设有一个商品详情页,商品信息存在数据库里,同时放到 Redis 缓存中。当用户修改商品价格时,系统需要做两件事:
更新数据库中的价格;
更新或删除缓存中的价格;
如果这两个操作不是原子操作的话,中间又有读请求并发请求进来,就可能出现:
数据库已经是新价格;
缓存中还是旧价格;
用户看到的商品价格不对;
这就是我们常说的缓存和数据库双写不一致。
二、先更新数据库,再更新缓存?
这是最容易想到的一种方案,流程大概是:
-
更新数据库中的价格。
-
更新缓存中的价格。
但它有个很明显的缺陷:并发写时容易覆盖新值。

最后数据库是 89 元,缓存却是 99 元。
所以,先更新数据库,再更新缓存,一般不推荐。
三、先删缓存,再更新数据库?
另一种常见做法是:先删除缓存,再更新数据库。
这个方案看起来合理:缓存删掉之后,下次读请求会重新从数据库加载最新数据。但它也有风险,假设:

所以,先删缓存再更新数据库,在高并发读场景下也可能不一致。
四、先更新数据库,再删除缓存
目前比较主流的做法是:先更新数据库,再删除缓存。
也就是经典的 Cache Aside Pattern。
它的流程是:
-
更新数据库;
-
删除缓存;
下次读请求发现缓存为空,重新从数据库加载并写入缓存。
为什么这个方案更好?
因为即使出现并发读,缓存里短暂写入了旧数据,后续删除缓存操作仍然会让缓存失效。下一次读请求会重新从数据库取到最新值。
当然,它也不是绝对完美;如果删除缓存失败,或者数据库主从同步存在延迟,仍然可能出现短暂不一致。
所以,它更适合追求 最终一致性 的业务场景。
/**
* 更新商品信息的标准流程:Cache Aside Pattern
*/
public void updateProduct(Long productId, Product newProduct) {
// 1. 先更新数据库
// 注意:这里通常是在事务中执行,确保数据落盘
productMapper.updateById(newProduct);
// 2. 再删除缓存
// 关键点:是删除(Delete),而不是更新(Set)!
String cacheKey = "product:" + productId;
redisTemplate.delete(cacheKey);
// 此时如果有读请求进来,会发现缓存为空,
// 进而去数据库读取最新数据并回填缓存。
}
五、延迟双删:简单但不优雅
为了解决“先删缓存再更新数据库”的并发问题,有些系统会采用 延迟双删。流程是:
-
先删除缓存;
-
更新数据库;
-
休眠一段时间;
-
再次删除缓存。
它的思路是:等可能的旧数据写回缓存之后,再删一次。
但这个方案有几个问题:
-
休眠时间不好确定;
-
线程会被阻塞,影响吞吐;
-
第二次删除仍可能失败;
-
代码侵入性比较强。
所以,延迟双删可以作为过渡方案,但不算最佳实践。
六、更稳的方案:异步删除缓存
如果业务对一致性要求比较高,更推荐的做法是:更新数据库后,异步删除缓存。常见实现有两种:
1. 消息队列异步删除
更新数据库成功后,发送一条消息到消息队列,比如 RabbitMQ、Kafka。
消费者收到消息后,执行删除缓存操作。
如果删除失败,可以重试。
这样即使删除缓存暂时失败,也能通过重试机制保证最终一致。
2. 监听 Binlog 删除缓存
另一种更优雅的方式是使用 Canal 监听数据库 Binlog。
数据库发生变更后,Canal 捕获变更事件,再触发缓存删除。
这种方式的优点是:
-
业务代码不需要关心缓存删除;
-
缓存删除和数据库变更解耦;
-
可以通过重试机制保证可靠性;
-
更容易扩展到其他缓存或下游系统。
七、面试时可以怎么回答?
这个问题不建议只背一句话,最好按层次回答。
我的回答:"一般情况下,我们会采用"先更新数据库,再删除缓存"的策略,也就是 Cache Aside Pattern。对于一致性要求更高的场景,可以结合消息队列异步删除缓存,或者通过 Canal 监听数据库 Binlog 来删除缓存,保证缓存删除的可靠性。
如果业务允许短暂不一致,优先保证系统可用性和最终一致性;如果对一致性要求非常严格,则需要结合业务场景评估是否仍然适合使用缓存,或者引入更强的补偿机制。"
八、总结
缓存和数据库双写不一致,本质上是在 性能、一致性、复杂度 之间做权衡。常见方案对比:
|
先更新库,再更新缓存 |
实现简单 |
并发写容易覆盖新值 |
|
先删缓存,再更新库 |
能避免部分脏数据 |
并发读可能写回旧数据 |
|
先更新库,再删缓存 |
主流方案,实现简单 |
删除失败会不一致 |
|
延迟双删 |
能缓解并发问题 |
延迟时间难确定 |
|
异步删除 / Binlog |
可靠性高,易扩展 |
架构复杂度更高 |
实际项目中,大多数场景选择 先更新数据库,再删除缓存 就够用了。
如果系统流量大、一致性要求高,再考虑引入消息队列或 Binlog 监听。
觉得有用的话,请到【Java笔记 小李版】👇
后续我会继续更新更多【Java进阶实战】和【面试真题】,我们一起进步!
网硕互联帮助中心





评论前必须登录!
注册