news 2026/9/16 23:06:20

缓存更新策略全解析:从一致性保障到高并发实战选型

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
缓存更新策略全解析:从一致性保障到高并发实战选型

我自己在高性能网站设计上踩过最大的坑,就是缓存更新。缓存命中率上去了,接口响应时间下来了,结果一更新数据,用户刷出来的全是旧值,或者一会儿新一会儿旧,后台还查不到原因。后来把缓存更新的各种套路捋清楚,才算把这块彻底稳住。

这篇内容围绕缓存更新这个核心话题,整理了我在实际项目中用过的策略、踩过的坑、以及可以照抄的选型方案和排障方法,适合正在做高并发接口、读写分离架构、以及被缓存一致性折磨的后端开发同学参考。如果你刚接触缓存,也能从里面拿到一套可以直接落地的思路。

1. 缓存更新到底在解决什么问题

1.1 高性能网站为什么绕不开缓存更新

任何一个读多写少的系统,都会把缓存放在数据库前面。缓存能把读请求的响应时间从几十毫秒压到几毫秒,这是高性能网站设计里最基础的一招。但缓存引入之后,最头疼的问题就是:数据库里的数据变了,缓存里的旧数据怎么办。

直接不更新,用户会一直读到旧值,严重的话会出现库存超卖、订单状态错乱;每次写操作都同步更新缓存,写路径又会被拖慢,高并发下还容易产生缓存和数据库之间的一致性问题。所以,缓存更新不是简单地在写入后刷一下缓存,而是一套需要根据业务场景精细设计的策略。

我自己做电商后端时遇到过最典型的情况:商品价格改了,管理后台改完数据库,用户端还是显示旧价格,客服那边接到大量投诉。原因就是当时只做了缓存过期,TTL还设得很长,数据库更新后缓存要等几分钟才失效。这个案例让我意识到,缓存更新策略必须在系统设计阶段就定好,而不是上线后出了问题再补。

1.2 缓存不一致的几种典型现场

先把问题定义清楚。缓存和数据库不一致,常见有下面几种现场:

  • 数据库更新成功,缓存更新失败,导致读取到旧数据
  • 并发请求下,一个线程写数据库,另一个线程写缓存,写入顺序错乱
  • 删缓存和更新数据库之间没有原子性,中间态被其他请求读到
  • 缓存过期时间设置过长,数据库已经变更但缓存迟迟不刷新

每一种现场背后,对应的解决思路都不一样。比如缓存更新失败的场景,核心是补偿重试机制;并发写顺序错乱的场景,核心是串行化或者版本控制。把这些现场记住,后面选策略的时候就能对号入座。

2. 主流缓存更新策略逐一拆解

2.1 Cache Aside:最常用但最容易踩坑

Cache Aside 是大部分团队的首选方案,思路很简单:读的时候先读缓存,读不到就读数据库,然后把数据写回缓存;写的时候先更新数据库,然后删除缓存。

这个方案的精髓在于“更新缓存”这个动作换成了“删除缓存”。删掉之后,下一次读请求会触发缓存重建,读到的一定是新数据。我最早不理解为什么更新数据库后不是直接改缓存,而是删掉重写,后来在高并发场景下才明白:直接改缓存存在竞争条件,两个并发写请求同时更新数据库和缓存,可能会出现数据库里是B值、缓存里却是A值的错乱。删除缓存则规避了这个风险,让缓存重建发生在读路径上,配合单飞的并发控制,一致性反而更好。

但Cache Aside也有个经典问题:先更新数据库还是先删缓存。如果先删缓存,再更新数据库,那么在删缓存和更新数据库之间的时间窗口里,有一个读请求发现缓存为空,就会去读数据库,把旧数据写回缓存。等数据库更新完成后,这个旧缓存值会一直存活到过期。所以更稳妥的顺序是先更新数据库,再删除缓存。这样至少能保证,在数据库更新完成之前,缓存里的值还是有效的,顶多是读到旧值,而不会出现新数据库值搭配旧缓存值的永久不一致。

Cache Aside适合大多数业务场景,尤其是读多写少、对一致性要求不是极端严格的场景。实现简单、容易理解,出问题了也容易排查。

2.2 Read Through / Write Through:把更新交给缓存组件

Read Through 和 Write Through 在架构上的特点是,应用程序不直接操作缓存和数据库,而是把读写请求都交给缓存组件,由缓存组件负责和数据库同步。

以 Write Through 为例,写请求先写缓存,缓存组件同步把数据写入数据库,两个操作作为一个整体对外暴露。这样做的好处是,应用层不需要关心先写哪个、再删哪个,一致性逻辑收拢到了缓存组件内部。

Read Through 则是读请求直接走缓存,缓存里没有数据时,由缓存组件自己加载数据库数据并回填。这和 Cache Aside 的“应用层手动回填”不同,应用层只知道缓存里有数据,不用管这个数据是怎么来的。

我实际使用下来,这个方案适合那些有成熟缓存中间件支持的团队,比如用 Redis + 自定义加载器,或者使用带持久化能力的缓存系统。但要注意,Write Through 会拉长写路径的延迟,因为每次写操作都要同步写数据库。如果写入量很大,需要评估是不是能接受这个性能损失。

2.3 Write Behind:用异步换吞吐

Write Behind 也叫 Write Back,它的核心思路是:写请求只更新缓存,立即返回成功;缓存组件异步地把数据批量写入数据库。

这个策略的最大优点是写吞吐极高,因为写操作不再同步等待数据库响应。但我必须泼一盆冷水:异步写意味着数据存在丢失窗口。如果缓存服务在异步刷盘之前宕机,这批数据就丢了。所以在金融、订单这类强一致场景,我不会推荐 Write Behind;它更适合计数器、浏览量、点赞数这类允许轻微丢失的数据。

使用 Write Behind 还需要注意写入顺序问题。比如同一个用户的多个操作,异步线程如果乱序落库,会导致最终数据和用户操作顺序不一致。解决办法是使用队列,按 key 维度把操作串行化,保证同一个 key 的写操作按照到达顺序执行。

在我维护过的点赞系统里,就是用 Write Behind 把每日千万级的点赞写入压到了很低延迟,异步批量落库之后数据库负载也非常平稳。但为了防丢数据,我额外做了定时全量对账,一旦发现缓存和数据库的差值超过阈值,就触发重放修复。

3. 策略选型背后的计算与决策

3.1 更新顺序决定一致性边界

无论是 Cache Aside 还是其他变种,最关键的一个决策点是“数据库更新”和“缓存变更”的先后顺序。我梳理过一张决策表,可以直接对照使用:

顺序一致性表现适用场景
先更新数据库,再删缓存数据库更新期间短暂旧读,更新完成后下次读即新值,一致性较好绝大多数业务
先删缓存,再更新数据库删缓存后到数据库更新完成前,读请求会把旧值回填,可能出现旧值长期存活对短暂不一致敏感的读多写少场景,需配合延迟双删
先更新数据库,再更新缓存并发更新数据库时易出现缓存与数据库值错位,不推荐几乎不适用
先更新缓存,再更新数据库缓存与数据库长时间不一致,且缓存可能被回源覆盖强烈不推荐

我自己实测下来,最稳的组合是“先更新数据库,再删除缓存”。之前在订单状态流转场景中,把原先的“先删缓存再更新DB”改成这个顺序之后,用户端看到状态回退的概率直接降到了零。

3.2 延迟双删的适用边界与参数选择

在一些极端并发场景下,单纯“先更新数据库,再删除缓存”还是不够。比如两个并发读请求同时发现缓存为空,同时回源数据库,其中一个回源拿到的是旧值并写回缓存,可能导致旧数据覆盖新数据。

针对这种“缓存重建期间的旧值回填”,业界常用的手段是延迟双删:先删一次缓存,更新数据库,然后隔一段时间再删一次缓存。第二次删除是为了清掉第一次删除到数据库更新完成之间可能被回填的旧值。

延迟时间的选择是个关键参数。我一般按“缓存重建耗时”的倍数来估算。假设一次数据库查询耗时 10ms,缓存回填耗时 2ms,那么第二次删除可以放在 50ms 到 200ms 之间。这个时间要大于“某个读请求从发现缓存为空到完成回填”的最长可能时间。设得太短,旧值可能还没写完就被更新事件覆盖了;设得太长,会无谓增加缓存空窗期。

延迟双删解决的是小概率的并发覆盖问题,不是银弹。它引入了额外的一次删除操作和定时任务,增加了系统复杂度。如果业务对一致性的要求没那么高,普通双删甚至单删已经够用。

3.3 不同业务场景的选型对照

根据业务特性选缓存更新策略,比单纯追求某种“最佳方案”重要得多。我按自己接触过的典型业务,整理了下面的对照表:

业务场景推荐策略核心理由
商品详情页Cache Aside,先更新DB再删缓存读多写少,删除后重建成本低
库存扣减Cache Aside + 延迟双删 + DB乐观锁强一致,防超卖,缓存重建要串行化
用户会话信息Cache Aside,TTL较短数据可用性高,短暂不一致可接受
热门榜单Write Behind + 定时全量重建写频繁,读量巨大,允许秒级延迟
计数器/点赞数Write Behind + 队列串行高吞吐,允许少量丢失,需要对账
AI模型嵌入向量缓存Cache Aside + 版本号模型升级后缓存必须整体失效,不能等TTL

每次做选型,我都会问三个问题:这个数据丢失或延迟一致的成本有多高?写入频率和读取频率的比例是多少?团队有没有能力维护额外的补偿和一致性机制?想清楚这三个问题,方案基本就浮出水面了。

4. 实操过程:一套可以落地的缓存更新方案

4.1 基础设施与工具准备

在动手之前,先确认你手上的基础设施。我平时会准备这几样东西:

  • Redis 集群:缓存组件,建议开启 AOF 持久化,避免缓存重启后大面积穿透
  • 消息队列:用于缓存更新失败的异步重试
  • 定时任务框架:用于延迟双删、对账任务
  • 数据库 binlog 订阅组件:如果团队成熟,可以考虑监听 binlog 自动失效缓存

我这里用一个实战项目的简化版举例:商品服务。数据库是 MySQL,缓存是 Redis,更新入口是一个管理后台接口,读入口是用户端商品详情接口。

4.2 核心流程实现

先看写路径。商品信息更新时,我采用“先更新数据库,再删除缓存,失败则进入重试队列”的流程:

def update_product(product_id, new_data): # 第一步:更新数据库 db.update(product_id, new_data) # 第二步:删除缓存 cache.delete(product_key(product_id)) # 第三步:如果删除失败,进入重试队列 if not cache.delete_success: retry_queue.push(product_id)

这里有个细节,删除缓存的操作不是直接把 Redis 的 del 命令发出去就完了。我会把删除动作封装成一个可重试的任务。如果 Redis 临时抖动导致删除失败,任务会进入消息队列,由消费者在几秒后再次尝试。

读路径实现如下:

def get_product(product_id): # 第一步:读缓存 data = cache.get(product_key(product_id)) if data: return data # 第二步:缓存未命中,加锁回源数据库 with cache.lock(product_key(product_id)): data = db.query(product_id) cache.set(product_key(product_id), data, ttl=600) return data

加锁这段是关键。不加锁时,热点商品缓存刚失效,几百个请求同时回源数据库,数据库可能直接被打挂。加了锁之后,只有一个请求去数据库取数据,其余请求等待锁释放后直接读缓存。这个操作对高并发下的稳定性提升非常明显。

TTL 的选择我一般按“业务能容忍的最长旧数据存活时间”来定。商品信息我习惯设 600 秒,因为后台修改商品后,希望最长 10 分钟内用户能看到新值,这个 TTL 可以兜底。

4.3 场景扩展:AI 推理服务的缓存版本更新

最近我在处理一个文本嵌入推理服务的缓存更新问题,类似于 Hugging Face 的 TEI 这类服务,需要把模型输出的向量结果缓存起来,避免重复计算。

这类缓存更新有个特殊点:模型版本升级后,同一个文本生成的向量可能完全不同,旧缓存必须整体失效。如果只靠 TTL 过期,TTL 没到之前,新旧版本的向量会混用,导致检索结果混乱。

我的做法是在缓存 key 里加入模型版本号。比如:

text_embedding:v3:你是一个测试文本

当模型从 v2 升级到 v3 时,业务代码切换到新的 key 前缀,旧数据自然被隔离。同时,我会通过消息队列主动清理 v2 前缀的批量数据,而不是傻等 TTL。这种做法比“更新缓存数据”更高效,也避免了缓存值和模型版本不匹配的问题。

这种“版本号进入缓存 key”的思路,对于算法模型迭代频繁的团队特别实用。不只是 AI 服务,凡是底层依赖版本会变的缓存数据,都可以照搬。

5. 常见问题与排查技巧实录

5.1 数据不一致的定位思路

如果线上已经出现了缓存和数据库不一致,先别急着清缓存,按下面的思路排查:

  • 确认不一致是持久的还是短暂的。短暂不一致可能是延迟双删的时间窗口内,持久不一致要看更新顺序是否正确。
  • 检查更新路径有没有异常。查看删除缓存是否成功,是否有失败后没有补偿的记录。
  • 确认并发情况。查看是否有多个实例同时写同一个 key,存在写顺序错乱的可能。
  • 观察 TTL。如果设置了过长的 TTL,数据库已更新但缓存迟迟不过期,也会表现出持久不一致。

我记得有一次,运营反馈商品价格一直不对,我查了很久发现是定时任务在更新数据库时,没有走我们封装的缓存失效逻辑,而是直接用 SQL 脚本改了库。缓存自然不会被删除。后来所有连数据库的账户和入口都强制走统一的数据访问层,问题才彻底解决。

5.2 更新失败的补偿机制

缓存删除失败这个事,靠人盯是盯不住的。我推荐一套机制:

  • 删除失败时写本地内存表,记录 key 和操作时间
  • 定时任务每隔 30 秒扫描失败记录,重新执行删除
  • 重试达到一定次数还失败,就触发告警,人工介入

这套机制成本很低,但能把缓存更新的可靠性提升一个量级。还有一种更彻底的做法:订阅数据库 binlog,每次数据变更自动触发缓存删除。这样即使业务代码漏了删除逻辑,binlog 通道也能兜底。代价是要额外维护一个消费组件,对团队人力有要求。

5.3 环境层面的缓存异常:以 Ubuntu 系统更新缓存为例

这里分享一个和“高性能网站”看似无关、但运维同学经常踩的坑:Ubuntu 系统更新缓存时偶尔会报错,现象是在执行软件源刷新时提示部分索引下载失败,或者哈希校验不一致。

处理思路其实和业务缓存差不多。先把损坏的列表缓存目录清理掉,再用干净的源列表重新生成索引。我在服务器上会定期做一次软件源清理,避免陈旧的缓存元数据干扰日常部署。这个经验告诉我们:缓存不只是 Redis 里的数据,系统层面、应用层面的缓存,同样需要一套更新和失效策略。

对一个线上网站来说,系统源缓存出错虽然不直接影响业务接口,但会阻塞安全补丁更新,间接影响系统的稳定性和性能表现。我习惯把所有服务器的基础镜像和软件源版本固定下来,配合定期的缓存刷新任务,减少这类问题。

写在最后的实操心得

缓存更新不是一个可以“上线后再说”的问题。我在多个项目里验证过,提前设计好更新策略,比事后修数据省太多时间。常用的方案其实就是先更新数据库再删缓存,配合延迟双删和重试队列,能覆盖绝大多数场景。遇到了 AI 模型版本升级这类特殊场景,把版本号放进缓存 key 就能快速切换,不用做复杂的数据迁移。

如果你现在正被缓存不一致困扰,我建议你先做一件事:把项目中所有写数据库的入口列出来,检查它们有没有统一的缓存失效逻辑。很多“诡异”的缓存问题,根源就是某个旁路接口绕过缓存更新直接改了数据库。

再补一个小技巧:删除缓存时不要用 select-then-delete 这种组合,直接 del 就好。某些缓存组件看起来支持“先比较再删除”的操作,在高并发下反而会引入新的竞态条件,简单直接往往最可靠。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/16 23:06:15

Java多线程:Thread.sleep()与Object.wait()核心区别与应用场景

1. Thread.sleep()与Object.wait()的本质区别在Java多线程编程中,Thread.sleep()和Object.wait()是两个经常被混淆的方法。虽然它们都能让线程暂停执行,但底层机制和适用场景完全不同。我在实际开发中见过不少因为误用这两个方法导致的线程阻塞和性能问题…

作者头像 李华
网站建设 2026/9/16 23:06:09

OpenMontage:面向AI视频生产的智能体编排框架

1. 项目概述:OpenMontage 是什么,它解决的不是“视频剪辑”而是“智能创作流编排”OpenMontage 这个名字乍看像某个开源视频编辑器——毕竟 montage 在影视行业里专指“蒙太奇”,是镜头组接、节奏调度的核心概念。但如果你真去 GitHub 搜索 O…

作者头像 李华
网站建设 2026/9/16 23:05:52

DolphinDB动态脚本优化:循环加速3倍,零代码改造

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/16 23:03:10

HiClaw Star:渐进式HTML增强工具链解析

1. HiClaw Star项目背景解析HiClaw Star近期在技术社区引发广泛关注,这个开源项目正在经历爆发式增长。从GitHub的star增长曲线来看,过去一个月内其star数量增长了300%,这种增长速度在同类工具中实属罕见。作为一个新兴的技术解决方案&#x…

作者头像 李华
网站建设 2026/9/16 23:02:14

16S rRNA测序分析新流程:DADA3与GTDB-r214的应用突破

1. 项目背景与核心价值微生物组研究正在经历一场数据革命。16S rRNA基因测序作为微生物群落分析的黄金标准,其分析流程从最初的简单物种注释发展到如今的多维度生态网络构建,技术迭代速度远超大多数研究者的学习曲线。2026年最新发布的扩增子分析流程&am…

作者头像 李华