数据库存:技术负责人年度版:缓存同步的完整方法与步骤
缓存同步最容易被低估的地方,是它表面上只有一句“更新数据库后删缓存”,实际上却牵涉事务提交、并发读写、消息投递、缓存重建、故障补偿和数据对账。很多系统并不是因为 Redis 性能不够而出问题,而是因为团队没有定义“谁是事实来源、允许多久不一致、失败后谁负责修复”。我更愿意把缓存同步看成一条需要闭环治理的数据链路,而不是一个简单的缓存 API 调用。
在绝大多数以关系型数据库为核心的业务系统中,数据库应当承担持久化和事实来源的职责,缓存则承担降低读取延迟、削峰和保护数据库的职责。这个角色划分非常重要,因为它决定了缓存丢失之后能不能重建,也决定了缓存中的内容是否允许被当成最终事实。
如果一个系统把缓存当成事实来源,又把数据库当成“备份”,同步问题就会从缓存一致性升级成双主数据冲突。此时无论采用延迟双删、消息队列还是数据库日志订阅,都无法从根本上解决责任边界不清的问题。
我在做缓存方案评审时,通常会先问一个问题:如果现在把整个缓存集群清空,业务数据是否可以依靠数据库、事件日志或其他持久化系统完整恢复?如果答案是否定的,先不要讨论缓存同步方案,而应先修复主数据设计。
“缓存和数据库保持一致”是一句方向正确但不可验收的话。技术负责人需要把它转换成可执行的约束,例如:商品描述允许最多 5 秒旧数据,权限变更要求 1 秒内完成主动失效,库存扣减不能依赖缓存判断可售数量,推荐结果允许分钟级延迟。
不同业务对一致性的容忍度不同。商品详情中的营销文案短暂旧一点,通常只会影响展示;用户权限旧 30 秒,则可能形成越权;库存缓存旧 1 秒,也可能在高并发下放大超卖风险。真正应该被管理的不是“缓存是否绝对一致”,而是不一致的最大持续时间、影响范围和修复成功率。
| 一致性等级 | 典型场景 | 允许的旧数据窗口 | 主要技术要求 | 不应采用的做法 |
|---|---|---|---|---|
| 强一致或近似强一致 | 余额、库存扣减、支付状态 | 通常不允许依赖旧缓存做决策 | 数据库事务、原子校验、版本控制、可靠补偿 | 把缓存值直接当作扣减依据 |
| 短暂不一致可接受 | 商品详情、用户资料、门店信息 | 毫秒级到秒级 | 写库后失效、重试、TTL、并发回填控制 | 只设置 TTL,不处理主动失效失败 |
| 最终一致 | 推荐、统计、搜索索引 | 秒级到分钟级 | 事件传播、幂等消费、积压监控、对账 | 把“发送消息”当成“已经同步完成” |
| 可丢失或可重建 | 热点榜单、临时聚合、首页推荐 | 由重建时间决定 | 限流、预热、降级、回源保护 | 为临时数据设计过度复杂的双写链路 |
上表中的时间窗口不是通用标准,而是技术负责人需要和业务共同确认的验收口径。没有时间边界,就没有告警阈值;没有风险边界,就没有方案取舍。

一套可长期运行的缓存同步方案,至少要包含五个闭环:写入闭环、失效闭环、传播闭环、补偿闭环和验证闭环。写入闭环解决数据库事务是否成功;失效闭环解决缓存是否被删除或更新;传播闭环解决多个服务如何获知变更;补偿闭环解决失败后如何追赶;验证闭环解决团队如何知道系统真的达标。
很多方案只设计了前两个闭环。开发人员写完数据库更新代码,再追加一行删除缓存代码,代码评审就结束了。但线上真正发生故障时,团队还会遇到:删除命令是否执行、Key 是否一致、消息是否丢失、消费是否重复、旧请求是否回填、异常是否报警、是否能定位影响用户。
因此,我通常不会把“缓存删除成功”当成最终结果。它最多只能说明一次操作返回成功,不能证明所有副本、所有节点、本地缓存和下游派生数据都已经完成同步。
商品详情缓存通常读多写少,很多团队会认为它非常简单:后台修改商品描述,服务更新数据库,然后更新 Redis。问题在于,更新缓存意味着应用必须重新构造一份与数据库一致的缓存对象,未来数据库字段变化、序列化格式变化、关联查询变化,都可能让这段双写逻辑逐渐偏离真实查询逻辑。
例如,商品详情不仅包含商品主表字段,还可能包含品牌名称、价格策略、库存展示状态和营销标签。如果写入数据库后直接拼装缓存对象,开发人员很容易遗漏一个关联字段。数据库中的查询结果是新的,缓存中的部分字段却是旧的,这种不一致不会通过 Redis 命令报错,只会在用户页面上表现为“有些字段更新了,有些字段没更新”。
因此,对普通商品详情,我往往更倾向于采用“数据库更新成功后删除缓存”。下一次读取由统一的查询逻辑重新加载完整对象,避免在多个地方维护同一份组装规则。
权限缓存与商品详情的核心差异,不在于 Redis 的数据结构,而在于旧数据的业务后果。一个用户刚刚被撤销管理员权限,如果某个服务节点仍然使用本地缓存或旧的分布式缓存,用户可能在短时间内继续执行高风险操作。
权限缓存不能只依赖一个较短 TTL 来解决。TTL 只能限制最长存活时间,不能保证紧急撤权在业务要求的窗口内完成。更稳妥的设计是:权限变更写库成功后发布失效事件,服务收到事件后删除本地缓存和分布式缓存;对关键接口,再在请求链路上增加版本校验或实时状态校验。
在这种场景里,缓存命中率不是第一优先级。宁可牺牲一部分读取性能,也不能让缓存成为安全控制的唯一依据。
库存展示可以使用缓存,但库存扣减不能简单依赖缓存中的“剩余数量”。缓存读取与真正扣减之间存在天然时间差,多个请求可以同时读到相同的库存值。即使缓存同步做得很快,也不能替代数据库条件更新、库存服务原子扣减或可靠的预扣减机制。
正确的分层方式是:缓存负责展示和部分读压力承接,数据库或库存服务负责扣减事实;交易成功后,再通过事件或失效机制更新展示缓存。库存缓存可以短暂落后,但不能反过来决定交易是否成立。
当系统同时使用浏览器缓存、网关缓存、服务本地缓存和 Redis 时,删除 Redis 并不等于用户马上看到最新数据。本地缓存可能仍保留旧对象,网关可能仍返回旧响应,甚至某些客户端还会继续使用未过期的本地数据。
我在评审多级缓存时,会要求团队画出完整的缓存层级,并为每一层标注:数据写入方式、失效触发方式、最大 TTL、是否支持主动清理、清理失败后的处理方式。如果图中只有 Redis,没有本地缓存和网关缓存,通常说明系统认知还不完整。

下面是一段我在方案复盘中经常使用的示例时间线。它不是某家公司的对外事故数据,而是根据常见并发读写过程构造的情景,用来说明旧值为什么会重新进入缓存。
这个问题的关键不是“删除命令是否成功”,而是旧读请求是否拥有在新写入之后回填缓存的资格。常见处理手段包括延迟二次删除、写入版本校验、回填时比较版本号,以及对热点数据采用互斥重建。但这些手段的可靠程度和工程成本不同,不能只背方案名称。

数据库更新成功只代表主数据已经改变,不代表缓存已删除,也不代表消息已被消费,更不代表所有应用节点的本地缓存已失效。把数据库事务成功作为整个链路成功,会导致监控指标与用户体验脱节。
更合理的状态拆分是:数据库事务状态、事件投递状态、缓存失效状态、缓存重建状态和最终校验状态。技术负责人应当知道每个状态在哪里记录、如何查询,以及状态之间出现断裂时由哪个系统负责推进。
TTL 的作用是限制旧数据最长存活时间,它不是同步机制。一个 TTL 为 60 秒的缓存,理论上可能在 59 秒内持续返回旧值;如果数据在这 60 秒内发生多次变化,用户看到的可能还是多次变更之前的内容。
TTL 适合做最后一道安全网,而不是唯一的失效手段。对于商品详情,TTL 可以降低异常缓存长期残留的风险;对于权限和交易状态,TTL 不能替代主动失效、版本校验和关键接口的实时判断。
延迟双删通常是先删除缓存,等待一段时间后再次删除,用于降低并发旧读回填的影响。它有实际价值,但它依赖等待时间是否覆盖慢查询和请求传播时间。如果慢请求耗时超过等待时间,旧值仍然可能在第二次删除之后回填。
另外,延迟任务本身也可能失败。线程池重启、服务发布、进程异常、任务丢失,都会让第二次删除没有执行。因此,延迟双删必须与任务持久化、重试记录、告警和对账结合,而不是简单使用一个定时线程。
直接更新缓存在某些场景下确实可以减少一次回源,但它要求应用准确构造缓存对象,并处理数据库事务成功而缓存更新失败的情况。如果缓存对象来自多表关联查询,更新缓存的逻辑会逐渐复杂,最终可能出现数据库写入逻辑、缓存写入逻辑和查询组装逻辑三套规则。
对于简单计数器、短结构配置或明确版本的数据,直接更新缓存可以接受;对于复杂详情对象,我更倾向于让数据库负责持久化,缓存负责失效,统一通过读流程重建。
消息队列只能提供事件传播能力,不能自动保证消息不丢、顺序正确、消费成功或数据最终追平。生产端事务提交了但消息没有可靠落盘,消费端收到消息但处理失败,消费者重复处理同一事件,都会让最终一致性停留在口号层面。
如果采用消息驱动同步,至少要回答四个问题:消息怎样和数据库事务绑定,消息是否可重放,消费是否幂等,积压超过业务允许窗口后如何处理。缺少任何一个答案,都不应把方案描述为完整闭环。
缓存命中率只说明请求从缓存中得到了结果,不说明结果是新的,也不说明结果符合业务规则。一个错误地长期保留旧值的缓存,也可能拥有极高命中率。
技术负责人应把命中率与不一致持续时间、主动失效成功率、消息延迟、回源峰值和业务投诉结合起来观察。性能指标和正确性指标必须同时存在,不能用一个指标替代另一个指标。

如果数据库是唯一事实来源,缓存丢失后可以重建,方案设计相对清晰;如果数据库、缓存、搜索引擎和第三方系统都可能修改同一业务字段,就必须先处理多主写入和冲突解决,否则缓存同步只是表面问题。
我建议每一个缓存 Key 都建立一份“数据归属卡”,至少记录事实来源、查询来源、写入方、失效方、重建方式、版本字段和负责人。这个动作看起来像文档工作,却能在故障时显著缩短定位时间。
不要从技术方案倒推一致性等级,而要从业务风险倒推。可以把风险拆成三个维度:旧数据会不会造成资金损失,旧数据会不会造成安全问题,旧数据会不会影响用户决策或运营判断。
如果只是展示文案,秒级延迟往往可以接受;如果是权限状态,延迟窗口必须与安全策略匹配;如果是资金或库存,关键决策应绕开普通缓存读取,直接依赖可验证的原子操作。
读多写少、重建成本低的数据,适合失效后按需重建;写入频繁、读取量大且重建成本高的数据,需要考虑事件驱动更新、异步预热或分片缓存。这里不能只看 QPS,还要看一次回源会触发多少数据库查询、关联多少下游服务。
例如一个商品详情 Key 的重建需要查询 6 张表,并调用 2 个服务,那么大面积失效后的回源成本远高于一个只查询单表的配置 Key。两者都叫“缓存”,但同步和故障恢复策略不能相同。
单体服务、单 Redis 集群和单数据库的同步链路相对短;当系统拆成多个服务,或部署到多个机房后,缓存同步就会变成事件分发问题。此时需要考虑事件顺序、跨地域延迟、网络分区、消费进度和局部恢复。
本地缓存尤其容易被忽略。Redis 删除成功并不代表应用进程内的对象已经被删除。如果本地缓存没有事件订阅、版本检查或较短 TTL,服务节点可能继续向用户返回旧数据。
成熟方案和普通方案的差异,通常不在正常路径,而在失败路径。正常路径下,数据库更新、消息投递和缓存删除都可能成功;真正考验系统的是网络抖动、进程重启、消息积压、消费者异常和数据库主从切换。
我会把“失败可见、失败可重试、失败可回放、结果可验证”作为方案准入条件。只要其中一项无法实现,就要降低一致性承诺,明确告知业务方该方案的边界。

Cache Aside 的基本读流程是先查缓存,命中则直接返回;未命中时查询数据库,把结果写入缓存,再返回给调用方。写流程通常是先更新数据库,成功后删除缓存。
读取流程:
value = cache.get(key)
如果 value 存在,直接返回
如果 value 不存在,从数据库读取
对空结果进行有限时间缓存
将数据库结果写入缓存
返回结果
写入流程:
它的优势是边界清楚:数据库仍然是主数据源,缓存只存放查询结果。它的缺点是删除缓存存在失败窗口,未命中并发回填也可能造成击穿或旧值覆盖。
对于普通详情、用户资料、门店信息等读多写少数据,我通常先选择这个方案,再根据真实故障和指标决定是否引入消息队列、版本控制或异步预热,而不是一开始就搭建复杂的 CDC 链路。
先删除缓存再更新数据库存在一个明显窗口:缓存删除后,新的读请求回源数据库,可能读到旧值并重新写入缓存;随后数据库更新成功,缓存却已经重新出现旧数据。写库后删除缓存,至少可以避免大部分“数据库还没更新就被旧值回填”的问题。
但“写库后删缓存”并不是绝对安全。数据库提交成功后,应用可能在执行删除前崩溃;删除命令也可能因网络、超时或权限异常失败。因此,删除动作需要有可靠记录,而不是只依赖同步代码中的一次调用。
常见的补偿方式包括本地可靠任务表、事务消息、消息队列重试、数据库日志订阅和定时对账。选择哪一种,取决于系统已有基础设施以及业务允许的延迟窗口。
延迟二次删除的思路是:数据库更新成功后立即删除缓存,等待一段时间,再执行一次删除。它主要针对慢查询旧结果回填的问题,而不是解决所有缓存不一致。
等待时间可以从业务请求的实际耗时分布中推导。不要直接复制网络文章中的固定值。更合理的做法是观察数据库查询 P95、P99,叠加服务排队和网络延迟,再进行压测验证。如果慢请求 P99 为 180 毫秒,等待 50 毫秒就可能不足;如果等待过长,又会扩大缓存缺失时间和数据库回源压力。
延迟任务还需要持久化。使用进程内定时器虽然实现简单,但服务重启会丢任务。对重要数据,应把二次删除任务写入可恢复的任务表或消息系统,并记录任务状态、重试次数和最后失败原因。
当多个服务都维护自己的本地缓存,或者一个业务变更需要同时刷新多个派生数据时,事件驱动比每个写入方逐一调用下游接口更容易治理。生产端发布“商品已变更”“权限已撤销”等事件,消费端分别处理自己的缓存。
事件内容不要只放一个模糊的“刷新商品”字符串。至少应考虑业务对象 ID、事件类型、版本号、发生时间、来源服务和幂等键。消费端收到重复事件时,应能够安全地再次删除或判断版本,而不是执行不可逆的重复业务动作。
{
"eventType": "商品详情已变更",
"eventId": "evt-20260916-000123",
"objectId": "product-7812",
"version": 42,
"occurredAt": "2026-09-16T10:20:30Z",
"source": "商品服务"
}
如果事件只用于删除缓存,消费幂等通常比较容易实现;如果事件用于按内容更新缓存,就必须防止乱序事件覆盖新版本。此时应在缓存中保存版本号,只有新版本大于当前版本时才允许写入。
数据库日志订阅的优势是能够观察数据库实际提交的变更,避免每个业务写入方都重复编写缓存同步代码。对于多个系统订阅同一批数据变更、历史写入入口较多的系统,CDC 可以减少遗漏。
它的代价是基础设施复杂度明显提升。团队需要处理全量初始化、增量断点、日志延迟、表结构变化、主从切换、消息顺序和异常回放。CDC 不是“接上就自动同步”,而是把同步逻辑从业务代码转移到了数据基础设施层。
如果一个系统只有一个写入服务、缓存 Key 也不多,直接引入 CDC 可能得不偿失。只有当写入入口多、订阅方多、事件治理需求明确时,CDC 的解耦价值才足以抵消运维成本。
直接更新缓存适合对象结构简单、更新字段明确、缓存内容与数据库记录高度同构的场景。例如简单配置项、短结构状态和部分计数数据,都可能采用数据库成功后更新缓存。
对于复杂聚合对象,我会优先考虑删除后重建。因为缓存对象越复杂,写入缓存的逻辑越容易漏字段;而且数据库事务与缓存更新之间仍然存在失败窗口。直接更新并不会消灭一致性问题,只是把问题从“删除后回源”换成“缓存对象如何正确构造”。
| 方案 | 实现复杂度 | 延迟表现 | 失败恢复能力 | 适用对象 | 主要代价 |
|---|---|---|---|---|---|
| 写库后删缓存 | 低 | 下一次读取需要回源 | 依赖重试和对账 | 普通详情、资料、配置 | 短时缓存未命中 |
| 延迟二次删除 | 中 | 可缩短旧值回填窗口 | 依赖可靠延迟任务 | 并发读写较多的详情 | 等待参数和任务治理 |
| 消息事件同步 | 中高 | 异步传播 | 可重试、可回放 | 多服务、多缓存副本 | 消息积压和幂等设计 |
| CDC 订阅 | 高 | 取决于日志延迟 | 可通过断点恢复 | 多写入入口、多订阅方 | 基础设施和运维成本 |
| 直接更新缓存 | 中 | 通常较快 | 需处理双写失败 | 简单同构对象 | 对象组装逻辑重复 |

年度治理的第一步不是调参数,而是建立缓存资产清单。很多团队能列出 Redis Key,却说不清服务内部是否还有本地 Map、进程缓存、网关缓存、客户端缓存和搜索索引。
建议按业务对象盘点,而不是按技术组件盘点。一个“商品详情”对象可能同时存在数据库记录、Redis 对象、本地缓存、网关响应和搜索索引。只有把这些副本放在同一张图上,才能判断一次变更需要清理哪些地方。
缓存同步失败有时不是技术组件故障,而是两边使用的 Key 根本不一致。一个服务使用 product:7812,另一个服务使用 tenant:3:product:7812,删除操作都返回成功,但删除的是两个不同的对象。
Key 规则应集中管理,避免业务代码到处拼接。对于多租户、分区域、多语言场景,租户和区域必须成为明确维度,不能依赖调用方自行决定是否拼接。
对于可能乱序的异步事件,应把版本信息纳入缓存对象或事件消息。版本号可以来自数据库递增版本、更新时间或业务逻辑序列号,但必须保证比较规则稳定,不能混用不同时间源。
读流程不仅是“查缓存,未命中查数据库”。还要确定空值如何处理、缓存重建是否加锁、数据库异常时是否降级、反序列化失败后是否自动删除坏缓存,以及热点 Key 如何防止大量请求同时回源。
命中后需要校验数据结构版本。缓存对象的序列化格式变化时,旧格式可能无法被新代码正确解析。发现解析失败时,不能只记录日志并继续返回错误,通常应删除坏缓存并按策略回源重建。
未命中后应控制并发回源。可以采用互斥锁、请求合并、单飞机制或短暂预热,具体方式取决于重建耗时和热点程度。锁的等待时间、失败后的降级策略和锁释放方式都必须明确。
数据库查询失败时,不要无条件把异常结果写入缓存。对于非核心展示数据,可以返回短暂降级结果或旧版本;对于权限、订单、库存等关键数据,应优先返回可解释的失败,不要用不确定的缓存值继续做高风险决策。
最容易审查的写流程通常是:先完成数据库事务,再触发缓存失效。这里的“触发”不能等同于“同步执行”。对于重要业务,应把待失效任务写入可靠存储,使应用在删除动作执行前崩溃时,仍然能够被后台任务找回。
一个可落地的任务记录至少包含业务对象、缓存 Key、版本号、创建时间、当前状态、重试次数、最后错误和下一次重试时间。这样,运维人员可以知道哪些 Key 失效失败,业务人员也能知道某次变更是否仍处于追赶过程中。
数据库事务提交成功
|
v
写入缓存失效任务 / 发布可靠事件
v
消费者删除本地缓存与分布式缓存
+—-成功—->记录完成时间与版本
+—-失败—->按退避策略重试
+—-超过阈值—->死信 / 人工处理 / 对账修复
如果应用先提交数据库事务,再发送消息,可能出现数据库已变更但消息发送失败;如果先发送消息,再提交数据库事务,消费者可能在数据库尚未成功时执行缓存处理。两种顺序都存在问题。
常见解决思路是本地消息表或事务消息。应用在同一个数据库事务中写入业务数据和待发送事件,事务提交后由可靠投递任务将事件发送到消息系统。这样即使发送暂时失败,也能依靠任务重试继续推进。
本地消息表的缺点是增加数据库写入和清理成本;事务消息依赖消息系统能力和运维经验;CDC 则依赖数据库日志。三者没有绝对答案,关键是选择团队真正能维护和排障的方式。
删除缓存通常具有天然幂等性,多次删除同一个 Key 不会产生额外业务影响。但“按事件内容更新缓存”并不天然幂等。重复事件可能重复写入,乱序事件可能用旧版本覆盖新版本。
我建议至少使用一种版本判断机制。消费端读取当前缓存版本,只有当新事件版本更大时才执行更新;如果事件只要求删除,则可以直接删除,并在必要时由下一次读取重建最新数据。
幂等键应覆盖业务对象和变更版本,而不是只使用消费者收到消息的时间。时间可能因为机器时钟、网络延迟和重试顺序而失真,业务版本通常更适合用来判断新旧。
补偿任务不能只是一个“失败后再试三次”的脚本。它需要区分临时故障和永久故障,例如 Redis 网络超时可以重试,Key 规则错误则需要告警并进入人工处理。
重试建议使用递增退避,避免缓存集群或消息系统异常时形成重试风暴。超过阈值后进入死信或待处理列表,并携带完整上下文:业务对象、版本、首次失败时间、最近错误、重试次数和影响范围。
对账是最终验证手段。可以根据业务风险采用实时抽样、小时级抽样或每日批量对账。对账不应简单比较序列化字符串,而应比较关键字段、版本号和更新时间,并记录不一致持续时间。
上线前必须验证正常流程和失败流程。正常流程包括数据库更新、缓存命中、缓存未命中和批量变更;失败流程包括缓存不可用、消息积压、消费者重启、数据库主从切换和重复事件。
如果方案不能在测试环境演示“故意让删除失败,然后自动恢复并留下可查询记录”,就不能认为补偿链路已经完成。很多团队只测试主流程,不测试故障恢复,最终把线上用户当成故障演练对象。

假设商品详情接口日均读取量较大,写入频率中等,详情对象由商品主表、品牌表、价格展示表和营销标签组成。此时直接更新缓存会让写入方承担复杂的对象组装责任,任何一个关联字段漏更新,都可能产生半新半旧的缓存。
更稳妥的方案是数据库事务提交后删除商品详情 Key,并把删除任务写入可靠任务表。下一次读取统一走详情查询逻辑,查询成功后重建缓存。对于热门商品,可以在删除后异步预热,避免大量用户同时回源。
在这个场景中,缓存重建的关键不是“越快越好”,而是“重建结果必须来自同一套查询规则”。如果预热脚本使用了另一套 SQL 或接口逻辑,预热本身可能把错误数据重新写入缓存。
假设系统有多个业务服务,每个服务都有本地权限缓存,同时共享一个分布式缓存。用户角色发生变更后,单纯删除分布式缓存并不足够,因为各服务进程中的本地对象仍可能有效。
可以将权限变更抽象成带版本的事件。事件包含用户 ID、权限版本和变更类型,各服务收到后删除本地缓存及分布式缓存。对于高风险接口,再读取用户当前权限版本进行校验,避免本地缓存延迟导致权限绕过。
这里的取舍是牺牲一部分性能换取安全边界。普通页面展示可以使用缓存,敏感操作则应执行更严格的版本或状态检查。不同接口不必使用完全相同的缓存策略。
库存展示可以缓存,但扣减必须依赖可靠的库存事实。一个典型的数据库条件更新是:只有当前库存大于等于扣减数量时才执行扣减,并通过受影响行数判断是否成功。缓存只在交易完成后用于展示结果或承接查询流量。
UPDATE inventory SET available_quantity = available_quantity - :quantity, version = version + 1 WHERE sku_id = :sku_id AND available_quantity >= :quantity;
如果更新影响行数为 0,说明库存不足或版本条件不满足,业务应拒绝扣减,而不是继续相信缓存中的可售数量。缓存同步的目标是尽快让展示接近真实值,不是让缓存承担库存扣减的原子性。
缓存系统至少应该增加“缓存新鲜度”指标。可以在缓存对象中保存数据库版本或更新时间,读取时通过抽样方式与数据库当前版本比较,计算版本落后和持续时间。
下面的数字是情景模拟,用于展示监控方法。假设某商品详情系统连续四周命中率从 92% 提升到 96%,但同步失败率也从 0.08% 上升到 0.31%,说明性能优化可能是以正确性风险为代价换来的。
| 观察周期 | 缓存命中率 | 失效失败率 | 版本落后抽样率 | 平均不一致时长 | 数据库回源峰值 |
|---|---|---|---|---|---|
| 第一周 | 92% | 0.08% | 0.12% | 4.6 秒 | 日常流量的 1.8 倍 |
| 第二周 | 93% | 0.11% | 0.16% | 6.2 秒 | 日常流量的 2.1 倍 |
| 第三周 | 95% | 0.23% | 0.29% | 11.8 秒 | 日常流量的 2.9 倍 |
| 第四周 | 96% | 0.31% | 0.44% | 18.5 秒 | 日常流量的 3.4 倍 |
这个例子说明,命中率提升并不自动代表系统变好。真正值得技术负责人关注的是:版本落后是否增加,不一致是否超过业务窗口,回源峰值是否接近数据库承载上限。

不一致事件总数容易误导。一次只持续 100 毫秒的普通商品缓存失效,和一次持续 20 分钟的权限缓存失效,不能用同样权重计算。更合理的风险评分至少应包含业务等级、影响用户数、不一致持续时间和是否自动恢复。
可以使用一个简单的内部评分模型:风险分值等于业务影响等级乘以影响用户数的分段权重,再乘以持续时间权重。它不需要成为精确数学模型,但能帮助团队把治理资源投入到真正危险的 Key 上。
| 事件类型 | 业务等级 | 持续时间 | 是否自动恢复 | 优先级判断 |
|---|---|---|---|---|
| 普通商品文案旧值 | 低 | 3 秒 | 是 | 记录并观察 |
| 商品价格展示旧值 | 中 | 45 秒 | 部分恢复 | 增加主动失效监控 |
| 用户权限未及时撤销 | 高 | 8 秒 | 否 | 立即整改并进行安全复盘 |
| 库存展示旧值 | 高 | 20 秒 | 是 | 检查是否影响交易判断 |
第一阶段是数据库写入,包括事务成功率、提交延迟和版本变化;第二阶段是事件或任务生成,包括任务创建成功率和待处理数量;第三阶段是缓存处理,包括删除成功率、消费延迟和重试次数;第四阶段是结果验证,包括抽样差异率和不一致持续时间。
如果只监控 Redis 命中率,团队只能知道缓存有没有被访问,无法知道同步链路有没有完成。至少要让一次业务变更能够关联到一个事件 ID 或任务 ID,并沿着日志、指标和告警追踪完整过程。
普通商品详情允许 10 秒不一致,权限变更只允许 1 秒,那么两者不能使用同一个消费延迟告警阈值。告警应当根据业务对象分级,而不是对所有消息主题设定统一的分钟级阈值。
告警还要避免“只报警不处理”。每个高等级告警都应绑定处理动作,例如自动重放、切换备用消费者、暂时关闭缓存写入、强制回源或通知安全负责人。没有处理预案的告警,最终会变成被忽略的噪音。
适合权限、价格和高风险状态。系统不必比较所有数据,可以按用户、商品或租户抽样,比较数据库版本和缓存版本,发现差异后立即触发失效或修复。
适合商品详情、门店资料等数据量较大的场景。每天或每小时扫描业务对象,计算关键字段摘要或版本差异,再对异常 Key 进行删除和重建。
适合消息驱动系统。对比已提交的数据库变更事件、已消费事件和缓存处理结果,寻找处于“数据库已变更但缓存任务未完成”的对象。
对账任务本身也要受到保护。不能在业务高峰期无节制地全表扫描数据库,否则为了验证缓存一致性,反而给主库增加新的压力。建议使用变更时间窗口、版本范围和分片任务,并限制单批次处理量。

建议从 Cache Aside 和写库后删缓存开始。为缓存 Key 统一命名,设置合理 TTL,增加删除失败日志和周期性抽样对账。只有当线上出现明显的并发回填、热点回源或删除失败积累时,再增加延迟二次删除或可靠任务表。
这类系统不必一开始引入复杂的 CDC。技术负责人应先保证主数据清晰、Key 规则统一和故障可追踪,再根据指标判断是否需要升级架构。
建议采用事件驱动同步。数据库写入成功后生成带版本的业务事件,消费者负责删除或更新自己的缓存。事件需要支持重复消费、失败重试、死信和人工重放。
如果多个服务只需要知道“对象变了”,事件内容可以尽量简单,消费者自行回源重建;如果消费者需要精确更新派生数据,事件应包含足够的版本和变更信息,并建立严格的顺序处理规则。
可以评估 CDC 或统一领域事件平台。此时重点不再是某个服务如何删 Key,而是如何保证所有变更入口都能被捕获,所有订阅方都能独立消费,历史事件能够回放。
引入 CDC 前,建议先做一次全量和增量切换演练。尤其要验证:首次全量同步期间产生的增量如何衔接,日志断点如何恢复,表结构变更是否会导致消费者解析失败。
不要让缓存成为最终决策依据。可以缓存用于展示,但真正的授权、扣减和价格确认应回到可靠主数据或具备原子性的业务服务。缓存同步的任务是缩短展示延迟,不能替代业务事务。
对于权限变更,应支持主动失效和紧急全局清理;对于价格,应在下单或支付前再次确认;对于库存,应使用条件更新、锁定或库存服务的原子接口,而不是直接使用缓存数量。
首先明确是“每个地域独立事实”还是“单一中心事实”。如果各地域都可以写入同一对象,就必须定义冲突解决规则;如果只有中心写入,则重点是事件延迟、区域断网后的读取策略和恢复后的追赶。
跨地域同步通常不适合追求任意时刻的绝对一致。更现实的做法是为不同数据定义不同区域策略:核心交易数据走中心服务,普通展示数据允许区域缓存短暂落后,恢复后通过版本和对账追平。
先检查失效策略是否过于集中,例如统一 TTL、批量发布导致大量 Key 同时过期、集群扩容触发迁移,或后台任务一次性清理了整个命名空间。大面积失效的风险通常不在缓存本身,而在回源请求同时打到数据库。
应采用随机 TTL、分批预热、热点 Key 互斥重建、回源限流和降级策略。对可重建数据,可以暂时返回旧版本或静态兜底;对不可使用旧值的数据,则应清晰返回业务不可用,而不是返回未经验证的结果。
写库后删缓存经常被认为“不够高级”,但它的优势是责任链短、故障面少、团队容易理解。对于数据量有限、写入入口单一、允许短暂不一致的系统,简单方案可能比复杂事件平台更可靠。
简单方案的前提是把失败处理补上。没有重试、没有任务记录、没有对账的简单方案确实脆弱;有任务记录、有告警、有对账的简单方案,可能已经满足大部分业务需求。
消息队列和 CDC 能够提高多服务传播和历史恢复能力,但也会带来新的运行面。团队需要维护消息主题、消费者组、重试队列、死信队列、监控面板、回放工具和权限配置。
如果团队没有专人维护这些基础设施,复杂方案可能比直接失效更容易失控。技术负责人需要把运维能力纳入架构评估,而不是只比较理论上的吞吐量和延迟。
| 判断因素 | 更适合删除缓存 | 更适合更新缓存 |
|---|---|---|
| 缓存对象结构 | 多表聚合、组装复杂 | 单表同构、字段简单 |
| 缓存重建成本 | 查询成本可接受 | 重建昂贵且读取压力很高 |
| 写入频率 | 低频更新 | 更新逻辑明确且可控 |
| 一致性风险 | 可接受短暂回源 | 必须快速提供新值且有版本保护 |
| 代码维护 | 更少重复组装逻辑 | 需要维护数据库与缓存双写逻辑 |
缓存不可用时,是继续回源数据库,还是直接降级?这不是纯技术问题。商品推荐可以返回默认内容,权限校验不能默认放行,库存展示可以显示“暂不可用”,订单状态则可能需要查询可靠主数据。
建议为每类缓存定义降级等级:允许返回旧值、允许返回默认值、必须回源、必须失败。这样在缓存集群故障时,服务可以按照业务等级执行,而不是由每个开发人员临时决定。

缓存同步问题会随着业务增长不断变化。一个原本只有单体服务的系统,可能新增本地缓存、搜索服务和跨区域部署;一个原本只缓存商品详情的系统,可能开始缓存价格、库存和权限。年度治理的意义,就是定期重新检查数据归属和同步边界。
建议每季度做一次缓存资产盘点,每月检查失效失败和不一致持续时间,每半年做一次缓存故障和消息积压演练,每年复盘由旧缓存引起的业务事故。治理频率应与业务风险匹配,高风险数据不应等到年度才检查。
收到“页面数据旧了”的反馈后,不要第一时间清理整个 Redis。先确认数据库当前版本、更新时间和业务状态,判断用户看到的旧值是否真的与主数据不一致。有些问题其实是读到了错误租户、错误区域或错误语言版本。
如果数据库也没有新值,问题在写入流程;如果数据库有新值而缓存旧,才进入缓存同步排查。这个分界可以避免团队在错误方向上扩大故障影响。
检查业务对象 ID、租户 ID、区域、语言、版本前缀和命名空间。尤其要注意数字 ID 与字符串 ID 的转换、大小写、序列化格式以及批量删除时的通配符。
生产环境不建议随意使用宽范围通配符删除。一次误删可能让大量请求同时回源,造成数据库压力。更安全的方式是根据对象清单生成精确 Key,并记录删除范围。
如果缓存刚删除就重新出现旧值,重点查看缓存写入日志、查询耗时和请求链路 ID。确认写入缓存的请求何时开始查询数据库、拿到哪个版本、何时完成回填。
如果确认是旧请求回填,可以评估延迟二次删除、版本校验、写入时间门槛或互斥重建。不要只延长 TTL,因为 TTL 越长,错误回填后的影响时间可能越长。
如果采用异步事件,要检查事件是否生成、是否投递、是否消费、是否重试以及是否进入死信。若使用本地缓存,还要检查本地缓存是否订阅了失效事件,或是否因为进程内对象没有清理而继续返回旧值。
排查过程最好有统一关联 ID。数据库变更、失效任务、消息事件和缓存操作都能通过同一个业务对象与版本串联起来,避免工程师只能依靠分散日志猜测。
批量清理适合缓存对象可重建、影响范围明确的场景。对权限、价格和库存等数据,批量修复前要先评估业务后果,必要时暂停相关操作、强制回源或执行版本校验。
修复完成后,不要只看 Redis 中 Key 是否消失,应抽样验证用户实际读取结果、数据库版本与缓存版本,并观察回源峰值和错误率是否恢复正常。

每月应查看缓存命中率、失效失败率、任务积压、消息延迟、死信数量、版本落后率和不一致持续时间。指标需要按照业务对象分组,不要只看整个缓存集群的平均数。
平均数可能掩盖高风险对象。例如所有商品的平均同步延迟是 500 毫秒,但权限缓存的延迟可能达到 8 秒。技术负责人需要看到分业务、分租户、分区域和分缓存层级的差异。
至少模拟缓存集群不可用、消息消费中断、数据库提交成功但事件投递失败、消费者重复消费、消息严重积压和大面积缓存失效。演练目标不是证明系统永远不会出错,而是验证团队能否及时发现、控制影响并恢复。
演练结束后要记录恢复时间、人工步骤、遗漏告警和数据修复范围。如果每次都依赖某位熟悉系统的工程师手工操作,说明流程还没有真正制度化。
业务风险会变化。原本只是展示的字段,可能后来参与价格计算;原本允许分钟级延迟的状态,可能后来被接入自动化审批。年度评审应重新确认哪些数据可以缓存、哪些数据只能展示、哪些数据绝不能依赖缓存做决策。
我建议把缓存策略写进业务接口契约,而不是只写在基础设施文档里。接口调用方必须知道返回数据的更新时间、版本和一致性等级,才能正确处理旧数据或降级结果。

没有用户投诉不代表没有不一致。很多旧值只在特定租户、特定区域、特定时间窗口出现,用户未必会主动反馈。团队需要预先定义目标,例如高风险权限事件 99% 在 1 秒内完成失效,普通详情缓存 99.9% 在 10 秒内完成版本追平,死信任务必须在业务窗口内清零。
这些目标应基于系统基线和业务风险制定,不能直接套用其他公司的数字。重要的是让目标可测量、可告警、可复盘,并能关联到责任人和处理动作。
第一,选择一个最容易出问题或影响最大的缓存对象,确认它的数据库事实来源、Key 规则、写入方、失效方和重建方式。不要一开始就试图盘点全公司的所有缓存,先用一个对象跑通方法。
第二,为这个对象增加版本或更新时间,并在缓存中保留必要的可观测字段。即使暂时不做实时对账,也要让团队能够判断缓存到底落后了多少。
第三,故意制造一次删除失败,验证失败是否可见、是否会重试、是否会告警、是否可以人工重放。这个测试通常比新增一个缓存配置更能暴露系统真实成熟度。
建立按业务等级划分的同步指标和告警;补充抽样对账或周期性对账;完成消息重复、乱序、积压和消费者重启演练;对大面积缓存失效进行回源压力验证。
如果系统已经出现多个服务、多写入入口和多缓存副本,再评估消息事件平台或 CDC。评估时要把建设成本、运维人力、历史数据回放和故障演练一起纳入,而不是只比较同步延迟。
一套缓存同步方案是否合格,可以用五个问题判断:数据库是不是明确的事实来源;一致性窗口是不是可量化;失败是不是可发现;任务是不是可重试和回放;最终结果是不是可通过对账验证。
如果五个问题都有明确答案,即使采用的是“写库后删缓存”,也可能是一套可靠方案;如果五个问题都没有答案,即使使用了消息队列和 CDC,也可能只是把复杂度搬到了别处。
缓存不是事实来源,删除也不是一致性本身。真正的缓存一致性,是一套能解释、能监控、能补偿、能验证的工程闭环。技术负责人下一步不必先重构所有缓存,而应选择一个高风险业务对象,画出它从数据库提交到用户读取的完整路径,标出每一个可能失败的节点,再为这些节点补上记录、重试、告警和对账。


读者评论
文章把缓存同步从“删缓存”提升到事务、消息、补偿和对账的完整链路,尤其是旧请求回填脏数据的案例很有参考价值。不过文中部分方案仍需结合业务延迟目标和系统规模落地。
权限和库存场景的区分比较客观:权限不能只依赖短 TTL,库存缓存也不能直接作为扣减依据。这样的风险分层比单纯罗列延迟双删等方案更实用。
多级缓存部分提醒得很到位,实际排查问题时确实容易只关注 Redis,忽略本地缓存、网关和客户端。若能再补充消息幂等、版本校验的示例代码,落地性会更强。