数据库存:数据库管理员避坑指南:做性能优化时别忽略缓存不同步
目录

数据库存:数据库管理员避坑指南:做性能优化时别忽略缓存不同步 | 九数云-E数通

eshutong 发表于2026年9月16日

数据库存:数据库管理员避坑指南:做性能优化时别忽略缓存不同步

数据库查询从 180 毫秒降到 35 毫秒,缓存命中率从 72% 提升到 94%,监控面板上的性能曲线几乎全绿,但用户仍然看到“待支付”的订单状态。类似问题在性能优化后并不少见:数据库已经写入新值,缓存却保留旧值;缓存刚被删除,又被并发请求用旧数据重新填充。性能指标变好,不等于数据访问链路变正确。

我在排查这类问题时,最常见的误区不是不会使用缓存,而是团队把缓存当成单纯的加速器,只关注命中率、响应时间和数据库负载,却没有把缓存失效、数据库事务、读写分离、消息重试和版本校验放在同一条链路里看。缓存一旦进入业务读路径,它就不再只是性能组件,而是数据正确性的一部分。

一、先讲核心结论:缓存不同步不是“小故障”,而是性能优化的隐性成本

1. 数据库正确,不代表用户一定读到正确数据

数据库通常是业务事实来源,但用户请求未必直接访问数据库。一次查询可能经过浏览器缓存、网关缓存、应用本地缓存、分布式缓存、读副本,最后才到达主库。只要其中一层仍然保存旧版本,用户就可能看到与数据库当前值不同的结果。

因此,排查“用户为什么看到旧数据”时,不能只执行一条 SQL 验证主库。必须同时确认用户命中的缓存层、缓存键、缓存值版本、数据库连接节点以及最近一次失效事件。只证明数据库是对的,无法证明整条读取路径是对的。

2. 缓存一致性问题往往由性能优化触发

很多系统在初期直接查数据库,虽然响应较慢,但数据路径短,出现问题时容易定位。后来团队为了降低数据库压力,引入本地缓存、分布式缓存和读写分离,查询速度确实提升了,但数据传播路径也变长了。

新增的每一层都会带来新的时间窗口:数据库提交后,缓存何时删除;主库提交后,副本何时追上;消息发出后,消费者何时执行;本地缓存收到通知后,何时完成失效。优化后的系统不是只有一个“数据库写入成功”节点,而是多个“新版本传播完成”节点。

3. TTL 只能限制旧数据寿命,不能证明数据已经同步

设置过期时间是必要的兜底手段,但它解决的是“旧值最多保留多久”,不是“数据库更新后缓存立即正确”。假设订单缓存 TTL 为 30 分钟,删除动作失败后,用户理论上仍可能在接下来 30 分钟内读到旧状态。

如果业务允许最多 5 分钟的旧数据,TTL 可以作为保护措施;如果数据涉及支付状态、库存扣减、权限变更或风控结果,仅靠 TTL 就不够。此时必须增加主动失效、版本校验、可靠消息或强制回源等机制。

数据库存:数据库管理员避坑指南:做性能优化时别忽略缓存不同步

4. DBA 的职责不只是把查询变快

数据库管理员在性能优化中经常负责索引、连接池、慢查询、主从复制和容量规划,但当应用引入缓存后,数据库与缓存之间的关系也应纳入性能评估。否则可能出现数据库负载下降、接口延迟下降,却把问题转移成订单状态错误、库存超卖或权限未及时收回。

我更倾向于把优化结果拆成三组指标:第一组是速度,包括 P95、P99 和数据库执行耗时;第二组是资源,包括 CPU、内存、连接数、磁盘读写和副本延迟;第三组是正确性,包括缓存失效失败率、版本差异、回源旧值比例和业务校验失败率。只有三组指标同时改善,才算完成一次合格的优化。

二、先把几个容易混淆的“缓存”分清楚

1. 数据库 Buffer Pool 不是业务缓存

数据库 Buffer Pool 负责把数据页、索引页等内容保留在数据库进程内存中,目的是减少磁盘 I/O。它保存的是数据库内部页的缓存,不是应用可以直接读取的业务对象。

Redis、Memcached、应用本地 Map 或某个缓存组件保存的,则通常是序列化后的用户信息、商品信息、订单状态或权限结果。调整数据库 Buffer Pool 只能改善数据库自身的访问效率,不能删除应用缓存里的旧订单,也不能修复缓存键拼接错误。

如果监控显示数据库磁盘读取下降,但用户仍然看到旧数据,问题大概率不在数据库 Buffer Pool,而在业务缓存失效、读路由或数据回填逻辑。

2. 本地缓存和分布式缓存的风险不同

本地缓存读取速度快,不需要网络跳转,但每个应用实例都有自己的副本。一个实例删除了缓存,不代表其他实例内存中的旧值也被清除。应用扩容后,实例数量增加,本地缓存的一致性成本会随之上升。

分布式缓存通常由多个应用实例共享,失效操作更集中,但它会引入网络超时、节点故障、集群切换、连接池耗尽和序列化失败等问题。不能因为分布式缓存只有一份,就认为它天然一致。

如果同时使用本地缓存和分布式缓存,还要明确失效顺序。例如,分布式缓存已经删除,本地缓存仍保留旧值,应用就不会回源到分布式缓存。此时只清理分布式缓存并不能解决用户问题。

3. 缓存不同步的准确含义

本文所说的缓存不同步,是指同一个业务对象在数据库和一个或多个缓存层中存在不同版本,并且这种差异已经影响了用户读取、业务判断或后续写入。

它不只包括“数据库是新值、缓存是旧值”,也包括缓存之间版本倒退、主库是新值但副本是旧值、缓存对象字段不完整,以及旧消息覆盖新消息等情况。

对象主要职责典型内容常见风险
数据库 Buffer Pool减少磁盘访问数据页、索引页内存不足、脏页刷盘压力
应用本地缓存减少网络与远程访问用户、配置、商品对象实例之间失效不同步
分布式缓存共享热点数据序列化业务对象、列表结果删除失败、网络超时、旧值回填
数据库只读副本分担读请求主库数据的复制版本复制延迟导致回源读取旧值

这张区分表的实际价值在于:不同对象的修复动作完全不同。数据库 Buffer Pool 问题通常看内存与 I/O;业务缓存问题要看键、失效与回填;副本问题要看复制延迟和读路由。把它们都叫“缓存问题”,会让排查方向从一开始就偏离。

二、先把几个容易混淆的“缓存”分清楚

三、缓存不同步最常见的五个时序陷阱

1. 数据库写成功,缓存删除失败

这是最容易理解、也最容易被低估的场景。应用先提交数据库事务,事务成功后执行缓存删除。如果删除操作遇到网络超时、连接池耗尽或缓存节点故障,数据库已经是新值,缓存却仍然保留旧值。

更危险的是,有些代码只记录一条错误日志,然后继续返回成功。业务方看到的是“更新成功”,运维看到的是“接口成功率 100%”,但缓存失效失败已经悄悄发生。

缓存删除失败至少应该进入可查询的失败队列或补偿机制。日志中需要包含业务主键、缓存键、数据库版本、操作类型、失败原因和重试次数,否则后续很难判断哪些缓存仍然可能是旧值。

2. 先删缓存,再更新数据库,造成旧值回填

假设系统采用以下流程:请求 A 先删除缓存,然后更新数据库;此时请求 B 读取缓存未命中,转而查询数据库。由于请求 A 的数据库事务还没有提交,请求 B 可能读到旧值,并把旧值写回缓存。

请求 A 随后提交新值,但没有再次删除缓存。之后所有请求都会命中 B 写入的旧值。这个问题并非每次都会发生,是否出现取决于并发时序、事务耗时、读请求耗时和回填策略,但高并发环境下不能把它当作极小概率。

这里的关键不是简单记住“先更新数据库还是先删缓存”,而是理解缓存回填发生在哪个时间点,以及旧值能否在新版本提交后再次进入缓存。

3. 延迟双删被当成万能方案

延迟双删通常是先删除缓存,更新数据库,等待一段时间后再次删除缓存,或者先更新数据库,再立即删除并延迟再次删除。它的价值在于增加一次清理机会,降低并发读取旧值并回填的概率。

但延迟双删不能覆盖所有异常:第二次删除仍可能失败;延迟时间可能不足;消费者、定时任务或其他服务可能在第二次删除之后再次写入旧对象;读副本延迟也可能让回源请求重新拿到旧值。

延迟时间不能凭“睡眠 500 毫秒”或“固定 1 秒”拍脑袋决定。更合理的估算是观察数据库事务提交耗时、读请求耗时、缓存回填耗时、网络延迟和副本同步延迟,再通过压测验证。在高峰流量下,平均耗时没有意义,应该重点看 P99 和异常长尾。

4. 数据库事务回滚,但缓存已经更新

另一类问题是缓存操作发生在数据库事务提交之前。应用先修改缓存,然后数据库事务因为校验失败、锁等待超时或连接断开而回滚,结果是缓存保存了一个数据库中不存在的新值。

缓存通常不会自动参与数据库事务。即使应用代码把两者写在同一个方法里,也不代表它们具备原子性。事务回滚后,缓存不会自动回滚,除非应用明确设计补偿动作。

涉及订单、库存、账户余额和权限的数据,通常不建议在数据库事务提交前把新值直接写入缓存。更稳妥的做法是以数据库提交结果作为失效或发布事件的触发条件。

5. 读写分离让“回源”重新写入旧值

这是线上很容易漏查的一层。主库更新成功后,应用删除缓存。随后一个查询请求发现缓存未命中,按照读路由规则访问只读副本,但此时副本还没有完成复制,于是返回旧数据。应用把旧数据重新写进缓存,缓存又恢复成错误版本。

这种问题看起来像缓存删除失败,实际上缓存删除可能完全成功。真正的问题是回源读取了尚未追平的副本。排查时,如果只查看缓存操作日志,会误以为“缓存已经删掉了,为什么又变旧”,而忽略了副本延迟。

对于强一致要求较高的读请求,可以在写入后的一段时间内强制读主库,或者携带数据库日志位点、版本号等信息,确保读取节点至少已经追上目标版本。

数据库存:数据库管理员避坑指南:做性能优化时别忽略缓存不同步

四、不要只背方案名称,要先判断业务到底需要什么一致性

1. 先判断旧数据带来的损失

不同业务对旧数据的容忍度差异很大。内容浏览、商品描述、推荐标签可能允许短时间旧值;支付状态、库存余额、账户额度和权限状态则可能因为一个旧值引发资金损失或越权。

我通常会先问三个问题:旧值最多可以存在多久;旧值被读取后会不会触发下一步写操作;出现一次错误时,能否自动修复,还是需要人工介入。答案比“系统是否使用缓存”更能决定架构。

业务类型可接受旧值时间建议策略重点监控
文章标题、展示标签数分钟至数小时旁路缓存加 TTL,删除失败后异步补偿失效失败率、缓存命中率
商品详情与营销信息通常可接受数十秒至数分钟更新数据库后失效,热点键增加主动刷新旧值时长、回源峰值、热点键
订单状态通常不应依赖长时间旧值版本校验、可靠事件、关键状态回源主库状态版本差异、重复消费、主从延迟
库存与账户余额接近零容忍数据库原子操作为准,缓存只做展示或加速超卖、余额差异、并发写失败
权限与风控结果撤销应尽快生效短 TTL、主动广播失效、关键操作强制校验权限撤销延迟、旧授权命中

2. 旁路缓存适合大多数读多写少场景

旁路缓存的典型流程是:读取时先查缓存,未命中再查数据库并回填;写入时先更新数据库,再删除缓存。它的优点是实现成本低、组件依赖少、故障时容易降级回源。

它的短板也非常明确:缓存删除可能失败;高并发下可能发生旧值回填;缓存击穿时会产生大量回源请求;多服务共享数据时,任何一个服务都可能绕过统一规则写入缓存。

对于文章内容、商品详情、配置数据等读多写少业务,我通常会选择旁路缓存,但会补上三项能力:删除失败重试、缓存值版本号、热点键的回源保护。方案简单不等于治理可以简单。

3. 消息驱动失效适合允许最终一致的系统

如果业务可以接受几十毫秒到数秒的传播延迟,可以在数据库事务提交后发布缓存失效事件,由消费者删除或刷新缓存。它把同步调用改成了可追踪的异步事件,便于重试、积压监控和失败补偿。

消息方案的关键不是“用了消息就可靠”,而是要回答四个问题:数据库提交成功但消息没发出怎么办;消息重复消费怎么办;多条更新事件乱序怎么办;消费者长期失败怎么办。

通常需要结合可靠投递、幂等处理、重试队列、死信队列和定期校验任务。没有这些配套机制,消息只是把缓存删除失败从应用日志转移到了消息系统。

4. 版本号校验适合并发更新和数据价值较高的场景

可以在数据库记录和缓存对象中加入单调递增的版本号。例如订单每次状态更新后版本号加一,缓存写入时携带该版本。任何低于当前版本的写入都不能覆盖高版本数据。

版本号不能解决所有问题,但它能阻止“旧数据覆盖新数据”。即使某条旧消息因为重试或乱序晚到,消费者也可以识别其版本过低并跳过处理。

版本号策略要求所有写入入口遵循同一规则。如果有一个定时任务仍然按照无版本方式直接覆盖缓存,版本保护就会被绕开。因此,版本控制不仅是字段设计,也是写入权限和代码规范问题。

数据库存:数据库管理员避坑指南:做性能优化时别忽略缓存不同步

5. 强一致不是越强越好

强一致机制往往需要同步事务协调、主库读取、写后校验或更严格的锁控制,代价可能是更高的写延迟、更复杂的故障处理以及更低的可用性。

如果一个商品介绍更新延迟 10 秒不会造成损失,就没有必要让每一次展示请求都强制访问主库。相反,如果是支付结果,就不能为了几毫秒的响应优势接受长时间旧值。

一致性级别应该由错误成本决定,而不是由缓存组件的宣传能力决定。先定义业务风险,再决定缓存策略,通常比先选一个流行方案再强行套用更稳妥。

五、一个订单状态案例:数据库已经更新,页面为什么仍显示“待支付”

1. 场景和读取链路

下面用一个常见的订单状态场景说明排查过程。订单服务将订单状态从“待支付”更新为“已支付”,数据库主库查询结果正确,但用户刷新页面后仍然看到“待支付”。查询接口的读取链路是:先查应用本地缓存,再查分布式缓存,缓存未命中后访问只读副本。

这个案例中的数字是为了展示排查方法的情景模拟,不代表某个具体企业的生产数据。真实系统应根据自身日志和链路追踪结果重新测量。

  • 数据库事务提交耗时:约 42 毫秒。
  • 缓存删除请求平均耗时:约 3 毫秒,P99 为 180 毫秒。
  • 只读副本复制延迟:通常低于 20 毫秒,高峰时达到 260 毫秒。
  • 订单缓存 TTL:30 分钟。
  • 应用本地缓存 TTL:5 分钟。

2. 第一次排查:发现数据库和分布式缓存不一致

第一步不是直接清空所有缓存,而是选取一个具体订单号,分别记录主库值、只读副本值、本地缓存值和分布式缓存值。这样可以判断问题发生在哪一层,而不是用“清缓存后恢复”掩盖根因。

检查对象观察结果初步判断
主库订单状态已支付,版本 18数据库写入已经成功
只读副本订单状态待支付,版本 17存在复制延迟或读路由问题
分布式缓存待支付,版本 17缓存仍保存旧对象
本地缓存待支付,版本 17本地层未收到有效失效通知
缓存删除日志分布式缓存删除超时 2 次至少存在主动失效失败

如果此时只清理分布式缓存,应用仍可能先命中本地缓存中的旧状态。如果只清理本地缓存,回源又可能访问尚未追平的只读副本,并把“待支付”重新写回分布式缓存。

3. 第二次排查:还原完整时间线

继续查看链路追踪后,可以还原出如下过程:支付服务提交主库成功;缓存删除第一次超时;查询请求命中本地缓存旧值;本地缓存过期后,查询请求访问只读副本;只读副本仍是旧版本;应用将旧结果回填到分布式缓存。

这说明该问题不是单一故障,而是三个风险叠加:缓存删除失败、本地缓存没有及时失效、读副本延迟导致旧值回源。任何一个环节单独存在,影响可能都有限,叠加后就会形成持续性错误。

数据库存:数据库管理员避坑指南:做性能优化时别忽略缓存不同步

4. 修复动作和验证方式

临时处置可以包括清理受影响订单的所有缓存层、对关键状态强制回源主库、暂停旧副本读取,以及通过订单状态校验任务重新刷新异常缓存。但临时清缓存只能止血,不能替代长期修复。

长期修复需要至少覆盖四个方面:数据库提交成功后才触发失效事件;失效事件具备重试和死信能力;本地缓存能够接收广播或缩短关键数据 TTL;支付后的一段时间内,订单状态查询优先访问主库或具备版本门槛的副本。

验证时不能只看接口返回 200。应在高并发条件下连续执行状态更新和读取,并记录数据库版本、缓存版本、用户返回版本和消息消费版本,确认没有版本倒退和旧值回填。

5. 这个案例最值得记住的判断

订单状态问题表面上是“缓存没有更新”,实际是“缓存失效策略没有考虑多级缓存和副本延迟”。如果架构中存在本地缓存、分布式缓存和只读副本,任何只针对 Redis 的修复都可能不完整。

缓存一致性排查的最小单位不是缓存节点,而是一个业务对象的一次完整版本传播。只要能沿着订单号或用户编号还原版本从主库到用户的流转,就比泛泛查看缓存命中率更容易找到根因。

六、DBA 做性能优化时,必须增加的检查项

1. 优化前先画出读写链路

在调整索引、增加缓存或启用读写分离之前,我建议先画出一个具体业务对象的读写图。图中至少要包含写入服务、数据库事务、消息系统、本地缓存、分布式缓存、主库、只读副本和用户读取接口。

  • 谁是唯一事实来源?
  • 谁可以写入缓存?
  • 缓存删除由哪个服务执行?
  • 数据库事务提交前是否会操作缓存?
  • 查询未命中时读取主库还是只读副本?
  • 本地缓存如何接收失效通知?
  • 消息丢失、重复和乱序如何处理?
  • 是否存在定时任务或脚本绕过标准写入流程?

如果这些问题没有明确答案,就不应该直接把缓存命中率当作优化目标。因为团队可能正在优化一条自己尚未完全理解的读取路径。

2. 优化中不要只看命中率和响应时间

缓存命中率是重要指标,但它只能说明请求是否命中了缓存,不能说明缓存内容是否是最新版本。一个错误缓存如果命中率很高,反而可能让错误更稳定、更难被发现。

建议把下面的指标一起纳入监控。指标名称可以根据业务系统实际情况调整,但必须能关联到具体业务对象和版本。

指标它回答的问题异常时的排查方向
缓存失效失败率数据库更新后,删除动作是否成功网络、连接池、权限、键名和节点状态
缓存版本差异数缓存版本是否低于数据库版本回填、消息乱序、删除失败和并发写入
旧值回源比例缓存未命中后,数据库返回的是否仍是旧版本只读副本延迟、事务提交时机和读路由
消息消费延迟失效事件多久才能到达缓存消费者积压、消费者容量和重试策略
本地缓存过期命中次数应用实例是否仍大量返回本地旧值广播失效、实例时钟和本地缓存配置
业务状态校验失败数用户看到的状态是否影响后续业务订单、库存、权限和风控链路

3. 优化后验证数据正确性

性能压测通常模拟大量相同查询,但缓存一致性验证必须同时模拟读写并发。只做“持续读”压测,很难暴露旧值回填;只做“单线程写后读”,也无法覆盖事务与副本延迟。

我建议至少设计以下测试场景:

  1. 数据库更新成功,缓存删除成功,立即并发读取。
  2. 数据库更新成功,缓存删除超时,观察是否进入重试队列。
  3. 数据库事务回滚,确认缓存没有保留未提交的新值。
  4. 缓存删除后,模拟高并发回源和回填。
  5. 主库提交后制造只读副本延迟,观察是否回填旧版本。
  6. 消息重复消费,确认不会产生错误覆盖。
  7. 新旧消息乱序到达,确认低版本事件不会覆盖高版本缓存。
  8. 本地缓存与分布式缓存同时存在时,验证两层是否都能失效。

数据库存:数据库管理员避坑指南:做性能优化时别忽略缓存不同步

4. 代码层面要让失败可见、可追踪、可补偿

缓存删除失败不能只打印一句“delete cache failed”。日志和指标需要携带足够的上下文,至少包括业务类型、业务主键、缓存键、数据库版本、操作时间、异常类型和重试次数。

下面是一段简化的伪代码,用来表达“数据库提交成功后再触发缓存失效”的思路。它不是可以直接复制到生产环境的完整实现,实际项目还需要补充幂等、超时、重试、死信和监控。

public void updateOrderStatus(Long orderId, String newStatus) {
long version;

transactionTemplate.execute(status -> {

Order order = orderRepository.lockById(orderId);

order.changeStatus(newStatus);

order.increaseVersion();

orderRepository.save(order);

return null;

});

version = orderRepository.findVersion(orderId);

try {

cacheInvalidationPublisher.publish(

new CacheInvalidationEvent("order", orderId, version)

);

} catch (Exception ex) {

retryQueue.enqueue(orderId, version, ex.getMessage());

metrics.increment("cache_invalidation_publish_failure");

}

}

这段逻辑的核心不是“把删除放在事务后面”这么简单,而是建立数据库版本与失效事件之间的关联。消费者收到低版本事件时可以跳过,收到高版本事件时才执行删除或刷新。

5. 发布前要准备人工止血开关

任何涉及缓存的性能优化都应该准备降级方案。例如关闭某类缓存读取、强制关键接口读主库、暂停旧副本路由、缩短指定业务的 TTL、批量清理指定前缀,以及临时关闭缓存回填。

止血开关不等于“出问题就清空全部缓存”。全量清缓存可能造成数据库瞬时流量激增,引发缓存击穿和数据库雪崩。更合理的是按业务类型、租户、时间范围或主键集合进行精准处理。

七、线上发现缓存不同步,按这个顺序排查

1. 先固定一个具体业务对象

不要一开始就观察全局平均指标。先选一个用户、订单、商品或配置项,记录它的数据库版本、缓存键、缓存值生成时间、请求实例和读节点。

如果没有具体主键,所有日志都只是统计现象,无法建立因果关系。一个明确的业务对象可以帮助团队沿着请求链路逐层确认:哪一层首先出现了旧版本。

2. 确认请求到底命中了哪一层

先查看请求是否命中浏览器、网关、本地缓存或分布式缓存。很多团队看到分布式缓存中已经是新值,就认为系统没有问题,却忽略了应用实例仍返回本地缓存旧对象。

  • 记录缓存层级和命中结果。
  • 记录缓存键的完整字符串,不要只记录业务主键。
  • 记录对象版本、写入时间和过期时间。
  • 确认是否存在同义键、大小写差异或租户前缀错误。
  • 确认序列化对象是否来自旧版本代码。

3. 核对数据库主库、只读副本和事务状态

确认主库中的值只是第一步,还要确认写事务是否真正提交、读取请求连接的是哪个节点、只读副本的复制位点是否追上目标版本。

如果数据库使用读写分离,建议在日志中记录数据库节点标识。没有节点信息时,“查数据库得到旧值”这句话几乎没有排查价值,因为你不知道查的是主库还是副本。

4. 查看缓存失效、消息和回填日志

把数据库提交时间、缓存删除时间、消息发送时间、消息消费时间和缓存回填时间放到同一条时间线上。重点寻找以下关系:数据库提交后是否有失效事件;失效事件是否失败或延迟;失效后是否存在旧值回填;旧值回填时读取的是主库还是副本。

如果消息事件没有业务版本号,建议尽快补充。只有事件时间而没有版本信息,很难判断一条晚到消息是应该执行还是应该丢弃。

5. 判断是偶发节点故障还是设计缺陷

单个缓存键异常,可能是数据脏写、键规则错误或特殊业务分支;一批键同时异常,可能是消息积压、批量任务或缓存集群故障;所有写请求都出现旧值,则更像是统一失效策略存在缺陷。

现象范围优先怀疑对象第一动作
单个业务对象键错误、脏数据、特殊分支核对键、版本和单次写入日志
同一批次对象批处理、消息积压、任务失败查看批次号、消费延迟和失败重试
固定时间段集中发生发布、流量高峰、备份或切换对齐发布记录、资源曲线和副本延迟
所有更新都可能发生统一失效策略或缓存协议缺陷审查写入顺序、事务边界和补偿链路
只在读副本读取时发生复制延迟和读路由强制主库验证并查看复制位点

数据库存:数据库管理员避坑指南:做性能优化时别忽略缓存不同步

八、不同情况下的行动建议与取舍

1. 读多写少、旧值影响较小的业务

例如文章内容、商品描述、运营标签和非关键配置,可以采用旁路缓存加 TTL。数据库更新成功后删除缓存,删除失败进入异步重试,定时任务负责抽样核对数据库与缓存版本。

这类业务不必为了极短的传播延迟引入复杂事务协调,但要把最大旧值时间写进业务约定。例如允许最多 60 秒旧值,就不能把 TTL 设置为 30 分钟后再宣称“最终会一致”。

取舍是:实现简单、读取速度快,但短时间内可能存在旧值。适合把资源投入到失效监控和故障补偿,而不是追求每次读取都访问主库。

2. 读多写多、并发更新明显的业务

商品价格、营销库存、活动资格等场景通常更新较频繁,单纯删除缓存容易受到并发回填影响。可以考虑版本号、写入序列号、延迟二次失效或消息驱动刷新。

如果对象更新频率很高,缓存整个对象可能比缓存稳定字段更危险。例如商品价格变化频繁,但商品描述变化很少,可以拆分缓存对象,分别设置失效规则,避免一个字段更新导致整个对象不断被覆盖。

取舍是:版本保护和事件处理会增加开发与运维成本,但能显著降低旧版本覆盖新版本的风险。不要只因为延迟双删代码少,就把它当成高并发写场景的完整答案。

3. 订单、支付、库存和余额场景

这类数据不能把缓存当作唯一判断依据。缓存可以用于展示和加速,但最终扣减、支付确认、余额校验等关键动作应以数据库原子操作或专门的事实系统为准。

订单详情页面可以展示缓存状态,但用户点击“确认收货”或“申请退款”时,服务端仍应重新校验当前状态。库存展示可以使用缓存,但真正扣减要依赖数据库条件更新、库存服务或可靠的原子组件。

取舍是:关键动作增加一次校验会带来额外延迟,但换来业务正确性。这里不应该用“接口 P99 增加了 10 毫秒”作为取消校验的理由,因为一次错误扣减的成本远高于一次数据库查询。

4. 权限、风控和配置变更场景

权限撤销和风控规则更新的特点是:旧值可能导致安全风险。对于角色权限、黑名单、额度规则等数据,建议采用主动广播失效、较短 TTL,并在高风险操作前强制查询权威数据源。

多实例部署时,本地缓存必须有明确的失效通知机制。可以使用发布订阅、配置推送或版本轮询,但不能假设实例会自然感知其他实例的更新。

取舍是:更频繁的失效会降低缓存命中率和增加回源压力,但安全相关数据不应为了追求命中率而延长旧值寿命。

5. 已经出现线上数据错误时

第一优先级是控制业务影响,而不是马上优化架构。根据数据类型选择强制回源、暂停缓存写入、临时读主库、精准清理受影响键或关闭相关功能。

第二优先级是保留现场。不要在没有记录缓存值、数据库值、版本和时间线之前全量清空缓存,否则可能失去定位根因所需的证据。

第三优先级是补偿数据。对于订单、库存和账户类数据,应建立异常对象列表,逐条核对数据库、缓存和业务流水,确认是否存在错误操作,而不是只让缓存重新变正确。

数据库存:数据库管理员避坑指南:做性能优化时别忽略缓存不同步

九、上线前可以直接使用的缓存一致性清单

1. 架构设计检查

  • 是否明确数据库或其他权威系统是唯一事实来源?
  • 是否画出了本地缓存、分布式缓存、数据库主库和只读副本的完整链路?
  • 是否明确每个缓存键由哪个服务创建、更新和删除?
  • 是否存在绕过标准流程的脚本、定时任务或后台操作?
  • 是否定义了业务可以接受的最大旧值时间?
  • 是否按照业务风险划分强一致、准实时一致和最终一致场景?

2. 写入流程检查

  • 缓存操作是否发生在数据库事务提交之前?
  • 数据库提交成功后,失效事件是否能够可靠产生?
  • 缓存删除失败是否有重试和补偿?
  • 重试是否幂等,是否可能重复刷新或覆盖新版本?
  • 是否使用版本号、时间戳或日志位点防止旧消息覆盖新消息?
  • 是否记录了业务主键、缓存键、版本和失败原因?

3. 读取流程检查

  • 缓存未命中时读取主库还是只读副本?
  • 只读副本延迟是否会造成旧值回填?
  • 回填缓存前是否校验数据版本?
  • 本地缓存和分布式缓存的失效顺序是否明确?
  • 是否存在多级缓存 TTL 不一致导致的长时间旧值?
  • 缓存击穿时是否有互斥、限流或请求合并机制?

4. 监控和演练检查

  • 是否监控缓存失效失败率和重试次数?
  • 是否监控消息积压、消费延迟和死信数量?
  • 是否监控数据库主从复制延迟?
  • 是否能查询单个业务对象的缓存版本和数据库版本?
  • 是否演练过缓存集群故障、消息重复、消息乱序和副本延迟?
  • 是否有按业务键精准清理和强制回源的降级开关?

5. 验收标准检查

性能验收至少应同时包含延迟、资源和正确性三部分。不能只提交一张“平均响应时间下降”的图,就认定缓存改造完成。

验收维度建议观察项不合格表现
性能P95、P99、缓存命中率、回源量平均耗时下降,但长尾和回源峰值明显上升
资源数据库 CPU、连接数、缓存内存、网络流量数据库负载下降,但缓存节点频繁淘汰或连接耗尽
正确性版本差异、失效失败、旧值回填、业务校验失败命中率很高,但关键业务仍出现旧状态
恢复性重试成功率、补偿时长、故障恢复时间只能人工清缓存,无法确认哪些对象受影响

十、最后的判断:不要把缓存当作数据库的“影子副本”

1. 缓存的价值是缩短读取路径,不是复制事实来源

缓存保存的是为了更快读取而生成的副本。只要它是副本,就会面临生成、更新、失效、过期和恢复问题。把缓存当成数据库的影子副本,容易产生“数据库更新了,缓存自然应该更新”的错觉。

事实上,数据库和缓存通常由不同的存储机制、网络连接、事务边界和失败模型维护。它们之间没有天然的原子提交。系统必须显式设计版本传播和异常补偿,才能让副本在可接受时间内追上事实来源。

2. 最值得监控的不是命中率,而是“旧值还会不会被继续使用”

命中率适合衡量缓存是否减少了回源,但它无法告诉我们命中的内容是否正确。对高风险业务,更值得关注的是版本差异、旧值暴露时间、失效失败率和业务校验失败次数。

如果暂时没有条件建立完整版本校验,至少可以从日志中记录缓存生成时间、数据库更新时间和请求读取时间,先估算旧值窗口。对一个无法量化的问题,团队很难做出合理的风险决策。

3. 性能优化的终点不是更快,而是更快且可证明正确

我对缓存优化的验收标准很简单:响应时间是否改善,数据库资源是否下降,错误数据是否可发现,失效失败是否可补偿,故障发生后是否能精准恢复。

如果只满足前两项,得到的可能是一套“跑得更快但错得更隐蔽”的系统。真正成熟的缓存架构,不是承诺永远不会不同步,而是能够明确允许多长时间不同步、哪些业务绝不能不同步,以及出现异常后如何发现、重试、校验和恢复。

下一步可以从一个最容易出问题的业务对象开始:选定一个订单、商品或权限记录,记录数据库版本、缓存版本、失效事件和最终读取结果。先把一次版本传播完整还原,再决定是否需要延迟双删、消息失效、版本校验或强制回源。这比盲目增加缓存容量、继续调高命中率,更接近数据库性能优化真正应该解决的问题。

常见问题解答(FAQ)

1. 数据库性能优化后,为什么用户反而更容易读到旧数据?

我给数据库加了索引、启用了 Redis 缓存,接口耗时从 180ms 降到了 35ms,缓存命中率也从 62% 提升到 94%。但订单状态更新后,用户仍然会看到“待支付”,这种情况到底是数据库没更新,还是缓存把旧数据重新返回了?

这类问题最容易被误判为“数据库更新失败”,但实际排查时,数据库往往已经提交成功,错误出在读取路径发生了变化。性能优化后,请求不再每次访问数据库,而是优先经过本地缓存、分布式缓存,再根据需要访问主库或只读副本;链路变长后,任何一个失效、回填或读路由环节都可能返回旧版本。

我建议先不要清空整个缓存,而是固定一个具体业务对象,记录四个时间点:数据库事务提交时间、缓存删除时间、缓存重新写入时间,以及用户读取时间。

下面是一组常见的演示时序: 时间事件数据库值缓存值 10:00:00.000用户读取订单待支付待支付 10:00:00.020支付服务提交更新已支付待支付 10:00:00.025删除缓存超时已支付待支付 10:00:00.030查询接口命中缓存已支付待支付 这里的关键判断是:缓存命中率越高,旧值一旦没有被正确失效,错误结果反而越稳定。

低命中率时,部分请求会回源数据库,问题可能很快被覆盖;高命中率时,用户会持续命中同一个错误缓存。因此,缓存命中率不能单独作为优化成功的证据。排查时应同时核对缓存 Key、缓存生成时间、数据版本号、数据库连接节点和数据库复制延迟。

如果数据库写入主库后,缓存失效却从只读副本回源,那么副本延迟还可能把旧值再次写入缓存。我的判断标准是:性能指标必须和数据正确性指标一起验收,至少要增加缓存失效失败率、回源后版本落后次数和业务旧值投诉数。

2. 先删除缓存再更新数据库,和先更新数据库再删除缓存,哪个更安全?

团队以前习惯先删缓存,再写数据库,觉得这样可以让下一次请求重新读取最新数据。后来压测时发现,并发读请求会把旧值重新写回缓存;如果改成先写库再删缓存,又担心删除失败,我应该如何选择?

在大多数旁路缓存场景中,我更倾向于“数据库事务成功提交后,再删除缓存”,而不是“先删除缓存,再更新数据库”。原因不是后者一定错误,而是先删缓存会主动制造一个空窗期:此时数据库仍然可能是旧值,恰好到来的读请求会把旧值重新回填。一个典型并发过程如下:A 请求先删除缓存,随后准备更新数据库;

B 请求发现缓存不存在,于是读取数据库中的旧值,并将旧值写入缓存;A 请求最后提交新值,但没有再次删除缓存。这样,数据库是新值,缓存却恢复成了旧值。先更新数据库再删除缓存也不是强一致方案。数据库提交成功后,删除缓存可能因为网络超时、缓存节点故障、Key 拼接错误或权限问题失败。

因此,真正可靠的做法不是迷信某个顺序,而是把“删除动作是否成功、失败后如何补偿”设计完整。

方案主要风险我的判断 先删缓存,再写库并发读可能回填旧值除非有额外保护,否则不建议作为默认方案 先写库,再删缓存删除失败导致旧值残留适合作为旁路缓存的基础方案,但必须配合重试 写库后发失效事件事件丢失、延迟或重复消费适合需要追踪和补偿的异步架构 写库后延迟再次删除延迟参数不合理或第二次删除失败只能降低风险,不能替代监控与补偿 如果业务对旧值非常敏感,例如库存、支付状态和权限,我会进一步加入版本号。

缓存写入时携带数据库版本,发现待写入版本小于当前缓存版本时直接拒绝覆盖。这样即使发生乱序回填,也不会让旧数据覆盖新数据。

3. 延迟双删真的能解决缓存不同步吗?延迟时间应该设置多久?

我看到很多方案都建议延迟双删:先删除一次缓存,更新数据库后再删除一次,或者写库后等待一段时间再删。问题是线上接口耗时、数据库提交时间和高峰流量每天都在变化,这个延迟值到底该凭经验设置,还是应该通过数据计算?

延迟双删的价值,是减少并发读请求把旧数据回填到缓存后的残留时间,但它不是一致性保证。它主要针对“读请求在数据库更新前后穿插执行”的场景,对缓存节点宕机、删除命令超时、消息丢失、读副本延迟和错误 Key 并没有自动修复能力。我不建议直接照搬 500 毫秒或 1 秒这样的经验值。

更合理的做法是先测量一次完整链路:数据库事务提交耗时、读请求从查库到回填缓存的耗时、网络最大延迟、线程池排队时间以及读副本延迟。

演示计算可以是:事务 P99 为 80ms,读请求 P99 为 120ms,网络与排队保护值为 100ms,那么初始延迟至少应覆盖 300ms 左右,再通过并发压测验证,而不是只看平均耗时。

指标演示观测值对延迟设置的意义 数据库事务 P9980ms反映新值何时真正提交 查询并回填缓存 P99120ms反映旧值可能存活多久 缓存操作与排队保护100ms覆盖网络抖动和线程池等待 建议初始延迟约 300ms仅是压测起点,不是生产结论 实际测试时,应同时制造高并发读写、缓存删除超时和数据库事务变慢三种条件,然后统计第二次删除前后仍然存在旧版本的比例。

如果延迟从 300ms 增加到 1 秒,旧值比例几乎不再下降,却明显增加写请求等待或任务堆积,就说明继续拉长延迟没有收益。我的建议是把延迟双删当作降低概率的补丁,而不是唯一方案。生产环境还应记录每次删除的 Key、版本、结果和重试次数;第二次删除失败时进入可靠补偿队列。

对支付状态、库存这类高风险数据,则优先考虑版本校验或强制回源,而不是单纯把删除延迟调得更长。

4. 线上出现缓存旧值,DBA 应该按照什么顺序排查?

业务反馈某个用户看到的订单状态和后台数据库不一致,但监控显示数据库延迟、缓存命中率和接口耗时都正常。以前我们遇到这类问题就直接清理缓存,结果问题过几天又出现,我想建立一套既能快速止损、又能定位根因的排查流程。

第一步不是清缓存,而是先确认用户实际命中了哪一层缓存。一个请求可能先读本地缓存,再读分布式缓存,最后才访问数据库;如果只删除了分布式缓存,本地缓存仍然可以继续返回旧对象。排查时需要拿到请求 ID、业务对象 ID、缓存 Key、缓存版本和生成时间。第二步要确认数据库中的“最新值”究竟在哪个节点。

写入主库成功,不代表只读副本已经同步;如果应用回源读取副本,旧数据可能再次进入缓存。建议把主库版本、副本版本、复制延迟和缓存版本放在同一条排查记录中,而不是分别查看几个监控页面。排查顺序需要确认的问题常见结论 1. 读取路径命中了本地缓存、分布式缓存还是数据库?

多级缓存未同步或清理范围不完整 2. 数据节点当前读取的是主库还是只读副本?副本延迟造成旧值回源 3. 版本信息缓存版本是否小于数据库版本?旧数据回填或乱序覆盖 4. 失效链路删除、消息、重试是否成功?缓存失效失败或事件积压 5. 恢复结果清理后是否会再次产生旧值?

清缓存只能止血,未解决设计缺陷 止损动作应根据业务风险选择。订单状态或权限错误时,可以临时让高风险查询绕过缓存并读取主库;普通商品描述则可以删除单个 Key,并设置较短 TTL。不要在没有确认影响范围的情况下执行全量清缓存,因为这可能造成瞬时回源洪峰,反过来把数据库压垮。

根因定位完成后,要做一次可复现测试:并发写入和读取、数据库事务回滚、缓存删除超时、消息重复消费、主从延迟以及服务重启后的缓存重建。只有在这些场景下都能记录数据库值、缓存值和用户最终读值,才算真正验证了修复。我的经验判断是,反复清缓存却没有增加版本、失效失败告警和补偿机制,问题几乎一定会再次出现。

核心关键词

读者评论

张可欣

文章把缓存问题从单纯的性能指标扩展到数据正确性,尤其是“删除缓存后被旧值回填”的并发时序,解释得比较清楚。实际排查时,确实不能只看主库和命中率,还要结合副本延迟、回填日志和版本信息。

武文博

延迟双删并不是万能方案这一点很有参考价值。支付、库存、权限等场景对一致性要求更高,单靠TTL或固定延迟很难保证可靠,最好配合事务提交后的消息、失败重试和版本校验。

袁书瑶

文章覆盖了本地缓存、分布式缓存和只读副本的差异,但落地时还需要根据业务容忍的旧值时间制定指标。建议补充一套可执行的监控示例,例如失效失败率、旧值回源比例和缓存版本不一致告警。

免责申明:本文内容通过AI工具匹配关键字智能整合而成,仅供参考,帆软及九数云不对内容的真实、准确或完整作任何形式的承诺。如有任何问题或意见,您可以通过联系jiushuyun@fanruan.com进行反馈,九数云收到您的反馈后将及时处理并反馈。
咨询方案
咨询方案二维码

扫码咨询方案

热门产品推荐

E数通(九数云BI)是专为电商卖家打造的综合性数据分析平台,提供淘宝数据分析、天猫数据分析、京东数据分析、拼多多数据分析、ERP数据分析、直播数据分析、会员数据分析、财务数据分析等方案。自动化计算销售数据、财务数据、绩效数据、库存数据,帮助卖家全局了解整体情况,决策效率高。

相关内容

查看更多
运营管理平台使用技巧:异常预警对应的精细化运营方法

运营管理平台使用技巧:异常预警对应的精细化运营方法

运营管理平台真正难用的地方,通常不是不会配置预警,而是预警触发之后没人知道该做什么。一个团队每天收到几十条“转 […]
运营管理平台中小商家:流程配置从哪里开始

运营管理平台中小商家:流程配置从哪里开始

中小商家配置运营管理平台时,最容易犯的错误,是一打开系统就从“订单、库存、审批、报表、权限”这些功能菜单开始逐 […]
想做好运营管理平台,先掌握中小商家中的数据看板

想做好运营管理平台,先掌握中小商家中的数据看板

很多中小商家并不是没有数据,而是每天被数据追着跑:老板在群里问销售额,店长打开收银系统,运营人员去看投放后台, […]
运营管理平台优化清单:数据看板与精细化运营的关键动作

运营管理平台优化清单:数据看板与精细化运营的关键动作

运营管理平台优化清单:数据看板与精细化运营的关键动作 很多企业的运营管理平台并不缺数据,真正缺的是“数据出现之 […]
运营管理平台决策指南:用精细化运营判断流程配置方案

运营管理平台决策指南:用精细化运营判断流程配置方案

运营管理平台决策指南,真正要解决的不是“哪个平台功能最多”,而是“哪种流程配置能够让业务动作被准确执行、过程被 […]

让电商企业精细化运营更简单

整合电商全链路数据,用可视化报表辅助自动化运营

让决策更精准