尧图网络 高端网站定制 · 原创设计
免费咨询热线
400-888-6620
免费获取方案
先更新数据库还是先删缓存?八种方案里错的有几种
先给结论先更新数据库再删除缓存。注意是删除不是更新。这一句话说完八种方案里能立刻判掉三种凡是更新缓存的都不推荐。剩下五种里能直接上线的只有一种其余要么留着并发缝隙要么是补丁要么代价高到不值得。一个典型的撕裂现场一个典型的场景是后台改商品价。运营在后台把价格从 99 改成 79保存成功接口返回 ok。切到前台一看还是 99。白屏刷两次变 79 了。这种问题之所以烦是因为它每一步都对。数据库里已经是 79缓存的过期时间也设了唯独在那几十毫秒的窗口里读出来的是旧值。更难受的是它不可稳定复现。一个人点几次没事压测并发一上来就出现。所以它经常以偶发显示不准的工单形式从客服那边反馈过来前端查了一圈说不是他们的问题最后才发现问题出在后端的写顺序上。如果把场景换成库存或者余额那就不只是显示不准了。这个问题其实在问三层第一层是顺序先碰数据库还是先碰缓存。第二层是动作那个写进缓存到底是SET一个新值还是DEL掉老的。第三层才是真正拉开差距的你要求的是强一致还是最终一致。多数人答不全是因为只答了第一层。问的人关心的从来是第三层。前置说明一下读路径后面所有推导都依赖它。Cache-Aside 模式下读是这样命中就返回没命中就查库查完了把值写回缓存。写路径才是本文要讨论的八种。八种方案逐个判先列个总账后面逐个拆。#方案判定一句话理由1先更新缓存再更新数据库错更新库失败缓存变成永久脏值2先更新数据库再更新缓存错并发写会导致先后覆盖错乱3先删除缓存再更新数据库有坑删完到改完之间读会把旧值回填4先更新数据库再删除缓存推荐窗口最小且删失败可重试5延迟双删补丁压低概率兜不住根因6删除失败走 MQ 重试必要让第 4 条真正可靠7Canal 订阅 binlog 删缓存解耦多服务写同一份缓存时用它8分布式锁串行化过重代价接近不用缓存为什么两个更新缓存都不行方案 1 的问题最直白更新缓存成功接着更新数据库失败回滚。缓存里躺着 79库里还是 99而且这个 79 会一直待到 TTL 到期。你连重试的机会都没有因为成功的那一步不可逆地被记住了。方案 2 稍微隐蔽一点。两个线程同时改同一个 key因为网络或者调度原因实际执行顺序可能是线程 A更新库存 10 线程 B更新库存 8 线程 A写缓存 10 ← 慢了一步 线程 B写缓存 8最终库里是 8缓存里却是 10。这不是可能出错是必然在某些并发下出错而且没办法通过加锁以外的手段完全规避。除了乱序还有一个更日常的理由更新缓存经常是白干活。一个商品的价格被改了 20 次中间没人来读那 20 次写里有 19 次是浪费的。删除的成本是固定的一次DEL更新还要把整份对象序列化塞进去热点大对象能到几 KB白赔网络带宽。先删缓存为什么也不推荐这条有争议很多博客说它也行。说一下它坏在哪。线程 A 要改数据先把缓存删了还没来得及改库线程 B 来读缓存 miss去查库此刻库里还是旧值 99然后把 99 写回缓存。之后 A 才慢悠悠地把库改成 79。结果库里是 79缓存里是 99且要等到下次 TTL 才恢复。对比方案 4先改库再删缓存会出现问题的时序是缓存刚好失效 → A 读到旧值 → A 还没写回缓存的极短时间内 B 完成改库 删缓存 → A 才把旧值写回去。这个要求读 miss 写回恰好被夹在毫秒级窗口里概率比方案 3 低得多。这也是它被推荐的根本原因不是因为它完美而是它窗口更小。至于这个窗口能不能接受其实是另一个层面的问题。是我们自己先接受了允许短暂不一致才决定用缓存的。推荐写法的骨架// JDK 17 Spring 6仅为表达语义 Transactional public void updatePrice(Long skuId, BigDecimal price) { skuMapper.updatePrice(skuId, price); // 关键提交之后再删。事务没提交就删别的线程会把旧值再读回去 TransactionSynchronizationManager.registerSynchronization( new TransactionSynchronization() { Override public void afterCommit() { cacheEvictProducer.send(skuId); // 走 MQ消费端执行 DEL } }); }afterCommit这一层容易被漏掉。事务没提交时删除缓存等于把窗口又拉回了方案 3 的量级。延迟双删真正的关键在幂等延迟双删长这样updateDB(record); redis.del(key); asyncSleep(500).then(() - redis.del(key)); // 补一刀它的作用是把方案 4 里那个极小概率的脏回填给清掉。注意它清的是可能已经被写脏的缓存所以它删的可能是一条别人刚写进去的数据。这件事听起来有破坏性实际上没有问题因为删除不引入任何值下一次读会重新从库里拿。但你一定面试被追问两个问题。第一个sleep 多久很多代码写死 500 毫秒。这个值没有普适答案它应该取你这条读链路的 P99 耗时再留一点余量。你们的服务如果读一次要 800 毫秒500 毫秒的延迟就删早了。别抄网上的数字用你自己监控里的 P99。第二个第二次删除失败了怎么办这是整件事的核心。第一次删失败还有第二次第二次失败就没了这条修改会一直脏到 TTL 到期。要让它可靠就得把删除丢到消息队列里做重试。而一旦上了重试幂等就从锦上添花变成了生死线。MQ 至少一次投递是常态消费端重投个几次很正常。如果你的动作是DEL key重复执行一万次效果跟执行一次完全一样如果你的动作是把 binlog 里的新值SET进缓存那么一条迟到的旧消息就可能把新值覆盖回去。所以说删除是幂等的这句话的分量在这里它让整条重试链路变得安全。换成更新缓存重试反而会制造新的错误。这也是 Canal 方案能成立的前提。Canal 伪装成 MySQL 从库订阅 binlog把变更投递出来去删缓存业务代码一个字不用改多服务写同一份缓存也不会漏。它之所以比业务代码里写 Redis更稳一半功劳在于删除幂等、可以随便重放。面试官在等的那句答到先更新数据库再删除缓存加上延迟双删很多人就停了。这段背得很熟但它停在方案层面。真正加分的是下一步这个窗口是消不掉的。只要你的数据在两个存储里各存一份且这两份只能先后写中间就一定有几十毫秒到几百毫秒的不一致。再多的删除、再巧妙的顺序都只是把概率往下压压不到零。所以工程上真正该问的是窗口里的脏数据能不能自愈。删除加 TTL 加重试幂等这三件事合起来保证的是自愈。而更新缓存保证不了自愈它会在缓存里留下一个可能长期不消失的错值。再往上抬一层。如果你真的要求强一致答案是不加缓存。缓存存在的意义就是用一个可接受的不一致窗口换取吞吐把窗口改成零缓存本身也就没价值了。想清楚这一点你就不会再为那 200 毫秒纠结半天。常见错误答案错一说延迟双删能彻底解决不能。它压低的是概率不是根因。窗口期内的脏数据最后由 TTL 兜底。错二sleep 直接抄 500 毫秒这个值要来自你自己读链路的 P99。不同服务差得远抄来的数字上线就是赌运气。错三不考虑删除失败删缓存是网络调用会失败会超时。失败不重试这次修改就要等到 TTL 才恢复。这是必须补的一环。错四什么场景都上 CanalCanal 有用但它是运维成本多了 canal-server 和消息中间件的依赖。一张改得很频繁、读 QPS 很高的表才值得。低频的配置类数据直接在业务代码里删就够了。错五把缓存一致性当成强一致问题见上一节。库存、余额这类数据本来就不该靠缓存来保证正确老老实实走数据库的行锁或者乐观锁。速查表你的场景够用的方案低频修改、能容忍短暂旧值先更新 DB再删缓存配一个合理 TTL中高频修改上面 MQ 异步删除重试多个服务写同一份缓存Canal 订阅 binlog 统一失效库存、余额、金额别指望缓存直接用数据库约束关于本文缓存这块还有几道没写Redis 分布式锁的五个坑、热 Key 和大 Key 怎么发现出来、缓存穿透击穿雪崩到底怎么分。这几道才是线上真正会咬人的第一道尤其容易栽。我会接着更新。另外完整的十二个模块Java / C / Go / JVM / MySQL / Redis / MQ / 分布式 / 事务 / 网络 / 系统设计 / 项目深挖我整理成了一份手册每题都标了考察点和加分答法。【草丛后端架构】持续分享后端架构与底层原理。
RELATED

相关推荐

Python 的哈希契约:为什么 list 不能当字典的键

Python 的哈希契约:为什么 list 不能当字典的键

原文链接:Python 的哈希契约:为什么 list 不能当字典的键 - 码日新 每日文章 Python 的哈希契约:为什么 list 不能当字典的键 上周帮同事看一个缓存问题:他用列表拼了个 key,运行时直接抛出 TypeError: unhashable t…

📅 2026/11/5 2:00:59
最小操作次数使数组元素相等 III【力扣LeetCode  每日一题】

最小操作次数使数组元素相等 III【力扣LeetCode 每日一题】

3736. 最小操作次数使数组元素相等 III 网页链接 力扣LeetCode 力扣LeetCode & 每日一题,完成每日打卡,即可获得力扣积分。获得相应数量的积分,能在【力扣商店】,换取相应奖品!助力每日有题做,丰盈积…

📅 2026/11/5 2:00:59
asyncio.run() cannot be called from a running event loop:在 async 函数里该怎么改

asyncio.run() cannot be called from a running event loop:在 async 函数里该怎么改

把一个本来能在命令行运行的异步示例搬进 Web 框架的 async 处理函数或支持顶层 await 的交互环境时,常见报错是: RuntimeError: asyncio.run() cannot be called from a running event loop这不是“异步函数不能调用异步函数”,而是当前线程…

📅 2026/11/5 2:00:59
MORE NEWS

更多资讯

📰

GitHub仓库如何助力大模型学习与工程实践

1. 项目概述:为什么GitHub仓库能成为大模型学习的关键资源?在人工智能领域,GitHub早已超越了单纯的代码托管平台角色,成为了技术前沿的晴雨表。特别是对于大模型这种快速迭代的技术方向,GitHub上的优质仓库往往包含了最…

📰

Sobol全局灵敏度分析MATLAB实现:方差分解与采样估计

简介:面向从事全局敏感性分析与不确定性量化的科研人员和工程技术人员,这套MATLAB代码演示了基于蒙特卡罗抽样与Sobol序列的Sobol灵敏度分析方法,可用于识别非线性数学模型中各输入参数对输出结果的影响程度,适合具备一定MATLAB基…

📰

Delta机器人正逆解:从几何约束到MATLAB实现与避坑

简介:这份资源面向学习并联机器人运动学的学生、研究人员与工程调试人员,聚焦Delta机器人运动学中的正解与逆解问题。正解由三个驱动关节角度推算末端执行器在笛卡尔空间中的位置,逆解则根据目标位姿反求关节角度,二者是理解Delta…

📰

Docker Compose 下载与更新全攻略:在线镜像加速与离线安装指南

Docker Compose 这个工具,但凡你碰过容器化部署,基本绕不开它。可真正让人卡住的往往不是写 YAML,而是第一步——把它弄到机器上,还得是对应的版本。尤其在国内网络环境下,直接从官方源拉取二进制文件,十次…

📰

幽默化设计:让家务变娱乐的5大策略

1. 项目背景与核心创意解析去年夏天的一次家庭聚会上,我意外发现一个有趣现象:当我把洗碗过程配上夸张的肢体动作和即兴喜剧台词时,原本抱怨洗碗的亲友们竟然主动加入了这个"表演"。这个偶然发现让我意识到,日常家务完全…

📰

技术人如何培养产品思维:从实现到洞察的转型

1. 从技术到产品的思维跃迁十年前当我第一次翻开《人人都是产品经理》这本书时,正处在职业生涯的十字路口。作为一名全栈工程师,我习惯用代码解决问题,却常常困惑为什么自己开发的功能用户并不买账。这本书彻底改变了我看待技术价值的视角——…

TODAY

今日更新

THIS WEEK

本周精选

THIS MONTH

本月热门

读完文章,想聊聊您的网站?

告诉我们您的行业与需求,资深顾问一对一梳理方案与报价,全程免费。

📞 💬