导读:本期聚焦于王柏年创作的《Redis缓存先删后更新还是先更新后删?深入剖析缓存一致性策略的取舍》,敬请观看详情。缓存和数据库的双写一致性一直是分布式系统设计中的经典难题,其中先删除缓存再更新数据库的方案因实现简单而被广泛使用,但它真的可靠吗?本文从Cache Aside模式讲起,详细分析先删后更新策略在高并发场景下可能出现的脏读问题,对比先更新数据库再删缓存的优缺点,并重点讲解延迟双删方案的实现思路与代码示例。同时介绍如何通过设置过期时间兜底、借助Binlog异步删除、使用Canal组件等手段进一步保证一致性,帮助你在实际项目中根据业务场景选择合适的缓存更新策略。

在使用Redis作为MySQL前置缓存的架构中,当数据发生变更时,究竟是先删缓存再更新数据库,还是先更新数据库再删缓存,这个看似简单的问题背后隐藏着并发时序的复杂逻辑。选错了策略,轻则出现短暂脏数据,重则缓存长期与数据库不一致,直接影响业务正确性。本文将围绕先删后更新这一经典策略展开分析,讲清楚它的适用场景、致命缺陷以及补救方案。

Redis缓存先删后更新还是先更新后删?深入剖析缓存一致性策略的取舍

先删后更新策略的基本原理

先删后更新,指的是在数据发生修改时,先执行DEL删除Redis中对应的缓存key,然后再去更新数据库。这种策略的思想是:删除缓存后,后续的读请求发现缓存未命中,就会回源到数据库读取最新值并重新写入缓存,从而保证缓存数据最终与数据库一致。

典型的代码实现如下:

public void updateProduct(Product product) {
    String key = "product:" + product.getId();
    // 第一步:先删除缓存
    redisTemplate.delete(key);
    // 第二步:再更新数据库
    productMapper.updateById(product);
}

这种方案最大的优点是实现简单,不需要考虑缓存写入失败的问题。因为删除操作是幂等的,即使删除两次也不会有副作用。而且它避免了先更新数据库场景下,缓存更新失败导致的脏数据长期存在的问题。正因如此,很多中小型项目在初期都会采用这种方式。

并发场景下的脏读陷阱

然而先删后更新存在一个明显的并发缺陷。考虑这样一个时序:线程A执行更新操作,先删除了缓存,接着开始更新数据库。就在数据库还没更新完成的时候,线程B发起了一次读请求,发现缓存未命中,于是去数据库读数据。此时读到的是更新前的旧值,接着线程B把这个旧值写回了缓存。等线程A把数据库更新完成后,缓存里留着的仍然是旧值,而数据库已经是新值,两者出现了不一致。只要这个key没有设置过期时间,这个脏数据就会一直存在。

用一个时间线来梳理这个过程:

时刻1:线程A 删除缓存          缓存 miss
时刻2:线程B 查询缓存未命中
时刻3:线程B 查询数据库,得到旧值
时刻4:线程B 将旧值写入缓存
时刻5:线程A 更新数据库为新值
结果:缓存 = 旧值,数据库 = 新值,不一致!

这个问题在读写并发量大的场景下出现概率显著上升,特别是读请求回源数据库本身就有耗时,写回缓存的窗口期并不短。要缓解这个问题,一方面可以给所有缓存key都设置合理的过期时间作为兜底,即使出现脏数据也能在过期后自愈;另一方面可以采用延迟双删来主动修复。

延迟双删方案详解

延迟双删是在先删后更新基础上演进而来的方案:在更新前删除一次缓存,更新数据库后,再等待一小段时间进行第二次删除。第二次删除的目的,就是把并发读请求可能写入的旧值清掉,从而保证后续读请求能拿到最新数据。

实现代码大致如下:

public void updateWithDoubleDelete(Product product) {
    String key = "product:" + product.getId();
    // 第一次删除缓存
    redisTemplate.delete(key);
    // 更新数据库
    productMapper.updateById(product);
    // 延迟一段时间后二次删除(异步执行,避免阻塞主流程)
    CompletableFuture.runAsync(() -> {
        try {
            Thread.sleep(500); // 延迟时间需大于一次读请求的耗时
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
        redisTemplate.delete(key);
    }, executor);
}

延迟时间的选择是关键:必须大于一次读请求数据库查询加写回缓存的耗时,通常设置几百毫秒到一秒。如果延迟过短,第二次删除发生在读请求写回之前,脏数据仍然会残留;如果延迟过长,又会拉长不一致的时间窗口。此外,如果服务是集群部署,删除操作最好通过消息队列广播到所有实例,或者直接在代码中异步执行,避免因为请求路由到不同节点导致第二次删除丢失。

先更新数据库后删缓存对比分析

与先删后更新相对的,是经典的Cache Aside模式中的先更新数据库再删缓存。理论上它也存在不一致的可能:读请求未命中缓存后查到旧值,写回缓存前,写请求完成了数据库更新和缓存删除,随后旧值被写回。但这个时序要求读请求在缓存未命中后、写回缓存前发生一次完整的写操作,概率远低于先删后更新的场景。

两种方案的对比可以总结如下:

对比维度先删后更新先更新后删(Cache Aside)
脏数据概率较高,读回写窗口大较低,时序条件苛刻
实现复杂度简单简单,但需处理删除失败
缓存击穿风险删除后读集中回源相对较小
推荐场景低并发、可配延迟双删大多数常规读写场景

需要注意的是,先更新后删也存在删除失败的问题。如果数据库更新成功但缓存删除失败,同样会出现不一致。对此可以引入重试机制,例如把删除失败的key写入消息队列异步重试,或者更彻底的做法是通过Canal订阅MySQL的Binlog,在数据真正落库后由独立服务负责删除缓存,彻底解耦业务代码与缓存维护逻辑。结合过期时间兜底,即使极端情况下重试也失败,脏数据也会在过期后被清理。

实践中的选型建议

综合来看,没有绝对完美的缓存一致性方案,只有适合业务场景的方案。对于读多写少的常规业务,优先采用先更新数据库再删缓存的Cache Aside模式,并给所有key设置过期时间作为最终兜底。对于已经采用先删后更新的系统,务必升级为延迟双删,并根据线上读接口的耗时统计数据调整延迟阈值。对一致性要求极高的场景,如库存、账户余额等,则应该考虑引入分布式锁串行化读写,或者基于Binlog的异步删除方案,甚至接受短暂不一致但通过版本号校验来拒绝过期数据。

最后提醒一点,无论选择哪种策略,监控都不可缺席。建议在系统中埋点统计缓存与数据库不一致的探测指标,例如定期抽样比对热点key的值,一旦发现不一致率上升,能够及时定位是延迟参数不合理还是删除操作失败,让缓存一致性处于可观测、可调优的状态。

Redis缓存缓存一致性延迟双删修改时间:2026-09-01 23:12:34

免责声明:​ 已尽一切努力确保本网站所含信息的准确性。网站内容多为原创整理与精心编撰,观点力求客观中立。本站旨在免费分享,内容仅供个人学习、研究或参考使用。若引用了第三方作品,版权归原作者所有。如内容涉及您的权益,请联系我们处理。
内容垂直聚焦
专注技术核心技术栏目,确保每篇文章深度聚焦于实用技能。从代码技巧到架构设计,为用户提供无干扰的纯技术知识沉淀,精准满足专业提升需求。
知识结构清晰
覆盖从开发到部署的全链路。AI、前端、编程、数据库、服务器、建站、系统层层递进,构建清晰学习路径,帮助用户系统化掌握开发与运维所需的核心技术。
深度技术解析
拒绝泛泛而谈,深入技术细节与实践难点。无论是数据库优化还是服务器配置,均结合真实场景与代码示例进行剖析,致力于提供可直接应用于工作的解决方案。
专业领域覆盖
精准对应开发生命周期。从前端界面到后端编程,从数据库操作到服务器运维,形成完整闭环,一站式满足全栈工程师和运维人员的技术需求。
即学即用高效
内容强调实操性,步骤清晰、代码完整。用户可根据教程直接复现和应用于自身项目,显著缩短从学习到实践的距离,快速解决开发中的具体问题。
持续更新保障
专注既定技术方向进行长期、稳定的内容输出。确保各栏目技术文章持续更新迭代,紧跟主流技术发展趋势,为用户提供经久不衰的学习价值。