缓存数据库(先更新数据库还是先删缓存?这4种缓存策略帮你搞定数据一致性)

缓存数据库(先更新数据库还是先删缓存?这4种缓存策略帮你搞定数据一致性)
先更新数据库还是先删缓存?这4种缓存策略帮你搞定数据一致性

一个灵魂拷问,80%的开发都踩过的坑

大家好。今天咱们来聊聊一个老生常谈但又极其容易踩坑的话题:当数据库和缓存一起使用时,到底先操作谁?

这个问题看似简单,但稍有不慎就会导致数据不一致,引发各种诡异的线上问题。下面我直接用最简洁的方式,给你讲透4种经典策略。

问题本质:并发场景下的脏数据

先看一个典型场景:

场景A:先删缓存,再更新数据库

线程1:删除缓存 → 线程2:读缓存(miss)→ 线程2:读数据库(旧值)→ 线程2:写缓存(旧值) → 线程1:更新数据库(新值)

结果:缓存里是旧值,数据库是新值,脏数据诞生。

场景B:先更新数据库,再删缓存

线程1:更新数据库 → 线程2:读缓存(命中,旧值)→ 线程1:删除缓存

结果:线程2读到了旧值,虽然短暂不一致,但缓存最终被删除,下次读取会重新加载新值。

对比结论:场景B的窗口期更短,风险更小。


策略一:先更新数据库,后删缓存(Cache Aside Pattern)

这是最经典的策略,也是大部分场景的首选。

执行流程:

1. 更新数据库2. 删除缓存(不是更新缓存)

为什么是删除而不是更新?

  • 更新缓存可能涉及复杂计算
  • 如果缓存被频繁更新但很少被读,纯属浪费资源
  • 删除缓存,等下次读取时再加载(懒加载)

代码示例:

// 伪代码public void updateProduct(Product product) {    // 1. 更新数据库    productDao.update(product);        // 2. 删除缓存    redisTemplate.delete("product:" + product.getId());}

注意:删除缓存这一步可能失败,需要用重试机制或监听binlog来兜底。

适用场景:读多写少、对短暂不一致不敏感的系统。


策略二:先删缓存,后更新数据库(慎用)

这种策略的问题我们开头已经分析过,并发下极易产生脏数据。如果你非要这么用,必须配合延迟双删。

延迟双删代码:

public void updateProduct(Product product) {    // 第一次删除缓存    redisTemplate.delete("product:" + product.getId());        // 更新数据库    productDao.update(product);        // 延迟一段时间(比如1秒),再次删除缓存    Thread.sleep(1000);    redisTemplate.delete("product:" + product.getId());}

延迟的目的是什么? 等待可能存在的其他读线程把旧数据写回缓存,然后第二次删除把它清理掉。

延迟时间怎么定? 大于一次读请求的耗时(通常100-500ms),保守点可以设1秒。

适用场景:读写并发极高的场景,且能接受短暂延迟。


策略三:延迟双删(双删缓存)

其实这是对策略二的优化,但足够经典,值得单独拎出来讲。

完整流程:

public void updateProduct(Product product) {    // 1. 第一次删除缓存    delCache(product.getId());        // 2. 更新数据库    updateDb(product);        // 3. 异步延迟删除(用消息队列或线程池)    asyncDelayDel(product.getId(), 1000);}private void asyncDelayDel(String id, long delayMs) {    CompletableFuture.runAsync(() -> {        Thread.sleep(delayMs);        delCache(id);    });}

优点:能有效解决并发脏数据问题,第二次删除保证了最终一致性。

缺点:代码复杂度增加,延迟时间内数据可能短暂不一致。


策略四:订阅数据库Binlog(最强兜底方案)

前面几种策略都有一个共同弱点:删除缓存可能失败。而binlog方案能完美解决这个问题。

架构流程:

1. 业务直接更新数据库2. 数据库产生binlog日志3. Canal / Debezium 订阅binlog4. 解析变更消息,异步删除对应缓存

代码示例(Canal配置):

# canal 配置canal.instance.master.address=127.0.0.1:3306canal.instance.dbUsername=canalcanal.instance.dbPassword=canalcanal.instance.filter.regex=test\..*

消费者伪代码:

public void handleBinlog(BinlogEntry entry) {    String tableName = entry.getTableName();    String rowId = entry.getRowId();        if ("product".equals(tableName)) {        // 删除缓存        redisTemplate.delete("product:" + rowId);    }}

优点:

  • 删除操作与业务解耦
  • 100%保证最终一致性
  • 即使业务代码忘记删缓存,兜底也能补救

缺点:

  • 引入额外组件,架构变复杂
  • 有延迟(毫秒级)

适用场景:对一致性要求极高、愿意接受架构复杂度的系统。


总结对比

策略

一致性

复杂度

适用场景

先更新DB后删缓存

高

低

通用首选

缓存数据库(先更新数据库还是先删缓存?这4种缓存策略帮你搞定数据一致性)

先删缓存后更新DB

低

低

不推荐

延迟双删

中

中

读写并发极高

Binlog订阅

最高

高

强一致性要求

记住一句话:缓存是数据库的复印件,更新时直接扔掉复印件,等下次要用了再复印新的。这就是Cache Aside Pattern的核心思想。

没有银弹,根据你的业务场景选择合适的策略。如果非要我给个建议:大部分业务用策略一 + 重试机制就够了,追求极致就用策略四兜底。

希望这篇文章能帮你彻底搞定缓存一致性问题。如果有不同见解,欢迎留言讨论!

💻 配套实训环境与动手练习

本文涉及的相关技术指令、开发环境与工具链已内置在边学边练在线实验室中,无需繁琐安装配置,随时在浏览器中实践体验:

文章版权声明:除非注明,否则均为边学边练网络文章,版权归原作者所有

最新文章

热门文章

本栏目文章