数据库存:架构师进阶教程:围绕缓存同步建立改善缓存同步闭环
目录

数据库存:架构师进阶教程:围绕缓存同步建立改善缓存同步闭环 | 九数云-E数通

eshutong 发表于2026年9月16日

《数据库存:架构师进阶教程:围绕缓存同步建立改善缓存同步闭环》真正要解决的,不是“数据库更新后该不该删缓存”这样一个孤立问题,而是数据库、缓存、消息队列、读副本和监控系统之间如何形成一条可发现、可修复、可验证的同步链路。很多线上脏数据并不是缓存命中率低造成的,而是系统根本不知道缓存已经错了:数据库中的商品价格已经变更,缓存仍保留旧值;订单状态已经支付成功,页面却继续显示待支付;

权限已经撤销,某个服务仍依据旧缓存放行。缓存同步的终点也不是追求每一次读写都绝对同步,而是让业务明确“允许多旧的数据”,让系统在偏离后能够自动恢复,并且用指标证明恢复确实发生。

数据库存:架构师进阶教程:围绕缓存同步建立改善缓存同步闭环

一、先讲核心结论:缓存同步的终点不是删除缓存,而是建立闭环

1. 缓存同步本质上是多副本治理

在没有缓存的系统中,数据库通常是事实来源,应用只需要处理数据库连接、事务和读写隔离。引入缓存之后,同一份业务数据至少存在于数据库和缓存两个位置。如果数据库还有主从复制、搜索索引、数据仓库或本地进程缓存,实际副本数量会进一步增加。

因此,缓存并不是简单的“加速组件”,而是一个派生数据副本。只要存在副本,就会出现传播延迟、更新失败、消息重复、消息乱序和副本过期等问题。架构设计的重点不应停留在“把数据放进缓存”,而要继续回答四个问题:谁是权威数据源,数据如何传播,偏差如何发现,发现后如何修复。

我的核心判断是:缓存同步方案的质量,不由正常链路有多顺畅决定,而由异常链路能否收敛决定。正常情况下执行一次删除缓存并不难,真正拉开架构差距的是缓存删除超时、数据库读副本延迟、消息重复消费以及并发回填同时发生时,系统是否仍然能够回到正确状态。

2. 先定义一致性目标,再选择同步技术

不同业务对“缓存旧数据”的容忍度完全不同。商品详情页的描述晚几秒刷新,通常不会造成严重后果;账户余额、库存扣减、用户权限和支付状态则不能依赖一个可能陈旧的缓存直接做最终决策。

我通常会先要求业务方给出一个可量化的约束,而不是直接讨论 Redis、消息队列或延迟双删。例如:“商品价格最多允许陈旧 5 秒”“权限撤销后 1 秒内必须对所有新请求生效”“库存展示可以短暂不准,但库存扣减必须以数据库条件更新结果为准”。这类约束会直接决定缓存能否参与业务判断,以及同步链路需要达到什么可靠性。

业务目标允许的数据状态缓存可以承担的角色推荐控制手段
允许短暂陈旧数秒内可读旧值展示加速、降低数据库压力Cache Aside、短 TTL、异步删除和定时校验
读己之写当前用户写入后应读到自己的新值读路径优化写后本地绕过缓存、版本标记、会话级路由
最终一致经过传播和补偿后必须收敛派生副本可靠事件、幂等消费、重试、死信和抽样校验
强一致决策不能依据旧值做关键决策只做展示或查询加速关键写操作回源、数据库条件更新、版本校验

数据库存:架构师进阶教程:围绕缓存同步建立改善缓存同步闭环

3. 一个完整闭环至少包含六个环节

我把缓存同步闭环拆成六个环节:定义、写入、传播、检测、修复和复盘。定义是明确主数据源、最大陈旧时间和关键字段;写入是决定数据库与缓存的动作顺序;传播是通过删除、更新、消息或 CDC 将变更送达副本;检测是发现超时、失败和不一致;修复是重试、重建或强制回源;复盘则是把一次事故转化为监控、代码或流程上的改进。

  1. 定义:明确数据权威来源,以及业务可接受的一致性级别。
  2. 写入:确定事务边界,避免数据库和缓存各自成功或失败后无人负责。
  3. 传播:设计同步事件、消息投递、删除策略和版本控制。
  4. 检测:监控删除失败、消息延迟、缓存陈旧和业务脏读。
  5. 修复:提供实时重试、延迟重试、扫描重建和人工兜底。
  6. 复盘:分析未发现、未修复或重复发生的原因,更新治理规则。

缺少其中任何一个环节,都可能形成“看起来能运行、实际上不可治理”的缓存架构。尤其是检测和修复,常常被当作运维问题推迟处理,最后只能靠重启服务或清空整个缓存来解决,这种方式既不可控,也容易把局部问题扩大成数据库流量事故。

二、真实场景:为什么数据库已经更新,用户仍然看到旧数据

1. 一个典型的商品价格失步链路

下面用一个常见的商品详情场景说明问题。商品表中的价格是 109 元,缓存 Key 为 product:10086。运营人员将价格改为 99 元,应用完成数据库更新后删除缓存。看上去流程非常简单,但线上可能同时存在一个正在执行的读请求。

  1. 请求 A 读取缓存,拿到价格 109 元。
  2. 请求 B 更新数据库,价格变为 99 元。
  3. 请求 B 执行删除缓存,由于网络超时,客户端未确认执行结果。
  4. 请求 A 将此前读到的 109 元重新写入缓存。
  5. 后续请求持续命中 109 元,直到 TTL 到期。

这里最容易被忽略的地方是:删除缓存和读请求回填之间没有建立顺序保护。即使删除动作在 Redis 服务端已经成功,旧读请求也可能在稍后把旧值重新写进去。很多团队因此直接增加一次延迟删除,但延迟双删并不是绝对一致性方案,它只是针对某一类并发窗口降低脏数据回填概率。

2. 主从延迟会让“正确的删除”再次变错

另一类问题发生在数据库读写分离架构中。更新请求写入主库后立即删除缓存,随后一个缓存未命中的读请求被路由到只读副本。由于复制延迟,副本仍返回旧价格,应用又把旧价格回填到缓存中。

这时 Redis 的删除操作没有失败,应用的代码也没有明显错误,真正的问题是“删除缓存后,回源读到了不具备最新视图的数据”。如果排查人员只盯着缓存日志,很容易得出“缓存删除成功,问题无法复现”的错误结论。

这也是我在设计读写分离系统时坚持标注数据来源的原因:日志里不能只记录“查询商品成功”,还应该记录请求使用的是主库还是副本、当时副本延迟是多少、回填的业务版本是多少。没有这些字段,排查只能依靠猜测。

数据库存:架构师进阶教程:围绕缓存同步建立改善缓存同步闭环

3. 订单状态比商品详情更容易暴露同步设计缺陷

商品详情旧几秒,用户可能只是觉得页面没有刷新;订单状态旧几秒,则可能引发重复支付、重复发货或客服投诉。订单状态还常常会同时被订单服务、支付服务、通知服务和前端查询接口读取,缓存副本数量更多,传播路径也更长。

例如,支付服务已经将订单状态改为“已支付”,订单服务发布状态变更事件,通知服务消费成功,但查询接口所在服务的本地缓存没有清理。用户刷新页面时,API 网关命中本地缓存;绕过本地缓存后又命中分布式缓存;只有清空两层缓存才显示正确状态。

这个场景说明,缓存同步设计不能只画数据库到 Redis 的一条箭头。只要存在本地缓存、网关缓存、分布式缓存或搜索索引,就必须明确每一层副本的失效责任、最大延迟和兜底方式。

4. 缓存命中率高,可能反而掩盖了数据错误

缓存命中率是性能指标,不是正确性指标。一个长期保存旧值的 Key,命中率可以非常高;如果系统没有版本比对或业务抽样校验,监控面板甚至会显示“缓存运行良好”。

因此,我不会单独用命中率评价缓存治理效果。至少应将命中率、缓存回源比例、同步延迟、删除失败率和抽样不一致率放在同一张监控视图中。命中率上升但不一致率也上升,通常不是优化成功,而是错误副本被更稳定地复用了。

数据库存:架构师进阶教程:围绕缓存同步建立改善缓存同步闭环

三、常见误区:很多“缓存一致性方案”只覆盖了成功路径

1. 误区一:先写数据库再删缓存就一定安全

“写数据库,再删除缓存”是 Cache Aside 中非常常见的策略,在许多读多写少场景下也确实实用。但它不是一个原子操作,数据库和缓存之间仍然存在时间窗口。数据库写成功而缓存删除失败时,旧值会继续被读取;删除成功后并发读请求回填旧值时,缓存仍可能重新变脏。

准确的表达应该是:先写数据库再删缓存可以降低缓存长期保存旧值的概率,但必须配合失败重试、幂等处理、版本控制和定期校验。如果业务要求极高,关键决策不能只依赖缓存。

2. 误区二:延迟双删可以解决所有并发问题

延迟双删通常是先删除缓存,更新数据库,再等待一段时间后再次删除缓存。它的设计目标是清理并发读请求可能回填的旧值。这个方法在工程上有价值,但它依赖延迟时间的选择,也无法解决所有问题。

如果数据库读副本延迟超过设定的等待时间,第二次删除后仍可能有旧数据回填。如果删除任务本身失败,延迟双删也没有自动恢复能力。如果服务在两次删除之间宕机,第二次删除根本不会执行。因此,延迟双删最多是并发窗口治理手段,不应被描述成一致性保证。

3. 误区三:加上消息队列就实现了最终一致

消息队列解决的是事件传播问题,不会自动解决事件可靠性、消费幂等、乱序、积压和死信处理。数据库事务提交成功后,如果消息发送失败,缓存消费者永远收不到变更;消息发送成功后,如果消费者执行删除缓存超时,重试又可能产生重复操作。

我会把消息同步方案拆成三个问题单独评审:变更事件是否可靠产生,事件是否能够至少送达一次,消费者是否能够安全重复执行。只有这三个问题都得到回答,才有资格讨论“最终一致”。

4. 误区四:TTL 设置得足够短就不需要同步

TTL 只能限制脏数据最长存活时间,不能消除脏数据。对于高频访问的 Key,短 TTL 会带来频繁回源和缓存重建;对于权限、库存等敏感数据,即使旧值只存在 1 秒,也可能在关键窗口造成业务错误。

TTL 更适合作为最后一道保险,而不是同步方案本身。同步负责尽快传播变更,TTL 负责在同步链路完全失效时限制损失范围,两者的职责不能混淆。

5. 误区五:全量清空缓存是最简单的修复办法

清空缓存确实能够让数据重新从数据库加载,但它把局部一致性故障转化成全局回源风暴。尤其是在数据库本身已经承压、缓存集群刚发生网络抖动或热点数据非常集中的情况下,批量失效可能造成数据库连接池耗尽。

更稳妥的做法是先确定受影响的业务 Key,再按批次、按优先级、按访问量执行重建。对不能确定范围的故障,应先限流和保护数据库,再逐步恢复,而不是直接执行全量删除命令。

6. 误区六:把缓存对象序列化后整体覆盖

整体覆盖看起来简单,却容易产生字段级并发覆盖。例如,两个请求分别更新商品库存和商品描述,两个请求都读取旧对象后修改各自字段,最后写入缓存的请求可能覆盖前一个请求的更新。

如果业务对象的字段更新频率差异很大,我会优先考虑拆分缓存 Key、使用版本号或在数据库完成合并后再删除缓存。缓存结构越复杂,直接“读出对象、改一个字段、整体写回”的风险越高。

三、常见误区:很多“ 缓存一致性 方案”只覆盖了成功路径

四、专业判断逻辑:架构师应如何选择同步模式

1. 先画出事实来源和派生副本

在评审缓存同步方案时,我不会先看代码,而是要求团队画一张数据流图。图中至少标出数据库主库、数据库副本、分布式缓存、本地缓存、消息队列、搜索系统以及每个服务的读写方向。

然后为每个节点写下三项信息:数据是否权威,允许延迟多久,出现不一致时由谁修复。很多争议并不是技术争议,而是团队没有明确某个节点到底是事实来源还是临时副本。

节点是否权威典型数据延迟不一致后的责任
数据库主库通常是事务提交后可见由数据库事务和数据修复流程负责
数据库只读副本不是毫秒至秒级,取决于复制状态由读路由和延迟监控负责
分布式缓存通常不是毫秒至分钟级由失效、更新、重建和补偿负责
本地进程缓存不是通常受 TTL 和进程生命周期影响由广播失效或版本校验负责
搜索索引通常不是秒级至分钟级由索引消费和重建任务负责

2. Cache Aside 适合大多数普通查询,但不要忽略写失败

Cache Aside 的优点是应用对数据库和缓存都有明确控制,系统组件少,出现问题时容易理解。商品详情、文章内容、门店信息和低风险配置通常可以采用这种模式。

它的最小可靠实现应包含:缓存未命中时的回源保护,数据库更新成功后的缓存失效,缓存操作失败后的重试记录,以及周期性一致性抽样。只写三行代码“更新数据库、删除缓存、结束请求”,在低流量测试环境看不出问题,一旦进入并发和故障场景,缺口会迅速暴露。

3. Write Through 适合统一写入口,但成本不一定更低

Write Through 将写入动作封装到缓存层,由缓存组件负责同步数据库。它可以让应用代码更简洁,并减少业务方各自实现双写逻辑的机会,但系统会更加依赖缓存层的持久化能力、事务语义和故障处理。

如果团队没有成熟的缓存抽象层,不建议为了追求“写路径统一”而强行引入。对于多个服务共同修改同一业务实体的系统,Write Through 还需要解决字段合并、权限校验、事务回滚和事件发布问题,实际复杂度可能高于 Cache Aside。

4. Write Behind 不能用于不能丢数据的事实写入

Write Behind 先写缓存,再异步落库,可以显著降低写入延迟,适用于计数、行为收集、临时排行榜或允许延迟持久化的场景。但它的本质是把数据库提交从同步路径移到了异步路径,数据丢失和落库延迟必须被业务接受。

如果业务人员说“库存写入要快”,不能直接推导出 Write Behind。库存扣减的正确性通常比写入延迟更重要,更适合在数据库中使用条件更新或专用原子操作,缓存只用于展示可用库存。

数据库存:架构师进阶教程:围绕缓存同步建立改善缓存同步闭环

5. 消息或 CDC 适合多副本传播,但要接受异步复杂度

当一个数据库变更需要同步到多个缓存、搜索索引、推荐特征或下游服务时,逐个调用下游系统会让主事务变得脆弱。此时可以使用 Outbox、事务消息或 CDC,将数据库变更转化为可靠事件,再由不同消费者独立处理。

需要强调的是,CDC 不是“自动一致性按钮”。它只能更可靠地捕获数据库变化,不能替代缓存 Key 设计、消息版本控制、消费幂等、失败重试和数据校验。引入 CDC 前,应先确认团队是否有能力运维日志位点、消费延迟、断点续传和重建流程。

五、具体案例:从商品价格同步看一套可落地的闭环

1. 场景设定与基线数据

以下案例是一个用于说明架构机制的情景模拟,不对应某个具体公司的生产数据。假设一个商品详情接口每天接收 300 万次查询,商品价格和库存由运营后台修改,详情对象缓存在分布式缓存中,数据库采用一主两从架构。

上线初期,团队只实现了“更新数据库后删除缓存”。在功能测试中,接口 P95 延迟约 42 毫秒,缓存命中率为 91%。但经过一周的日志抽样,发现价格字段不一致率约为 0.6%,其中约四分之一的异常持续超过 30 秒。

这个数字看起来不大,但如果每天有 300 万次查询,0.6% 并不是“偶尔发生”,而是可能影响约 1.8 万次请求。更重要的是,抽样只能看到部分异常,未被采样的热点 Key 可能造成更大的用户影响。

2. 先定位失步来源,而不是直接增加 TTL

团队最初的直觉是把缓存 TTL 从 10 分钟降到 2 分钟,希望通过更快过期减少旧数据存活时间。结果是数据库回源 QPS 上升约 3.4 倍,接口 P95 延迟升至 77 毫秒,价格不一致率只从 0.6% 降到 0.45%。

这次调整说明,TTL 只是缩短错误的生命周期,没有解决错误产生的原因。继续排查日志后,异常主要来自三个路径:缓存删除超时、只读副本延迟造成旧值回填,以及本地缓存未及时失效。

异常来源抽样异常占比主要表现修复方向
缓存删除超时约 38%数据库版本已更新,缓存仍为旧版本记录删除任务并可靠重试
只读副本延迟约 34%缓存删除后回源读到旧版本关键回填读主库或使用版本保护
本地缓存未失效约 21%分布式缓存正确,应用进程仍返回旧对象广播失效、短 TTL 和版本校验
Key 或序列化异常约 7%部分字段无法更新或删除目标错误统一 Key 规范和结构化校验

表中的占比属于该情景的样本推演,重点不是这些数字能代表所有系统,而是说明排查时应把“不一致”拆成可验证的原因类别。只有知道哪一类占比最高,才有必要决定是改读路由、改缓存操作,还是改事件链路。

数据库存:架构师进阶教程:围绕缓存同步建立改善缓存同步闭环

3. 改造后的最小闭环

改造方案没有一开始就引入复杂的全链路 CDC,而是先对普通商品详情建立最小闭环。数据库仍作为权威数据源,更新事务提交后发布一个包含商品 ID、业务版本和更新时间的事件,缓存消费者执行幂等删除,本地缓存通过广播收到失效通知。

对于缓存未命中的回源请求,优先读取主库,或者读取带有版本约束的副本。如果只能读取副本,则回填时记录数据库版本,禁止低版本数据覆盖缓存中的高版本数据。对删除超时的任务,写入重试表,按照 1 秒、5 秒、30 秒和 5 分钟的退避策略执行,超过次数后进入死信队列。

代码层面,事件消费不应只依赖一个布尔返回值。需要区分“删除成功”“Key 不存在”“连接超时但结果未知”“参数错误”和“不可重试异常”。其中“Key 不存在”可以视为幂等成功,而“连接超时但结果未知”应进入重试流程。

public void handleProductChanged(ProductChangedEvent event) {
String key = "product:" + event.getProductId();

try {

CacheVersion current = cache.getVersion(key);

if (current != null && current.version() > event.getVersion()) {

// 旧事件不能覆盖新版本,记录后安全结束

auditLog.ignoreOutOfOrderEvent(event, current);

return;

}

cache.delete(key);

retryRepository.markSuccess(event.getEventId());

} catch (TimeoutException ex) {

// 结果未知,不能直接判定失败或成功

retryRepository.scheduleRetry(

event.getEventId(),

event.getProductId(),

event.getVersion(),

nextBackoff(event.getRetryCount())

);

} catch (InvalidKeyException ex) {

// 参数错误不适合无限重试,进入人工处理队列

deadLetterRepository.save(event, ex.getMessage());

}

}

这段代码只是示意,不能直接复制到生产环境。它表达的重点是三个:事件必须带版本,消费必须幂等,超时必须区别于明确失败。很多重复消费问题并不是消息系统造成的,而是消费者把所有异常都简单处理成“失败重试”或“成功结束”。

4. 改造后的观察结果

在这个情景中,改造后缓存命中率从 91% 降到 89%,看起来性能指标略有下降,但数据库回源比例只从 9% 上升到 11%,接口 P95 从 42 毫秒升至 45 毫秒。与此同时,抽样不一致率从 0.6% 降到 0.08%,超过 30 秒的异常从每周约 1,200 次降到 90 次以内。

这组数据说明,缓存治理不能只追求命中率最大化。为了让缓存更快失效、降低脏数据存活时间,命中率可能略有下降;只要数据库压力、延迟和一致性指标处于业务可接受范围,这种下降是值得的。

数据库存:架构师进阶教程:围绕缓存同步建立改善缓存同步闭环

六、如何设计检测体系:让系统主动告诉你缓存哪里错了

1. 先监控同步动作,再监控最终结果

很多监控只统计缓存操作错误,例如 Redis 命令失败次数,却没有统计业务数据最终是否一致。操作成功不等于结果正确,操作失败也不一定导致最终错误,因为后续重试可能已经修复。

我建议将监控分为三层。第一层是基础设施层,包括缓存连接错误、超时、内存、淘汰和主从状态;第二层是同步过程层,包括事件发送成功率、消费延迟、重试次数和死信数量;第三层是业务结果层,包括版本不一致率、脏读次数和修复耗时。

监控层核心指标告警价值不能单独说明什么
基础设施层连接错误率、命令延迟、内存使用率发现缓存服务本身是否异常不能证明业务数据一定正确
同步过程层事件延迟、消费成功率、重试和死信发现变更传播是否受阻不能证明每个 Key 都已恢复
业务结果层抽样不一致率、陈旧时长、脏读次数直接衡量用户影响和一致性需要设计合理采样和对账口径
修复运营层平均修复耗时、人工介入量、重复故障率判断闭环是否持续改善不能替代根因分析

2. 用版本号比用时间戳更稳妥

时间戳看起来容易实现,但在多机器、时钟漂移和高并发更新场景下,时间戳并不总能可靠表示新旧关系。两个服务生成的时间可能相同,时钟回拨也可能让旧事件看起来更新。

如果业务允许,我更倾向于使用数据库生成的递增版本号,或者使用同一行的变更序列号。缓存中保存值和版本,事件中携带同一版本。消费者收到事件时,如果发现当前缓存版本高于事件版本,就丢弃旧事件;如果事件版本更高,则执行更新或删除。

3. 抽样校验应避免再次压垮数据库

一致性校验不能简单地每分钟全量扫描数据库和缓存。这样做会产生额外数据库压力,甚至在业务高峰时和正常查询竞争资源。更稳妥的方式是分层抽样:热点 Key 提高抽样频率,低频 Key 降低频率;出现同步失败的 Key 优先检查;对关键业务字段采用更严格的校验。

校验时还要明确比较口径。是比较完整 JSON,还是只比较价格、状态、版本和更新时间?如果缓存中包含展示格式、计算字段或随机推荐内容,完整对象比较可能产生大量无意义差异。对账字段应优先选择能够代表业务正确性的核心字段。

数据库存:架构师进阶教程:围绕缓存同步建立改善缓存同步闭环

4. 把“最大陈旧时间”变成告警条件

业务说“最终一致”往往过于宽泛。架构师应将它转成最大陈旧时间,例如商品详情不超过 30 秒,订单状态不超过 3 秒,权限数据不超过 1 秒。监控就可以据此判断某个 Key 是否已经越过业务红线。

告警条件不应只设置为“有一次删除失败”。如果某个 Key 删除失败后在 200 毫秒内重试成功,可能无需触发高等级告警;如果消息堆积持续 2 分钟,或某个订单状态超过允许时限仍未同步,则应升级为业务告警。

七、如何设计修复机制:从重试到重建,不要只保留一个补偿按钮

1. 实时重试要区分错误类型

缓存同步失败通常包括网络抖动、连接池耗尽、权限错误、Key 参数错误、序列化异常和数据版本冲突。网络超时可能适合退避重试,权限错误则需要立即报警,参数错误无限重试只会制造更多无效任务。

建议为异常分类建立处理策略:

  • 暂时性网络错误:采用指数退避,并设置最大重试次数。
  • 超时但结果未知:按照幂等操作处理,允许再次执行。
  • 明确的 Key 不存在:通常视为删除成功,记录审计日志即可。
  • 权限或配置错误:暂停批量重试,优先触发人工告警。
  • 版本冲突:根据版本规则丢弃旧事件或重新读取权威数据。
  • 序列化和数据契约错误:进入死信队列,不能依靠重复重试解决。

2. 延迟重试要避免重试风暴

当缓存集群或数据库发生故障时,大量同步任务会同时失败。如果所有任务在固定 5 秒后重试,就会形成第二次流量尖峰。重试策略应增加随机抖动,并根据业务优先级分层。

例如,权限失效和支付状态可以进入高优先级队列,商品推荐和浏览历史可以进入低优先级队列。重试时还应设置全局并发上限,确保补偿流量不会抢占正常业务所需的连接和 CPU。

3. 死信队列不是垃圾桶

死信队列的价值是保留无法自动处理的异常上下文,而不是把问题从主系统日志中隐藏起来。每条死信至少应包含业务 Key、事件 ID、版本、首次失败时间、最后失败原因、重试次数和当前数据摘要。

运维人员处理死信时,不能只点击“重新发送”。应先确认数据库当前版本和缓存当前版本,再决定删除缓存、重新构建、跳过事件或修正数据。否则,一个早已过时的死信事件可能在人工重放后覆盖新数据。

4. 定时扫描适合兜底,但要控制扫描范围

实时事件链路解决的是大部分正常变更,定时扫描解决的是事件丢失、消费者长时间宕机和历史脏数据。扫描可以只针对近期发生变更的记录、热点 Key、曾经失败的 Key 或业务风险较高的字段。

对于大型数据集,建议采用分片游标、限速、时间窗口和断点记录。扫描任务要能够暂停和恢复,并且把扫描产生的回源流量纳入数据库容量评估。没有限速的修复脚本,往往是第二次事故的起点。

数据库存:架构师进阶教程:围绕缓存同步建立改善缓存同步闭环

八、并发与顺序:缓存同步最容易被低估的工程细节

1. 旧读回填是一个时间顺序问题

很多团队只关注“谁最后写入”,却忽略了请求读取数据的时间。一个读请求可能在数据库更新之前拿到旧值,但在数据库更新之后才完成缓存回填。只看缓存写入完成时间,会误以为后写入的数据更可信,实际上它的业务版本可能更旧。

因此,缓存更新动作最好携带版本校验。无论采用直接更新还是删除后重建,都不应允许低版本事件覆盖高版本数据。对于无法在缓存层做版本判断的场景,删除缓存通常比直接写入旧对象更安全,因为下次回源可以重新获取权威数据。

2. 分布式锁不是一致性保证

分布式锁可以减少同一个热点 Key 的并发重建,但它不能保证数据库和缓存始终一致。锁可能过期,客户端可能在网络分区后失去锁状态,持锁进程也可能在完成数据库读取后长时间暂停。

我通常把锁的作用限定为“控制重建并发量”,而不是“保证数据正确”。数据是否正确,仍应由权威数据源、版本号和校验机制决定。锁的租期、续租、异常释放和数据库回源超时也需要纳入故障演练。

3. 热点 Key 要同时治理一致性和容量风险

热点 Key 的问题不只是缓存击穿。一个高访问量 Key 在更新时可能产生大量并发回源请求;如果同步事件不断删除它,缓存重建频率会升高,数据库可能被短时间打穿。

对于热点数据,可以使用逻辑过期、后台刷新、请求合并或预热。逻辑过期允许在短时间内继续返回旧值,同时由一个后台任务刷新;这适用于展示型数据,不适用于余额和权限。关键是把“允许旧多久”写进业务规则,而不是让技术人员自行决定。

4. 消息乱序必须有明确处理规则

假设商品价格先从 109 元变为 99 元,再变为 89 元。对应事件 v18 和 v19 可能因为不同分区、重试或消费者负载先后顺序而反向到达。如果消费者无条件执行更新,v18 可能在 v19 之后覆盖 89 元。

解决方式包括按业务 Key 分区保证顺序、在缓存中保存版本、消费者检测版本后丢弃旧事件,或者发生冲突时重新读取数据库。不同方式的成本不同,但“默认消息会按发送顺序到达”不能作为设计前提。

数据库存:架构师进阶教程:围绕缓存同步建立改善缓存同步闭环

九、数据库、缓存和消息如何形成可靠写入链路

1. 事务边界要优先保护权威数据

在大多数业务系统中,数据库应优先完成事务提交,缓存只承担派生副本更新。应用不能因为缓存写入失败就回滚已经成功的数据库事务,除非缓存本身被定义为事务的一部分,并且具备可验证的事务语义。

数据库事务成功后,应用需要可靠地产生同步事件。直接在事务提交后调用消息服务存在进程崩溃窗口:数据库已经提交,应用还没来得及发送消息就宕机。Outbox Pattern 的思路是将待发送事件和业务数据写入同一个数据库事务,再由后台投递器发送事件,从而避免“数据成功但事件丢失”。

2. Outbox 适用于需要可靠变更事件的场景

Outbox 表中的事件可以包含事件 ID、业务类型、业务 Key、版本号、操作类型、创建时间和投递状态。投递器发送成功后更新状态,发送超时则保留待重试。消费者以事件 ID 或业务版本实现幂等。

CREATE TABLE product_change_outbox (
event_id       BIGINT PRIMARY KEY,
product_id     BIGINT NOT NULL,
data_version   BIGINT NOT NULL,
event_type     VARCHAR(64) NOT NULL,
status         VARCHAR(20) NOT NULL,
retry_count    INT NOT NULL DEFAULT 0,
next_retry_at  TIMESTAMP NULL,
created_at     TIMESTAMP NOT NULL,
updated_at     TIMESTAMP NOT NULL,
UNIQUE KEY uk_product_version (product_id, data_version)
);

这类设计增加了数据库写入和投递维护成本,但它解决了一个非常具体的问题:业务数据提交和同步事件产生之间不再是两个完全独立的动作。对于低风险、低价值缓存,可以不必引入;对于订单状态、权限和多个下游副本,可靠事件通常更值得。

3. 事件表不能无限增长

Outbox 的另一面是数据生命周期治理。事件表如果没有归档和清理策略,最终会影响索引、扫描和写入性能。建议将“待投递”“重试中”“已完成”“死信”分开管理,已完成事件按照保留周期归档,死信则保留足够的审计信息并定期转存。

在高并发系统中,投递器不应对整张表执行无条件排序和扫描。可以使用状态、下一次重试时间、分片键和索引游标,控制每次批量大小。同步链路的可靠性不能以拖慢主库为代价。

4. CDC 适合集中治理,但需要成熟运维能力

CDC 可以从数据库日志中捕获变更,减少业务代码漏发事件的风险。它尤其适合多个服务都需要感知同一数据变化的场景。不过,CDC 链路一般会引入日志位点、消费组、表结构变更、全量初始化和增量切换等运维问题。

如果团队目前连缓存删除失败率、消费延迟和死信数量都没有监控,直接引入 CDC 往往只是把问题从应用代码转移到数据同步平台。正确的顺序通常是先建立事件模型和观测指标,再评估是否需要用 CDC 替代手工事件。

十、不同业务情况下的行动建议

1. 读多写少的普通展示数据

商品详情、文章内容、门店介绍和低风险配置,可以从简单方案开始。数据库作为权威数据源,读请求采用 Cache Aside,写请求提交数据库后删除缓存,失败任务进入重试队列,TTL 作为兜底,定期对热点 Key 做抽样校验。

这类业务不需要一开始就追求强一致。更重要的是明确最大陈旧时间,控制缓存击穿和重建风暴,并把删除失败纳入监控。如果数据变更频率很低,可靠删除加定时校验往往比复杂的实时同步平台更经济。

2. 订单状态和支付状态

订单状态通常适合使用可靠事件传播,但查询接口不能完全依赖缓存作为最终事实。支付回调更新数据库后,应产生带版本的状态事件,订单查询可以使用缓存加速,但在关键状态转换、支付确认和发货判断处必须回到权威数据源或使用带版本的条件判断。

对用户展示而言,可以允许极短时间的异步刷新;对内部决策而言,不能因为缓存显示“待支付”就重复发起支付,也不能因为缓存显示“已支付”就跳过数据库校验。展示一致性和决策一致性应当分开设计。

3. 库存和额度数据

库存展示可以使用缓存,但库存扣减不应依赖缓存中的普通数值。正确的扣减动作通常需要数据库条件更新、原子计数器或专门的库存服务,缓存只用于降低查询压力。

如果库存缓存与数据库短暂不一致,页面显示多几个库存并不一定是严重问题;但如果业务根据缓存判断“还有库存”并直接创建订单,就可能出现超卖。因此,必须把展示链路和扣减链路拆开,不能因为缓存命中率高就让缓存承担交易约束。

4. 用户权限和风控结果

权限撤销、黑名单和风控拦截属于错误代价很高的数据。此时可以使用短 TTL、广播失效、版本校验和关键请求强制回源等组合策略。对于高风险接口,即使缓存命中,也应在数据库或专门的权限服务中进行最终确认。

权限缓存的修复优先级通常高于商品详情。告警阈值应更严格,不能等到定时扫描才发现。只要存在撤销后仍然放行的可能,就要进行针对性的故障演练,包括缓存集群不可用、消息延迟和本地缓存未清理。

5. 多级缓存系统

当系统同时存在浏览器缓存、网关缓存、本地缓存和分布式缓存时,最先要解决的不是同步速度,而是失效协议。每一层都应有版本、过期时间和失效方式,至少要知道某一层返回旧值时,如何让请求继续向权威数据源前进。

多级缓存不建议全部采用相同 TTL。可以根据数据风险和缓存层位置设置不同策略,但必须评估失效传播的最坏延迟。若本地缓存的 TTL 是 10 分钟,即使分布式缓存已经删除,用户仍可能在本地进程中看到旧数据。

数据库存:架构师进阶教程:围绕缓存同步建立改善缓存同步闭环

十一、哪些方案值得采用,哪些取舍必须提前说清楚

1. 删除缓存与更新缓存的取舍

删除缓存的优点是逻辑简单,缓存不需要复制复杂的业务更新逻辑,适合对象结构复杂或字段变化频繁的场景。缺点是下次请求需要回源,热点 Key 可能产生瞬时重建压力。

直接更新缓存可以减少回源,适合数据结构稳定、更新逻辑简单且缓存对象与数据库字段高度一致的业务。但它要求应用同时维护两套写入逻辑,任何字段遗漏都可能形成隐性不一致。

选择优点代价更适合的场景
删除缓存实现简单,降低业务逻辑重复需要回源和重建保护复杂对象、读多写少、低风险展示
更新缓存读取延迟稳定,减少回源需要维护双写逻辑和版本顺序结构稳定、访问极热、更新逻辑简单
软失效可保留旧值进行降级必须明确陈旧数据边界允许短暂旧数据的展示型业务
强制回源正确性更容易控制数据库压力和延迟上升权限、余额、交易决策

2. 同步消息与定时校验的取舍

实时消息适合缩短传播延迟,但系统复杂度高,需要处理投递、消费和重试。定时校验实现成本较低,适合作为最后一道防线,但它无法保证短时间内发现问题。

二者不是互相替代的关系。成熟方案通常是“实时同步负责快速传播,定时校验负责发现遗漏,重试和死信负责处理异常”。如果只能先做一个,我会根据业务的最大陈旧时间决定:允许分钟级陈旧的展示数据可以先做定时校验;支付、权限和订单状态则应优先建立可靠事件。

3. 强一致与性能的取舍

强一致通常意味着更多回源、更少缓存参与决策、更严格的事务和版本控制。它会增加延迟、数据库压力和系统实现成本,但在错误代价高的场景中,这些成本是必要的。

最终一致并不是低质量方案。对于商品描述、推荐结果和非关键统计,最终一致可以用较低成本换取较高吞吐。真正不专业的做法,是在没有业务授权的情况下默认所有数据都可以旧,也没有任何机制证明旧数据最终会被修复。

4. 复杂同步平台与简单可靠代码的取舍

不是所有系统都需要 CDC、事务消息、分布式锁、版本控制和全量校验同时上场。复杂方案会增加部署、运维、排障和人才成本。对于只有一个服务、一个缓存集群、低频更新的系统,一套带重试和抽样的 Cache Aside 可能已经足够。

但简单不等于省略关键环节。即使不引入消息平台,也应至少保留删除失败记录、重试机制、TTL 兜底、关键字段校验和告警。架构取舍应该建立在业务风险和故障成本上,而不是建立在技术名词的数量上。

数据库存:架构师进阶教程:围绕缓存同步建立改善缓存同步闭环

十二、上线前后的实施路线:不要一次性重写整条链路

1. 第一阶段:先建立可见性

第一阶段不急于改架构,先补齐数据。为缓存操作、数据库写入和同步事件增加统一的业务 Key、请求 ID、事件 ID、版本号和来源标记。没有这些字段,后续无法把一次数据库更新和一次缓存读取关联起来。

同时建立基础面板,至少包含缓存命中率、缓存操作延迟、删除失败率、事件消费延迟、重试数量、死信数量、数据库回源 QPS 和抽样不一致率。先观察一到两个业务周期,区分偶发错误和持续性错误。

2. 第二阶段:补上失败和重试

第二阶段处理最常见的失败路径。所有缓存删除和更新动作都要明确返回结果;超时要保留上下文并进入重试;重试必须幂等;超过阈值进入死信;死信需要有处理入口和审计记录。

这一步通常能解决大量“数据库已变更、缓存一直不变”的问题,而且改动范围比引入完整数据同步平台小。对低风险业务而言,做到这一步可能已经能满足目标。

3. 第三阶段:加入版本和顺序保护

当系统存在并发回填、多服务更新或消息乱序时,需要引入业务版本。版本可以来自数据库递增字段,也可以来自统一的变更序列。所有更新、删除、重建任务都要传递版本,消费者不得无条件接受旧事件。

版本控制会增加数据结构和测试成本,但它能够把“凭时间猜新旧”变成“依据明确序列判断新旧”。对于订单、权限和库存等业务,版本控制的收益通常高于实现成本。

4. 第四阶段:建立自动校验和修复

第四阶段让系统具备自我发现和自我恢复能力。校验任务按风险分层抽样,对发现的差异自动执行删除或重建,并记录修复前后版本。对于连续失败的 Key,进入人工处理队列,避免自动化系统无限重试。

修复任务必须有流量保护。建议设置每秒最大回源数、单批次最大 Key 数、热点 Key 并发上限和数据库保护阈值。当数据库延迟超过基线时,修复任务应自动降速或暂停。

5. 第五阶段:用故障演练验证闭环

真正上线前,至少应模拟以下故障:缓存删除超时、消息发送失败、消费者宕机、消息重复、消息乱序、只读副本延迟、本地缓存未失效以及数据库短时不可用。

演练不只看系统是否报错,还要记录四个时间:异常发生时间、被发现时间、自动修复开始时间和业务恢复时间。没有这些时间点,就无法判断闭环是否真的改善。

数据库存:架构师进阶教程:围绕缓存同步建立改善缓存同步闭环

十三、架构师排查缓存不一致时的标准顺序

1. 先确认数据库权威状态

排查第一步不是执行删除缓存,而是确认数据库主库当前值、事务提交状态和业务版本。要确认看到的不是未提交事务、错误租户、错误分片或错误环境数据。

如果数据库主库本身的数据就不对,清除缓存只会让错误值重新被加载。数据库事实错误和缓存副本错误必须分开处理,不能把所有页面显示异常都归因于缓存。

2. 再确认缓存 Key 是否正确

缓存问题中有一类非常隐蔽:应用删除的 Key 和读取的 Key 并不是同一个。租户 ID、语言、渠道、版本后缀、大小写或序列化方式不同,都可能导致删除动作“成功”,但目标数据仍然存在。

建议把 Key 生成逻辑集中封装,并在日志中打印结构化的 Key 组成字段,而不是只打印一串不可读的最终字符串。生产环境可以对敏感信息脱敏,但不能完全丢失定位所需的维度。

3. 检查同步事件的完整生命周期

事件排查要从产生、发送、入队、消费、执行到确认逐段核对。常见断点包括数据库事务已提交但 Outbox 未生成、事件已生成但投递器未发送、消息已发送但消费者过滤错误、消费者执行成功但状态未更新。

每段链路都应使用同一个事件 ID。没有统一事件 ID 的系统,只能通过时间和业务 Key 进行模糊关联,在高并发环境下非常容易把多个相似请求混在一起。

4. 最后检查回源和并发重建

如果缓存曾经被删除,仍然显示旧值,应检查是否有读请求将旧数据回填。重点查看回源数据库类型、回源开始时间、回源完成时间、数据库版本和缓存写入版本。

对于热点 Key,还要查看是否存在多个实例同时重建、锁提前过期或旧请求在锁释放后继续写入。很多“删了又出现”的问题,本质上不是删除失败,而是旧读请求晚到。

十四、代码与数据契约:把缓存同步从约定变成可执行规则

1. 统一缓存对象的元数据

缓存值不要只保存业务对象本身。至少可以考虑加入业务版本、写入时间、来源和逻辑过期时间。这样出现异常时,运维人员能够判断缓存值来自哪一次变更,以及它已经陈旧多久。

{
"data": {

"productId": 10086,

"price": 99.00,

"stockDisplay": 18

},

"meta": {

"version": 18,

"source": "product-service",

"writtenAt": "2026-09-16T10:20:31Z",

"logicalExpireAt": "2026-09-16T10:25:31Z"

}

}

并非所有场景都适合把元数据和业务对象一起序列化。如果缓存空间非常敏感,可以把版本单独放在 Key 或哈希字段中。但无论采用哪种方式,都要确保排查和校验能够读取版本信息。

2. 对缓存操作定义明确的结果语义

“删除缓存返回 true”并不等于“所有副本都已失效”。如果系统存在本地缓存、分布式缓存和网关缓存,删除结果必须明确对应哪一层。统一封装时,应把每层执行结果分别记录,并允许某一层失败后单独重试。

对于更新操作,也要规定低版本事件的处理方式。是丢弃、延迟、重新读取数据库,还是将 Key 标记为需要重建,必须在协议中写清楚。否则不同服务会按照各自理解处理同一个事件。

3. 读路径要防止缓存击穿和旧值回填

一个稳妥的读路径通常包括:先查缓存,未命中后进行单 Key 并发控制,再访问权威数据源,写入缓存时携带版本和过期时间。若回源失败,可以根据业务决定返回空、返回短期旧值或降级结果。

展示型数据可以允许短时间使用逻辑过期旧值,但订单状态和权限数据不宜使用无条件旧值。所有降级行为都应记录指标,否则团队可能长期不知道系统已经处于“带病运行”状态。

十五、如何用数据判断改善是否有效

1. 不要只比较改造前后的平均延迟

平均延迟很容易掩盖少数严重请求。缓存同步改造后,应重点比较 P95、P99、最大陈旧时长、数据库回源峰值和修复耗时。某个方案平均延迟下降,但 P99 因缓存重建风暴上升,用户体验未必真的改善。

一致性也要看分布,而不是只看平均值。平均陈旧时间 300 毫秒,可能意味着大多数请求正确、少量请求陈旧 10 分钟;对权限和支付场景来说,后者才是决定风险的指标。

2. 建议建立一张联合评估表

指标观察意义改善方向注意事项
缓存命中率衡量读取加速效果优化 Key、TTL 和预热不能代表数据正确
数据库回源比例衡量缓存对数据库的保护减少无效失效和重建风暴关键数据主动回源是合理成本
同步延迟 P95衡量变更传播速度优化队列、消费者和批量策略需按业务类型分组
抽样不一致率衡量副本正确性改进版本、失效和校验要说明采样范围和字段口径
最大陈旧时长衡量极端风险加强告警和死信处理比平均值更适合风险控制
平均修复耗时衡量闭环恢复能力增加自动重试和重建应区分自动和人工修复

3. 用基线而不是绝对数字做评估

不同业务的缓存命中率和一致性阈值差异很大。一个高频商品接口命中率 90% 可能已经不错,而一个低频权限接口命中率 60% 也许完全可以接受。不要脱离访问模式和数据风险制定统一目标。

更合理的做法是先建立七天或十四天基线,再观察改造后的变化。例如,改造前同步延迟 P95 为 8 秒,改造后下降到 1.2 秒;改造前最大陈旧时长为 20 分钟,改造后控制在 45 秒以内。这样的对比比直接引用一个脱离场景的“命中率应达到 95%”更有决策价值。

数据库存:架构师进阶教程:围绕缓存同步建立改善缓存同步闭环

十六、最终检查清单:一套缓存同步闭环是否合格

1. 数据源和一致性目标

  • 是否明确数据库主库、只读副本和缓存的权威关系?
  • 是否为每一类数据定义最大可接受陈旧时间?
  • 是否明确哪些字段只能展示,哪些字段可以参与业务决策?
  • 是否区分最终一致、读己之写和强一致要求?

2. 写入和事件传播

  • 数据库提交成功后,是否一定能产生同步事件?
  • 事件是否携带业务 Key、事件 ID、版本和变更类型?
  • 消息发送失败时是否有重试或 Outbox 记录?
  • 消费者是否能够安全地重复执行?
  • 是否对消息乱序和旧版本事件有明确处理规则?

3. 缓存读写和并发控制

  • 缓存未命中时是否有单 Key 并发保护?
  • 回源读取是否可能访问延迟副本?
  • 缓存回填是否携带版本或更新时间?
  • 热点 Key 失效时是否有预热、请求合并或限流?
  • 是否存在本地缓存、网关缓存等未纳入失效协议的副本?

4. 监控和补偿

  • 是否监控缓存操作超时和删除失败?
  • 是否监控消息延迟、重试、积压和死信?
  • 是否有数据库与缓存的抽样校验?
  • 是否能统计最大陈旧时长和业务脏读次数?
  • 是否提供实时重试、延迟重试、定时扫描和人工兜底?
  • 修复任务是否具备限速、暂停和断点恢复能力?

5. 故障演练和复盘

  • 是否模拟过缓存删除超时和结果未知?
  • 是否模拟过消息重复、乱序和消费者宕机?
  • 是否模拟过数据库只读副本延迟?
  • 是否记录发现耗时、修复耗时和业务恢复耗时?
  • 是否将事故复盘结果转化为代码、监控或流程变更?

十七、总结:把缓存从“性能技巧”升级为“可治理的数据副本”

1. 最重要的三个判断

第一,缓存同步不是一个删除命令,而是一套副本治理机制。只要数据在数据库和缓存之间传播,就必须考虑失败、延迟、重复、乱序和修复。

第二,命中率不是缓存系统的最终成绩。高命中率只能说明请求更常读到缓存,不能说明读到的是正确数据。同步延迟、不一致率、最大陈旧时间和修复耗时,才是闭环治理的核心指标。

第三,复杂技术不等于成熟架构。对于普通展示数据,Cache Aside 加可靠重试、TTL 和抽样校验可能已经足够;对于订单、权限和库存,则应让缓存退回加速角色,并用数据库或专用服务承担最终决策。

2. 下一步怎么做

如果你正在治理一套已经上线的缓存系统,建议不要从重写代码开始,而是先选择一个最容易出现脏数据、同时影响范围可控的业务对象,完成一次小范围闭环建设。

  1. 确定数据库主库和缓存 Key 的对应关系。
  2. 为变更事件增加事件 ID、业务版本和更新时间。
  3. 统计缓存删除失败、消息延迟和抽样不一致率。
  4. 补上幂等重试和死信处理。
  5. 对热点 Key 做并发重建保护。
  6. 设置最大陈旧时间和业务告警。
  7. 模拟一次缓存删除失败,验证系统能否自动恢复。
  8. 根据故障数据决定是否需要 Outbox、CDC 或更强的一致性方案。

我更愿意把缓存看成一个需要持续维护的派生数据系统,而不是一个“放进去就能加速”的黑盒。真正成熟的缓存架构,不是永远没有不一致,而是能够知道哪里不一致、为什么不一致、影响了什么、什么时候恢复,以及下一次如何避免同类问题再次发生。围绕这个目标建立检测、修复、验证和复盘闭环,才是数据库存储架构从“能用”走向“可治理”的关键一步。

常见问题解答(FAQ)

1. 为什么数据库写成功、删除缓存之后,仍然会出现脏数据?

我以前一直认为“先更新数据库,再删除缓存”已经足够安全,直到遇到缓存被旧值重新覆盖的问题。我想知道,明明删除动作已经执行成功,旧数据为什么还会再次回到缓存里?

“写数据库再删缓存”只是一个较稳妥的基础策略,不是原子操作。数据库和缓存属于两个独立系统,中间任何一步出现延迟、失败或并发交错,都可能导致缓存重新变脏。最容易被忽略的是旧值回填场景。假设商品原价为 109 元,更新请求把数据库改成 99 元;与此同时,一个正在执行的读请求已经拿到了旧值 109 元。

更新请求删除缓存后,这个读请求才完成查询并把 109 元重新写入缓存,最终缓存又恢复成了旧数据。另一个常见原因是从库延迟。数据库主库已经写入 99 元,但读请求访问了尚未同步完成的只读副本,读取到 109 元后再次回填缓存。此时问题不在缓存删除命令,而在读链路选择了一个落后的数据源。

我建议先画出完整时序,而不是直接加“延迟双删”。至少要标记数据库提交时间、缓存删除时间、读请求开始和结束时间、缓存重建来源以及数据库副本延迟。没有这些信息,延迟双删往往只是碰巧降低问题出现概率。

故障场景表面现象更合适的治理方式 删除缓存失败旧值持续命中可靠重试、失败队列、告警 并发旧值回填删除后缓存再次变旧版本号、互斥重建、延迟校验 从库延迟缓存被旧副本覆盖关键回源走主库或进行版本校验 因此,判断方案是否可靠,不能只问“缓存删没删”,还要问“删除后是否可能被旧数据重新写入”“失败后谁负责补偿”“系统如何证明缓存已经恢复正确”。

这三个问题才是缓存同步闭环的核心。

2. 延迟双删、消息队列和 CDC,应该如何选择?

我在设计缓存同步时看过很多方案:有人推荐延迟双删,有人建议使用消息队列,也有人直接上数据库变更捕获。我不想只按技术流行度选型,想知道这些方案分别解决什么问题,以及什么时候会把系统变得更复杂。

这三种方案并不是同一层面的替代品。延迟双删主要用于降低并发读写造成的旧值回填概率;消息队列负责把变更事件可靠地传播给异步消费者;CDC 则负责从数据库变更日志中捕获事实变化。选型前应先确认问题发生在哪一段链路。

在一个读多写少的普通业务中,我通常先采用缓存旁路模式:数据库提交成功后删除缓存,删除失败进入重试队列,再用定时校验兜底。这样做的优点是写路径简单,缺点是需要自己补足失败重试、幂等和一致性检测。延迟双删适合处理“读请求可能在删除前拿到旧值,并在删除后完成回填”的竞态。

它不是精确算法,延迟时间应参考数据库查询 P99、网络抖动和并发读链路耗时,而不是机械地设置为几百毫秒。延迟过短覆盖不了慢请求,延迟过长又会增加缓存短暂不可用和数据库回源压力。消息队列更适合多个服务或多个缓存副本都需要响应同一数据变化的场景。

但消息队列本身不能自动保证一致性,还必须处理消息发送失败、重复投递、消费乱序、重试堆积和死信。消费者应使用业务 Key 加版本号实现幂等,不能假设每条消息只会到达一次。CDC 适合不希望在大量业务代码中手工维护同步事件的系统,尤其是多个写入口、遗留系统或跨服务同步场景。

它的代价是引入日志解析、位点管理、字段映射和变更格式兼容问题。若只是一个简单的商品详情缓存,直接上 CDC 可能是过度设计。

方案主要解决的问题适合场景不适合单独解决的问题 延迟双删并发旧值回填单体或少量服务、读多写少删除失败、消息丢失、跨系统传播 消息队列异步传播与解耦多服务、多副本同步数据库变更与消息原子关联 CDC捕获数据库事实变更多写入口、遗留系统、数据分发业务语义判断和缓存重建策略 我的判断是:先用最小方案覆盖真实故障,再逐步升级。

只有当写入口变多、同步对象扩展到多个系统,或者人工补偿成本已经超过基础设施成本时,才值得引入消息驱动或 CDC。

3. 如何判断缓存同步闭环真的改善了,而不是只提高了缓存命中率?

我曾经把缓存命中率从 82% 做到 94%,但线上仍然出现用户看到旧价格的投诉。这让我困惑:缓存命中率明明提升了,为什么数据质量没有同步变好?架构评审时到底应该看哪些指标?

缓存命中率只能说明请求是否命中了缓存,不能说明命中的内容是否正确。甚至在缓存长期保存旧值的情况下,命中率越高,错误数据被稳定传播的范围可能越大。因此,缓存同步治理必须把性能指标和一致性指标分开看。

以一组脱敏工程样本为例,某商品服务在调整策略后,缓存命中率从 82% 提升到 94%,数据库读 QPS 下降约 38%,但抽样比对发现缓存与数据库的版本不一致率仍为 0.07%。进一步排查后发现,消息消费延迟并不高,真正的问题是部分读请求从延迟副本读取旧值并回填缓存。我建议至少建立四组指标。

第一组是性能指标,包括缓存命中率、回源比例、数据库 QPS、P95 和 P99 延迟。第二组是同步链路指标,包括删除失败率、事件发送成功率、消费延迟、重试次数和死信数量。第三组是数据质量指标,包括数据库与缓存版本不一致率、关键字段脏读次数、缓存陈旧时间以及修复成功率。

第四组是业务影响指标,例如旧价格曝光次数、错误库存判断次数、受影响用户数和故障平均恢复时间。

指标能回答什么问题不能单独说明什么 缓存命中率请求是否减少了回源数据是否最新 消息消费延迟变更传播是否及时消费结果是否正确 版本不一致率副本是否存在偏差偏差是否已经影响业务 修复平均耗时异常能否快速恢复是否能阻止问题再次发生 闭环是否有效,要看一次故障从发现到恢复的完整路径:系统能否发现异常,能否定位是哪一段失步,能否自动补偿,补偿后能否验证版本一致,复盘后能否改进监控或流程。

只有命中率、延迟和不一致率同时向目标改善,才说明治理真正有效。

4. 库存、余额和权限等高一致性数据,是否还应该使用缓存?

我知道缓存能降低数据库压力,但库存、账户余额和权限状态一旦读旧,可能直接造成业务事故。我想知道,这类数据是不是应该完全禁止缓存,还是可以通过版本校验、短过期和强制回源来安全使用?

高一致性数据不一定要完全禁止缓存,但不能把缓存当作最终事实来源。更准确的原则是:缓存可以负责加速读取和展示,关键决策必须由具备一致性保障的数据源或原子操作完成。例如商品详情中的名称、图片和营销文案通常允许短暂陈旧,可以使用缓存旁路模式;但库存扣减不能只依据缓存中的库存数字做最终判断。

缓存显示还有 10 件,并不代表数据库或库存服务在当前事务中仍然有 10 件可扣。我通常会把数据分成三类。第一类是允许陈旧的数据,例如内容标题、地区名称和非关键统计值,可以使用较长 TTL。第二类是需要较快收敛的数据,例如商品价格和活动状态,应配置主动失效、版本校验和异常回源。

第三类是不能接受错误判断的数据,例如余额、库存扣减结果和权限校验,关键操作必须回到权威服务执行。

数据类型缓存职责关键操作建议 内容与展示信息主要负责加速允许短暂陈旧,依靠 TTL 和异步刷新 价格与活动状态加速读取、降低回源主动失效、版本号校验、异常时快速回源 库存与余额只做辅助查询或预估扣减、支付和结算必须由权威服务校验 权限状态降低重复查询高风险操作重新校验,权限变更主动失效 如果业务确实需要缓存高一致性数据,至少应增加数据版本、短暂有效期、关键请求回源、失效失败告警和定时抽样校验。

版本号比单纯比较更新时间更可靠,因为多个更新请求可能具有相同时间精度,或者消息到达顺序与数据库提交顺序不同。最终的判断标准不是“有没有缓存”,而是“错误缓存是否有机会直接改变业务结果”。如果答案是肯定的,缓存就只能作为性能层,不能作为决策层;如果答案是否定的,才可以用更激进的缓存策略换取吞吐量。

核心关键词

读者评论

高思妍

文章把缓存一致性从“删缓存”扩展到定义、传播、检测、修复和复盘,思路比较完整。尤其是读副本延迟导致旧值回填的案例,对排查线上问题很有参考价值。

徐诗涵

延迟双删和消息队列并不能自动保证最终一致,这个判断比较客观。实际落地时还需要结合业务容忍度、消息幂等、重试和死信机制,不能只依赖单一方案。

夏若溪

文中强调缓存命中率不等于数据正确性,这一点容易被忽略。若能进一步补充版本号设计、校验任务的实现示例,以及不同一致性指标的告警阈值,实操性会更强。

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

扫码咨询方案

热门产品推荐

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

相关内容

查看更多
运营管理平台实践指南:经营分析的进阶玩法怎样更有效

运营管理平台实践指南:经营分析的进阶玩法怎样更有效

运营管理平台实践指南:经营分析的进阶玩法怎样更有效?我先给出一个在实际经营分析项目中反复被验证的结论:平台上线 […]
运营管理平台建设路线:从跨部门协作到进阶玩法分几步

运营管理平台建设路线:从跨部门协作到进阶玩法分几步

运营管理平台建设最容易走偏的地方,是把“买系统”误当成“建平台”。我见过一个同时涉及市场、内容、销售、客服和数 […]
运营管理平台选择标准:异常预警维度如何评估进阶玩法

运营管理平台选择标准:异常预警维度如何评估进阶玩法

运营管理平台选择标准,最容易被忽略的不是“能不能发出预警”,而是“预警发出之后,是否真的改变了业务结果”。我在 […]
运营管理平台优化清单:目标拆解与进阶玩法的关键动作

运营管理平台优化清单:目标拆解与进阶玩法的关键动作

运营管理平台优化最容易走偏的地方,是把“功能上线”误认为“管理升级”。我见过一家拥有十多个业务看板的连锁服务企 […]
运营管理平台场景解析:权限管理中的进阶玩法怎么处理

运营管理平台场景解析:权限管理中的进阶玩法怎么处理

运营管理平台的权限问题,真正棘手的地方通常不是“有没有角色权限”,而是一个已经离职的员工仍能导出客户数据、一个 […]

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

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

让决策更精准