云计算百科
云计算领域专业知识百科平台

面试官:如果缓存和数据库双写不一致怎么办?

我们做后端开发,尤其是做电商、交易这类对数据敏感的系统,缓存和数据库的一致性几乎是绕不开的话题。

面试时也经常被问到:缓存和数据库双写不一致怎么办?

很多人第一反应是:先删缓存,再更新数据库。或者,先更新数据库,再删缓存。

但真正到了高并发场景,你会发现这两种做法其实都不够稳。那么今天我们就把这个问题拆开讲清楚。

一、为什么会不一致?

假设有一个商品详情页,商品信息存在数据库里,同时放到 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进阶实战】和【面试真题】,我们一起进步!

    赞(0)
    未经允许不得转载:网硕互联帮助中心 » 面试官:如果缓存和数据库双写不一致怎么办?
    分享到: 更多 (0)

    评论 抢沙发

    评论前必须登录!