先说结论:绝大多数业务,选「先更新数据库,再删缓存」(Cache Aside),删除失败用重试兜底,再加一个 TTL 作为最后防线。「先删缓存再更新数据库」在并发下会把旧值重新写回缓存,是脏数据的经典来源;「先更新数据库再更新缓存」和「先更新缓存再更新数据库」则各有更麻烦的乱序问题。强一致场景才考虑延迟双删或订阅 binlog。

下面把这个判断拆开讲透。

一、四种基本顺序,为什么只有一种能打

更新一条数据,缓存和数据库都要动。排列组合就四种,先摆在一起看:

方案 顺序 致命问题
先更新缓存,再更新数据库 写缓存成功、写库失败 缓存里是新值,库里是旧值,且失败后没人回滚缓存
先更新数据库,再更新缓存 库成功、缓存失败 缓存旧值存活到 TTL 结束;并发写还会乱序
先删缓存,再更新数据库 删了缓存再去写库 并发读会把旧值重新加载进缓存(下文详解)
先更新数据库,再删缓存 写库成功后删缓存 不一致窗口极小且触发条件苛刻(下文详解)

前两种直接淘汰:更新缓存失败没有任何自愈手段,除非加事务回滚或消息补偿,复杂度立刻上去了。真正的争议在第三种和第四种之间。

二、为什么「先删缓存」是脏数据的经典来源

先删缓存的时序是这样的:

T1 写请求:删除缓存 key
T2 读请求:缓存 miss,从数据库读到旧值(还没写入)
T3 写请求:更新数据库为新值
T4 读请求:把旧值写回缓存

结果:数据库已经是新值,缓存里躺着旧值,而且会一直活到 TTL 过期。这不是理论风险——只要「读请求恰好插入在删除和更新之间」就触发,高并发读 + 慢 SQL(比如那条更新语句带了 300ms 的行锁等待)的场景下,命中率并不低。

有人会反驳:把「删缓存」挪到更新数据库之后不就完了?对,这正是第四种方案。但注意,它并没有 100% 消除问题,只是把问题发生的前提变得极其苛刻:

T1 读请求:缓存恰好过期,miss
T2 读请求:从数据库读到旧值
T3 写请求:更新数据库为新值
T4 写请求:删除缓存(此时缓存本来就是空的,删了个寂寞)
T5 读请求:把 T2 读到的旧值写入缓存

要同时满足三个条件:读的时候缓存恰好过期、读库发生在写事务提交之前、写回缓存发生在删除之后。三个时间窗口叠加,概率比「先删缓存」低了几个数量级。工程上没有绝对的一致,只有概率上的一致——这个窗口小到可以接受。

三、删除失败了怎么办:重试 + TTL 双保险

第四种方案唯一的软肋是「更新库成功、删缓存失败」。解法是两层兜底:

第一层:异步重试。 删除失败不阻塞主流程,丢进队列重试。以 Laravel 为例:

use Illuminate\Support\Facades\Cache;

public function updateArticle(int $id, array $data): void
{
    // 1. 先更新数据库(走 Eloquent,事务内)
    DB::transaction(function () use ($id, $data) {
        Article::where('id', $id)->update($data);
    });

    // 2. 再删缓存,失败则交给队列重试
    try {
        Cache::forget("article:{$id}");
    } catch (\Throwable $e) {
        DeleteCacheJob::dispatch("article:{$id}")
            ->delay(now()->addSeconds(3))
            ->tries(3);
    }
}

// DeleteCacheJob::handle() 里就是一句 Cache::forget,
// 失败自动指数退避重试,三次后进死信表人工处理

第二层:TTL 兜底。 所有缓存 key 必须设置过期时间。即使重试全部失败,脏数据最多存活到 TTL 结束。本站自己就是这么做的:文章列表缓存 TTL 是 2 小时(7200 秒),意味着哪怕缓存清理完全失效,新文章最迟 2 小时后也会出现在列表里。TTL 是一致性的最后防线,没有 TTL 的缓存等于把一致性押在运气上。

四、什么时候需要更进一步

两种情况值得上更重的方案:

主从延迟场景。 如果读走从库,写主库后立刻删缓存,读请求可能从还没同步完的从库把旧值又加载回来。这时用延迟双删:

更新数据库 → 删缓存 → 睡 500ms~1s(覆盖主从延迟)→ 再删一次

延迟双删是补偿手段,不是默认方案——它引入了 sleep,拖慢响应,而且延迟值没法精确预估。

订阅 binlog 的最终一致。 用 canal 之类的工具订阅 MySQL binlog,数据变更事件驱动缓存删除。好处是把删缓存从业务代码里彻底解耦出来,天然保证「库成功才删」,还能顺便做异构同步。代价是多维护一套中间件,个人项目和小团队通常犯不上。

五、一个容易忽略的点:读走不走缓存,别用「更新缓存」

很多教程会写「更新数据库后顺手把新值写回缓存」,看似省了一次 miss。但两个并发写请求会产生乱序:A 先提交库、B 后提交库,但 B 的「写缓存」先执行、A 的后执行——缓存里最终是 A 的旧值。删除就不一样,删多少次结果都一样,幂等性天然安全。这是「删缓存」优于「更新缓存」的根本原因,不是习惯,是数学。

FAQ

Q:那「先删缓存再更新数据库」加延迟双删行不行? 行,但不如直接「先更新库再删缓存」——后者天然只有一个删除点,前者要删两次还多扛一个并发窗口。

Q:缓存和数据库能不能强一致? 做不到,除非放弃缓存或用分布式锁串行化所有读写,那缓存就失去了意义。缓存一致性的本质是控制不一致窗口的大小。

Q:先更新库再删缓存,需要加事务吗? 删除操作不要塞进数据库事务里。事务拉长会扩大锁持有时间,而删缓存失败本来就有队列重试兜底,没必要为它牺牲数据库吞吐。

艾林博客 - 技术分享、开发经验与AI探索的个人技术博客
艾林博客 - 技术分享、开发经验与AI探索的个人技术博客

延伸阅读:

2026 <span class="text-primary">程序员生存指南</span>:代码通胀时代,如何构建不可替代的“工程直觉”? 技术随笔
2026 程序员生存指南:代码通胀时代,如何构建不可替代的“工程直觉”?

深入探讨 2026 年 AI 编程普及背景下程序员的核心竞争力。分析 AI 生成代码带来的隐形技术债,强调架构设计与底层系统运维在“代码通胀”时代的重要性。本文为开发者提供了从“编码者”向“系统编排者”转型的实战路线图,剖析如何在高度自动化的开发流程中建立不可替代的个人护城河。

AI 后端

Valencio

/

2026-04-07

现代接口安全实战:<span class="text-primary">从加密到防滥用的全栈策略</span> 技术随笔
现代接口安全实战:从加密到防滥用的全栈策略

很多人以为接口加了个 API-Key 或 JWT 就算“安全”。其实现代 API 安全从来不靠某一种“工具”,而是靠传输加密、认证设计、权限隔离、限速防刷、异常监控、日志审计等多个防线共同构成闭环。这一篇文章将为你系统梳理接口安全的全栈策略,避免你在业务关键点裸奔不自知。

资源 Web 安全 优化 Http 后端

Valencio

/

2025-07-04

为什么平台都不管你 key 泄露? 技术随笔
为什么平台都不管你 key 泄露?

很多开发者疑惑:如果我的 API-Key 被盗了,为什么平台方(比如腾讯云、OpenAI)都不报警、不封禁?他们难道不负责吗?本篇文章将深入解析开放平台认证背后的“边界责任模型”,帮助你厘清平台方与调用方之间的安全分工与责任归属,避免你为他人的低级错误背锅。

优化 安全 Web 后端

Valencio

/

2025-07-04