Redis 缓存一致性全解:从 Cache Aside 到延迟双删、Canal+MQ 的进阶之路

引入 Redis 缓存层后,"数据库更新了缓存却没跟上"就成了绕不开的一致性难题。本文按"问题 → 朴素方案为何失败 → Cache Aside → 先删 vs 后删 → TTL 兜底 → 延迟双删 + MQ 重试 → Canal + MQ → 分布式锁 → 何时放弃缓存"的进阶路线,系统讲透更新缓存与删除缓存的取舍、并发读写场景下的脏数据窗口、ACK 时序、Canal 痛点以及强一致业务的选型逻辑,并配多张流程图辅助理解。


一、引入缓存层后的"原罪":数据一致性问题

在讲一致性之前,先回顾一下 Redis 缓存层在系统里的位置。业务数据最终落在 MySQL 的磁盘上,而 Redis 作为内存数据库挡在 MySQL 之前,承担"挡子弹"的角色------读请求优先查 Redis,命中就直接返回,未命中再查 MySQL 并回写 Redis。

这套流程在读多写少的场景下工作得很好,但只要数据库里的数据被更新,问题就来了:

数据库里的数据变了,Redis 里的缓存还是旧的,怎么办?

这就是缓存一致性问题。它的本质是:同一份数据存在两个副本(数据库一份、缓存一份),更新其中一份时另一份怎么跟着变。这是一个典型的双写问题,只要引入了缓存层就必然存在,区别只在于"如何把不一致的时间窗口压缩到可以接受的范围"。

围绕这个问题,需要做两个决策:

  1. 缓存侧的操作选择 :更新数据库时,对缓存是更新 还是删除
  2. 操作顺序:先操作缓存还是先操作数据库?

下面按"正常思维"逐步推演,看看每种选择会带来什么问题,最终为什么会演变成 Cache Aside、延迟双删、Canal+MQ 这一整套方案。


二、朴素方案:更新缓存------为什么必然失败

按正常思维,更新数据库之后直觉就是"把缓存也更新一下",让两者保持一致。那么剩下的问题就是:先更新缓存还是先更新数据库?分别分析。

2.1 先更新缓存再更新数据库

假设两个写请求 A 和 B 几乎同时到达,期望的最终结果是后到的请求胜出。来看实际执行过程:
写请求B MySQL数据库 Redis缓存 写请求A 写请求B MySQL数据库 Redis缓存 写请求A #mermaid-svg-zTFpZdZO2cnSPY6M{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-zTFpZdZO2cnSPY6M .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-zTFpZdZO2cnSPY6M .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-zTFpZdZO2cnSPY6M .error-icon{fill:#552222;}#mermaid-svg-zTFpZdZO2cnSPY6M .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-zTFpZdZO2cnSPY6M .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-zTFpZdZO2cnSPY6M .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-zTFpZdZO2cnSPY6M .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-zTFpZdZO2cnSPY6M .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-zTFpZdZO2cnSPY6M .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-zTFpZdZO2cnSPY6M .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-zTFpZdZO2cnSPY6M .marker{fill:#333333;stroke:#333333;}#mermaid-svg-zTFpZdZO2cnSPY6M .marker.cross{stroke:#333333;}#mermaid-svg-zTFpZdZO2cnSPY6M svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-zTFpZdZO2cnSPY6M p{margin:0;}#mermaid-svg-zTFpZdZO2cnSPY6M .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-zTFpZdZO2cnSPY6M text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-zTFpZdZO2cnSPY6M .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-zTFpZdZO2cnSPY6M .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-zTFpZdZO2cnSPY6M .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-zTFpZdZO2cnSPY6M .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-zTFpZdZO2cnSPY6M #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-zTFpZdZO2cnSPY6M .sequenceNumber{fill:white;}#mermaid-svg-zTFpZdZO2cnSPY6M #sequencenumber{fill:#333;}#mermaid-svg-zTFpZdZO2cnSPY6M #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-zTFpZdZO2cnSPY6M .messageText{fill:#333;stroke:none;}#mermaid-svg-zTFpZdZO2cnSPY6M .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-zTFpZdZO2cnSPY6M .labelText,#mermaid-svg-zTFpZdZO2cnSPY6M .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-zTFpZdZO2cnSPY6M .loopText,#mermaid-svg-zTFpZdZO2cnSPY6M .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-zTFpZdZO2cnSPY6M .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-zTFpZdZO2cnSPY6M .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-zTFpZdZO2cnSPY6M .noteText,#mermaid-svg-zTFpZdZO2cnSPY6M .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-zTFpZdZO2cnSPY6M .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-zTFpZdZO2cnSPY6M .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-zTFpZdZO2cnSPY6M .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-zTFpZdZO2cnSPY6M .actorPopupMenu{position:absolute;}#mermaid-svg-zTFpZdZO2cnSPY6M .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-zTFpZdZO2cnSPY6M .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-zTFpZdZO2cnSPY6M .actor-man circle,#mermaid-svg-zTFpZdZO2cnSPY6M line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-zTFpZdZO2cnSPY6M :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 线程调度切换到请求B 线程调度切回请求A 缓存是B的值 数据库是A的值 → 不一致! 1. 更新缓存为A的值 2. 更新缓存为B的值 3. 更新数据库为B的值 4. 更新数据库为A的值

按时间线一步步推演:

  • 第 1 步:请求 A 先到达,把缓存更新为自己的值。此时缓存里是 A 的值。
  • 第 2 步:请求 A 准备去更新数据库,但由于线程调度(CPU 时间片切换、其他线程抢占等),CPU 切到了请求 B 的线程上。请求 B 也来更新缓存,把缓存覆盖成了 B 的值。
  • 第 3 步:请求 B 继续往下走,把数据库也更新成了 B 的值。请求 B 完整执行完毕。
  • 第 4 步:CPU 又切回了请求 A 的线程,请求 A 继续从中断处往下执行,把数据库更新成 A 的值。请求 A 完整执行完毕。

最终结果是:

  • 缓存:B 的值(被第 2 步覆盖,之后没人再动)
  • 数据库:A 的值(被第 4 步最后写入,是最新值)

数据不一致。这种不一致和线程调度、CPU 时间片分配有关,发生概率不算高,但理论上无法消除。只要两个写请求并发到达,就有可能出现这种"缓存和数据库归属不同请求"的情况

2.2 先更新数据库再更新缓存

把顺序反过来,先更新数据库,再更新缓存。同样两个写请求 A 和 B 并发:
写请求B Redis缓存 MySQL数据库 写请求A 写请求B Redis缓存 MySQL数据库 写请求A #mermaid-svg-umg3hRBrOYMjXntt{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-umg3hRBrOYMjXntt .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-umg3hRBrOYMjXntt .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-umg3hRBrOYMjXntt .error-icon{fill:#552222;}#mermaid-svg-umg3hRBrOYMjXntt .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-umg3hRBrOYMjXntt .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-umg3hRBrOYMjXntt .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-umg3hRBrOYMjXntt .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-umg3hRBrOYMjXntt .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-umg3hRBrOYMjXntt .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-umg3hRBrOYMjXntt .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-umg3hRBrOYMjXntt .marker{fill:#333333;stroke:#333333;}#mermaid-svg-umg3hRBrOYMjXntt .marker.cross{stroke:#333333;}#mermaid-svg-umg3hRBrOYMjXntt svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-umg3hRBrOYMjXntt p{margin:0;}#mermaid-svg-umg3hRBrOYMjXntt .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-umg3hRBrOYMjXntt text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-umg3hRBrOYMjXntt .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-umg3hRBrOYMjXntt .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-umg3hRBrOYMjXntt .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-umg3hRBrOYMjXntt .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-umg3hRBrOYMjXntt #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-umg3hRBrOYMjXntt .sequenceNumber{fill:white;}#mermaid-svg-umg3hRBrOYMjXntt #sequencenumber{fill:#333;}#mermaid-svg-umg3hRBrOYMjXntt #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-umg3hRBrOYMjXntt .messageText{fill:#333;stroke:none;}#mermaid-svg-umg3hRBrOYMjXntt .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-umg3hRBrOYMjXntt .labelText,#mermaid-svg-umg3hRBrOYMjXntt .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-umg3hRBrOYMjXntt .loopText,#mermaid-svg-umg3hRBrOYMjXntt .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-umg3hRBrOYMjXntt .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-umg3hRBrOYMjXntt .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-umg3hRBrOYMjXntt .noteText,#mermaid-svg-umg3hRBrOYMjXntt .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-umg3hRBrOYMjXntt .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-umg3hRBrOYMjXntt .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-umg3hRBrOYMjXntt .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-umg3hRBrOYMjXntt .actorPopupMenu{position:absolute;}#mermaid-svg-umg3hRBrOYMjXntt .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-umg3hRBrOYMjXntt .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-umg3hRBrOYMjXntt .actor-man circle,#mermaid-svg-umg3hRBrOYMjXntt line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-umg3hRBrOYMjXntt :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 请求A还未更新缓存 线程调度切换到请求B 线程调度切回请求A 数据库是B的值 缓存是A的值 → 不一致! 1. 更新数据库为A的值 2. 更新数据库为B的值 3. 更新缓存为B的值 4. 更新缓存为A的值

按时间线推演:

  • 第 1 步:请求 A 先到达,把数据库更新为自己的值。此时数据库里是 A 的值。
  • 第 2 步:请求 A 准备去更新缓存,但还没来得及执行,CPU 调度切到了请求 B 的线程。请求 B 也来更新数据库,把数据库覆盖成 B 的值。
  • 第 3 步:请求 B 继续执行,把缓存也更新成 B 的值。请求 B 完整执行完毕。
  • 第 4 步:CPU 切回请求 A,请求 A 从中断处继续执行,把缓存更新成 A 的值。请求 A 完整执行完毕。

最终结果:

  • 数据库:B 的值(被第 2 步覆盖)
  • 缓存:A 的值(被第 4 步最后写入)

注意请求 A 是先到的,按"后写覆盖前写"的语义,本来应该最终结果是 B 的值,但缓存却被 A 覆盖了,这就是经典的写覆盖问题------后到的请求 B 反而没能最终胜出,被先到的请求 A 在缓存里"反杀"了。

2.3 结论:更新缓存策略无法解决并发问题

两种顺序都分析了,结果都一样:只要是更新缓存的操作,无论是哪种操作顺序,都必然存在并发场景下的数据不一致,而且这种不一致无法通过加锁或重试来解决------因为你根本不知道哪个请求"应该"是最终结果。

所以更新缓存这个思路从根上就被否掉了。需要换一种策略:删除缓存


三、Cache Aside 旁路缓存策略

3.1 核心思想:删除而非更新

更新缓存行不通,那就改用删除:更新数据库时,不更新缓存,而是把缓存中的数据删掉。下次读请求来时发现缓存没了,自然就去查数据库并重新加载到缓存里。

策略的英文名是 Cache Aside(旁路缓存策略),是业界最广泛使用的缓存一致性方案。它的核心思想可以拆成三块来理解:

3.1.1 懒加载机制:用到才加载

Cache Aside 的核心思想之一是懒加载------用到的时候再加载数据,避免不必要的资源浪费。

为什么懒加载能节省开销?举个具体例子:

假设某个 key 在一段时间内被写请求更新了 100 次,但期间没有任何读请求来查它。

  • 如果用更新策略 :每次写请求都要更新一次缓存,100 次写就更新了 100 次缓存。但前 99 次更新的缓存根本没有人读过,完全是白干的。只有第 100 次更新后的缓存才是有价值的。
  • 如果用删除策略:每次写请求只删一次缓存,100 次写就删了 100 次缓存(删除是轻量操作)。读请求来时,发现缓存没了,再去查数据库加载一次。整个过程中只加载了 1 次缓存(读请求触发的那次),但 100 次写操作期间没有浪费任何"更新缓存"的资源。

所以删除策略天然适配懒加载------数据没人读就不重建,有人读才重建。这种策略特别适合访问频率没那么高的数据:写多读少的场景下,每次写都更新缓存就是浪费,删除则把重建成本推迟到真正需要读的时候。

3.1.2 幂等性:删除能解决并发问题

为什么"删除"能解决"更新"的并发问题?因为删除是幂等操作------多次删除同一份数据,结果都一样。

无论多少个写请求并发到达,对缓存的操作都是"删",不会出现 A 把缓存覆盖成 A 的值、B 覆盖成 B 的值这种写覆盖问题。删除过的数据再删一次,结果还是"不存在",所以并发删除不会产生冲突

这就是 Cache Aside 能解决第二节那个并发问题的根本原因------把"更新"换成"删除",并发写请求之间就不再有竞争关系了。

3.1.3 读写分离:读时回填,写时删除

Cache Aside 的完整定义是:

  • 读请求 :先查缓存,命中直接返回;未命中查数据库,把数据回填到缓存,再返回。
  • 写请求 :更新数据库,然后删除缓存(不更新)。

注意一个细节------读请求未命中后要回填缓存,这一点在更新缓存方案下是没有的。正是这个"回填"动作,让 Cache Aside 的读写策略之间产生了新的并发交互,这也是第四节要重点讨论的内容。

3.2 读策略

到这一步可以先停下来想一个问题:既然 Cache Aside 把"更新缓存"改成了"删除缓存",那为什么还要单独把读策略和写策略拆开分析?

答案是:因为读请求未命中后要回填缓存,这个回填动作会跟写请求产生并发交互。所以分析 Cache Aside 的并发问题,不能只看写请求,必须把读请求也一起纳入考虑,分别看读策略和写策略在做什么、它们之间在什么时刻会产生交集。这就是为什么下面要分别列读策略和写策略。

Cache Aside 的读策略很直观:
#mermaid-svg-fl5EY8CyJrgfJp27{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-fl5EY8CyJrgfJp27 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-fl5EY8CyJrgfJp27 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-fl5EY8CyJrgfJp27 .error-icon{fill:#552222;}#mermaid-svg-fl5EY8CyJrgfJp27 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-fl5EY8CyJrgfJp27 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-fl5EY8CyJrgfJp27 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-fl5EY8CyJrgfJp27 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-fl5EY8CyJrgfJp27 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-fl5EY8CyJrgfJp27 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-fl5EY8CyJrgfJp27 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-fl5EY8CyJrgfJp27 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-fl5EY8CyJrgfJp27 .marker.cross{stroke:#333333;}#mermaid-svg-fl5EY8CyJrgfJp27 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-fl5EY8CyJrgfJp27 p{margin:0;}#mermaid-svg-fl5EY8CyJrgfJp27 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-fl5EY8CyJrgfJp27 .cluster-label text{fill:#333;}#mermaid-svg-fl5EY8CyJrgfJp27 .cluster-label span{color:#333;}#mermaid-svg-fl5EY8CyJrgfJp27 .cluster-label span p{background-color:transparent;}#mermaid-svg-fl5EY8CyJrgfJp27 .label text,#mermaid-svg-fl5EY8CyJrgfJp27 span{fill:#333;color:#333;}#mermaid-svg-fl5EY8CyJrgfJp27 .node rect,#mermaid-svg-fl5EY8CyJrgfJp27 .node circle,#mermaid-svg-fl5EY8CyJrgfJp27 .node ellipse,#mermaid-svg-fl5EY8CyJrgfJp27 .node polygon,#mermaid-svg-fl5EY8CyJrgfJp27 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-fl5EY8CyJrgfJp27 .rough-node .label text,#mermaid-svg-fl5EY8CyJrgfJp27 .node .label text,#mermaid-svg-fl5EY8CyJrgfJp27 .image-shape .label,#mermaid-svg-fl5EY8CyJrgfJp27 .icon-shape .label{text-anchor:middle;}#mermaid-svg-fl5EY8CyJrgfJp27 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-fl5EY8CyJrgfJp27 .rough-node .label,#mermaid-svg-fl5EY8CyJrgfJp27 .node .label,#mermaid-svg-fl5EY8CyJrgfJp27 .image-shape .label,#mermaid-svg-fl5EY8CyJrgfJp27 .icon-shape .label{text-align:center;}#mermaid-svg-fl5EY8CyJrgfJp27 .node.clickable{cursor:pointer;}#mermaid-svg-fl5EY8CyJrgfJp27 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-fl5EY8CyJrgfJp27 .arrowheadPath{fill:#333333;}#mermaid-svg-fl5EY8CyJrgfJp27 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-fl5EY8CyJrgfJp27 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-fl5EY8CyJrgfJp27 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-fl5EY8CyJrgfJp27 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-fl5EY8CyJrgfJp27 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-fl5EY8CyJrgfJp27 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-fl5EY8CyJrgfJp27 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-fl5EY8CyJrgfJp27 .cluster text{fill:#333;}#mermaid-svg-fl5EY8CyJrgfJp27 .cluster span{color:#333;}#mermaid-svg-fl5EY8CyJrgfJp27 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-fl5EY8CyJrgfJp27 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-fl5EY8CyJrgfJp27 rect.text{fill:none;stroke-width:0;}#mermaid-svg-fl5EY8CyJrgfJp27 .icon-shape,#mermaid-svg-fl5EY8CyJrgfJp27 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-fl5EY8CyJrgfJp27 .icon-shape p,#mermaid-svg-fl5EY8CyJrgfJp27 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-fl5EY8CyJrgfJp27 .icon-shape .label rect,#mermaid-svg-fl5EY8CyJrgfJp27 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-fl5EY8CyJrgfJp27 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-fl5EY8CyJrgfJp27 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-fl5EY8CyJrgfJp27 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 命中
未命中
读请求到达
查询 Redis 缓存
直接返回缓存数据
查询 MySQL 数据库
回写到 Redis 缓存
返回数据给用户
结束

流程说明:

  • 第 1 步:读请求到达,先去 Redis 查缓存。
  • 第 2 步 :判断缓存是否命中------
    • 命中分支:直接返回缓存数据,请求在 Redis 这一层就被消化掉,根本不会去 MySQL。
    • 未命中分支:继续往下走,去查 MySQL 数据库。
  • 第 3 步(未命中时) :从 MySQL 拿到数据后,回写到 Redis(这一步是关键,方便下次请求直接命中缓存)。
  • 第 4 步:返回数据给用户。

读策略中真正"危险"的是第 3 步的回填动作------它会把数据写到缓存里。如果在这个回填的时间窗口内有写请求改了数据库,回填的就是脏数据。这就是第四节要重点分析的场景。

3.3 写策略

写策略也简单:
#mermaid-svg-j0Pr05JEWEYJzVO4{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-j0Pr05JEWEYJzVO4 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-j0Pr05JEWEYJzVO4 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-j0Pr05JEWEYJzVO4 .error-icon{fill:#552222;}#mermaid-svg-j0Pr05JEWEYJzVO4 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-j0Pr05JEWEYJzVO4 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-j0Pr05JEWEYJzVO4 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-j0Pr05JEWEYJzVO4 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-j0Pr05JEWEYJzVO4 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-j0Pr05JEWEYJzVO4 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-j0Pr05JEWEYJzVO4 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-j0Pr05JEWEYJzVO4 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-j0Pr05JEWEYJzVO4 .marker.cross{stroke:#333333;}#mermaid-svg-j0Pr05JEWEYJzVO4 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-j0Pr05JEWEYJzVO4 p{margin:0;}#mermaid-svg-j0Pr05JEWEYJzVO4 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-j0Pr05JEWEYJzVO4 .cluster-label text{fill:#333;}#mermaid-svg-j0Pr05JEWEYJzVO4 .cluster-label span{color:#333;}#mermaid-svg-j0Pr05JEWEYJzVO4 .cluster-label span p{background-color:transparent;}#mermaid-svg-j0Pr05JEWEYJzVO4 .label text,#mermaid-svg-j0Pr05JEWEYJzVO4 span{fill:#333;color:#333;}#mermaid-svg-j0Pr05JEWEYJzVO4 .node rect,#mermaid-svg-j0Pr05JEWEYJzVO4 .node circle,#mermaid-svg-j0Pr05JEWEYJzVO4 .node ellipse,#mermaid-svg-j0Pr05JEWEYJzVO4 .node polygon,#mermaid-svg-j0Pr05JEWEYJzVO4 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-j0Pr05JEWEYJzVO4 .rough-node .label text,#mermaid-svg-j0Pr05JEWEYJzVO4 .node .label text,#mermaid-svg-j0Pr05JEWEYJzVO4 .image-shape .label,#mermaid-svg-j0Pr05JEWEYJzVO4 .icon-shape .label{text-anchor:middle;}#mermaid-svg-j0Pr05JEWEYJzVO4 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-j0Pr05JEWEYJzVO4 .rough-node .label,#mermaid-svg-j0Pr05JEWEYJzVO4 .node .label,#mermaid-svg-j0Pr05JEWEYJzVO4 .image-shape .label,#mermaid-svg-j0Pr05JEWEYJzVO4 .icon-shape .label{text-align:center;}#mermaid-svg-j0Pr05JEWEYJzVO4 .node.clickable{cursor:pointer;}#mermaid-svg-j0Pr05JEWEYJzVO4 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-j0Pr05JEWEYJzVO4 .arrowheadPath{fill:#333333;}#mermaid-svg-j0Pr05JEWEYJzVO4 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-j0Pr05JEWEYJzVO4 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-j0Pr05JEWEYJzVO4 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-j0Pr05JEWEYJzVO4 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-j0Pr05JEWEYJzVO4 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-j0Pr05JEWEYJzVO4 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-j0Pr05JEWEYJzVO4 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-j0Pr05JEWEYJzVO4 .cluster text{fill:#333;}#mermaid-svg-j0Pr05JEWEYJzVO4 .cluster span{color:#333;}#mermaid-svg-j0Pr05JEWEYJzVO4 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-j0Pr05JEWEYJzVO4 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-j0Pr05JEWEYJzVO4 rect.text{fill:none;stroke-width:0;}#mermaid-svg-j0Pr05JEWEYJzVO4 .icon-shape,#mermaid-svg-j0Pr05JEWEYJzVO4 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-j0Pr05JEWEYJzVO4 .icon-shape p,#mermaid-svg-j0Pr05JEWEYJzVO4 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-j0Pr05JEWEYJzVO4 .icon-shape .label rect,#mermaid-svg-j0Pr05JEWEYJzVO4 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-j0Pr05JEWEYJzVO4 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-j0Pr05JEWEYJzVO4 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-j0Pr05JEWEYJzVO4 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 选项1: 删除前
选项2: 删除后
写请求到达
更新 MySQL 数据库
对缓存做什么操作
先删除缓存

再更新数据库
先更新数据库

再删除缓存
结束
结束

流程说明:

  • 第 1 步:写请求到达,更新 MySQL 数据库。
  • 第 2 步 :决定对缓存做什么------前面已经否掉了"更新缓存",这里只剩"删除缓存"。但删除的时机又分两种:
    • 选项 1:先删除缓存再更新数据库------写请求一开始就把缓存删了,再去更新数据库。
    • 选项 2:先更新数据库再删除缓存------写请求先更新数据库,更新完之后再删缓存。

这里有个对比要重点提一下:以前"更新缓存"方案下,读请求是不会有并发问题的------读请求未命中缓存就直接查数据库返回,不回填 ,对缓存没有任何操作。但 Cache Aside 不一样,读请求未命中后要回填缓存,所以读策略和写策略之间产生了并发交互。

接下来的问题就是:删除缓存放在更新数据库前面还是后面?这就是 Cache Aside 真正的难点,也是第四节要展开的核心内容。


四、Cache Aside 的并发问题:先删 vs 后删

4.1 方案一:先删除缓存再更新数据库

到这一步可以先想一下:如果把删除缓存放在更新数据库之前,会出什么问题?

直觉上好像没问题------缓存先删了,等数据库更新完,下次读请求来时缓存未命中,去查数据库就能拿到新值。但一旦有读请求夹在中间到达,问题就来了。来看具体场景:一个写请求 B 加一个读请求 A 同时进来,假设写请求 B 先执行:
读请求A MySQL数据库 Redis缓存 写请求B 读请求A MySQL数据库 Redis缓存 写请求B #mermaid-svg-N1E6RxEL8TRvfmZK{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-N1E6RxEL8TRvfmZK .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-N1E6RxEL8TRvfmZK .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-N1E6RxEL8TRvfmZK .error-icon{fill:#552222;}#mermaid-svg-N1E6RxEL8TRvfmZK .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-N1E6RxEL8TRvfmZK .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-N1E6RxEL8TRvfmZK .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-N1E6RxEL8TRvfmZK .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-N1E6RxEL8TRvfmZK .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-N1E6RxEL8TRvfmZK .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-N1E6RxEL8TRvfmZK .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-N1E6RxEL8TRvfmZK .marker{fill:#333333;stroke:#333333;}#mermaid-svg-N1E6RxEL8TRvfmZK .marker.cross{stroke:#333333;}#mermaid-svg-N1E6RxEL8TRvfmZK svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-N1E6RxEL8TRvfmZK p{margin:0;}#mermaid-svg-N1E6RxEL8TRvfmZK .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-N1E6RxEL8TRvfmZK text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-N1E6RxEL8TRvfmZK .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-N1E6RxEL8TRvfmZK .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-N1E6RxEL8TRvfmZK .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-N1E6RxEL8TRvfmZK .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-N1E6RxEL8TRvfmZK #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-N1E6RxEL8TRvfmZK .sequenceNumber{fill:white;}#mermaid-svg-N1E6RxEL8TRvfmZK #sequencenumber{fill:#333;}#mermaid-svg-N1E6RxEL8TRvfmZK #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-N1E6RxEL8TRvfmZK .messageText{fill:#333;stroke:none;}#mermaid-svg-N1E6RxEL8TRvfmZK .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-N1E6RxEL8TRvfmZK .labelText,#mermaid-svg-N1E6RxEL8TRvfmZK .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-N1E6RxEL8TRvfmZK .loopText,#mermaid-svg-N1E6RxEL8TRvfmZK .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-N1E6RxEL8TRvfmZK .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-N1E6RxEL8TRvfmZK .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-N1E6RxEL8TRvfmZK .noteText,#mermaid-svg-N1E6RxEL8TRvfmZK .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-N1E6RxEL8TRvfmZK .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-N1E6RxEL8TRvfmZK .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-N1E6RxEL8TRvfmZK .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-N1E6RxEL8TRvfmZK .actorPopupMenu{position:absolute;}#mermaid-svg-N1E6RxEL8TRvfmZK .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-N1E6RxEL8TRvfmZK .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-N1E6RxEL8TRvfmZK .actor-man circle,#mermaid-svg-N1E6RxEL8TRvfmZK line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-N1E6RxEL8TRvfmZK :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 数据库尚未更新完成 此时数据库才更新完成 缓存是旧数据 数据库是新数据 → 不一致! 1. 删除缓存 2. 开始更新数据库(进行中) 3. 查询缓存 → 未命中 4. 查询数据库 → 读到旧数据 5. 回写旧数据到缓存

按时间线推演:

  • 第 1 步 :写请求 B 先到达,立刻把缓存删了。问题从这里开始------缓存空了,但数据库还没更新。
  • 第 2 步:写请求 B 开始更新数据库,但这个操作耗时较长(要走磁盘 IO、加锁、事务提交),还没完成。
  • 第 3 步:就在数据库更新的这段时间里,读请求 A 进来了。查询缓存发现未命中(被 B 删了)。
  • 第 4 步 :读请求 A 只能去查数据库。注意此时数据库还没更新完 ,读请求 A 读到的是旧数据
  • 第 5 步:读请求 A 按读策略的规则,把读到的旧数据回填到缓存里。
  • 第 6 步:写请求 B 终于把数据库更新完。但此时缓存已经被回填成旧数据了,写请求 B 不会再去清一次。

最终:缓存是旧数据,数据库是新数据,不一致。而且这个不一致会一直持续到 TTL 过期或下次写操作,影响时间很长。

这种方案的问题在于:删除缓存后到数据库更新完成之间有一个"脏数据窗口"------这个窗口的大小取决于数据库更新的耗时(通常毫秒到秒级),任何在这个窗口内到达的读请求都会把旧数据回填到缓存里。窗口越大,命中概率越高。

4.2 方案二:先更新数据库再删除缓存(推荐)

把顺序反过来,先更新数据库,再删除缓存。前面方案一的脏数据窗口是因为"缓存被删了、数据库还没更新完",方案二把数据库更新挪到删除缓存之前,看起来好像能避开这个问题,但实际上还是会有一类极小概率的不一致场景。来看具体推演:
写请求B MySQL数据库 Redis缓存 读请求A 写请求B MySQL数据库 Redis缓存 读请求A #mermaid-svg-EU8p2vxCWfMQfDp8{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-EU8p2vxCWfMQfDp8 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-EU8p2vxCWfMQfDp8 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-EU8p2vxCWfMQfDp8 .error-icon{fill:#552222;}#mermaid-svg-EU8p2vxCWfMQfDp8 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-EU8p2vxCWfMQfDp8 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-EU8p2vxCWfMQfDp8 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-EU8p2vxCWfMQfDp8 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-EU8p2vxCWfMQfDp8 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-EU8p2vxCWfMQfDp8 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-EU8p2vxCWfMQfDp8 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-EU8p2vxCWfMQfDp8 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-EU8p2vxCWfMQfDp8 .marker.cross{stroke:#333333;}#mermaid-svg-EU8p2vxCWfMQfDp8 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-EU8p2vxCWfMQfDp8 p{margin:0;}#mermaid-svg-EU8p2vxCWfMQfDp8 .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-EU8p2vxCWfMQfDp8 text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-EU8p2vxCWfMQfDp8 .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-EU8p2vxCWfMQfDp8 .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-EU8p2vxCWfMQfDp8 .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-EU8p2vxCWfMQfDp8 .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-EU8p2vxCWfMQfDp8 #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-EU8p2vxCWfMQfDp8 .sequenceNumber{fill:white;}#mermaid-svg-EU8p2vxCWfMQfDp8 #sequencenumber{fill:#333;}#mermaid-svg-EU8p2vxCWfMQfDp8 #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-EU8p2vxCWfMQfDp8 .messageText{fill:#333;stroke:none;}#mermaid-svg-EU8p2vxCWfMQfDp8 .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-EU8p2vxCWfMQfDp8 .labelText,#mermaid-svg-EU8p2vxCWfMQfDp8 .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-EU8p2vxCWfMQfDp8 .loopText,#mermaid-svg-EU8p2vxCWfMQfDp8 .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-EU8p2vxCWfMQfDp8 .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-EU8p2vxCWfMQfDp8 .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-EU8p2vxCWfMQfDp8 .noteText,#mermaid-svg-EU8p2vxCWfMQfDp8 .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-EU8p2vxCWfMQfDp8 .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-EU8p2vxCWfMQfDp8 .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-EU8p2vxCWfMQfDp8 .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-EU8p2vxCWfMQfDp8 .actorPopupMenu{position:absolute;}#mermaid-svg-EU8p2vxCWfMQfDp8 .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-EU8p2vxCWfMQfDp8 .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-EU8p2vxCWfMQfDp8 .actor-man circle,#mermaid-svg-EU8p2vxCWfMQfDp8 line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-EU8p2vxCWfMQfDp8 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 此时线程调度切换到请求B 线程调度切回请求A 缓存是旧数据 数据库是新数据 → 不一致! 1. 查询缓存 → 未命中 2. 查询数据库(读到的可能是旧数据) 3. 更新数据库为新值 4. 删除缓存 5. 把读到的旧数据回填到缓存

按时间线一步步推演:

  • 第 1 步:读请求 A 到达,查询缓存未命中(可能是缓存本来就没数据,或者刚被上一个写请求删过)。
  • 第 2 步 :读请求 A 去查 MySQL 数据库。注意此时数据库还是旧数据(写请求 B 还没来),读请求 A 拿到的也是旧数据。关键点:读请求 A 此刻手里攥着旧数据,但还没回填缓存。
  • 第 3 步:就在读请求 A 查完数据库还没回填的这个时间窗口里,CPU 调度切到了写请求 B 的线程。写请求 B 把数据库更新成新值。
  • 第 4 步:写请求 B 继续执行,删除缓存(注意此时缓存本来就是空的,删了等于没删,但 B 的语义是"删完就完事")。
  • 第 5 步:CPU 切回读请求 A,请求 A 把第 2 步查到的旧数据回填到缓存。

最终结果:

  • 数据库:B 的新值
  • 缓存:A 回填的旧数据

理论上也存在不一致。但和方案一不一样的是,这个场景发生概率极低,原因有两个:

  1. 缓存写入速度远高于数据库:数据库的更新操作要走磁盘 IO、加锁、事务提交,耗时通常是毫秒级;而 Redis 的删除操作是内存操作,耗时微秒级。读请求 A 要恰好卡在"查完数据库、还没回填"这个极短的时间窗口内被切走线程,概率非常小。
  2. 需要恰好在这个窗口内发生线程调度:CPU 时间片切换到写请求 B 上,且写请求 B 要完整地完成"更新数据库 + 删除缓存"两步操作后切回 A,这种巧合在实际生产中很少触发。

方案一中脏数据窗口是毫秒级甚至秒级(数据库更新耗时长),窗口大、命中率高;方案二的脏数据窗口要求读请求 A 卡在"查数据库到回填"这一段(通常只有微秒到毫秒级),且写请求 B 必须在这段窗口内完成"更新数据库 + 删除缓存"------理论上可能,实际中要触发需要非常巧合的时机。

所以 Cache Aside 的标准做法是先更新数据库,再删除缓存 。这种方案还有一个额外好处:如果更新数据库失败,删除缓存根本不会执行,此时不用浪费资源重建缓存(缓存里依然是有效的旧数据,可以继续服务读请求)。如果是"先删缓存"方案,缓存已经被删了,数据库又更新失败,等于让缓存无故失效一次,下一次读请求还得重建。

4.3 两种方案对比

维度 先删缓存再更新 DB 先更新 DB 再删缓存
不一致发生概率 较高(脏数据窗口大) 极低(窗口短到几乎可以忽略)
不一致持续时间 长(直到 TTL 或下次写)
数据库更新失败的影响 缓存无故失效,需重建 缓存保留旧数据继续服务
是否推荐 是(Cache Aside 标准做法)

五、TTL 兜底:最简单的一致性保障

虽然"先更新数据库再删缓存"的不一致概率极低,但理论上还是存在的。最简单的兜底是给所有缓存设置 TTL(过期时间)

java 复制代码
// 写请求
public void write(String key, Object newValue) {
    db.update(key, newValue);          // 1. 更新数据库
    redis.del(key);                    // 2. 删除缓存
}

// 读请求
public Object read(String key) {
    Object value = redis.get(key);
    if (value != null) {
        return value;                  // 命中缓存
    }
    value = db.query(key);
    redis.set(key, value, 30, TimeUnit.MINUTES);  // 回填时设置 TTL
    return value;
}

加上 TTL 后,即使出现不一致,也最多在 TTL 时间内存在,过期后缓存会被自动清理,下一次读请求会重新从数据库加载最新数据。

TTL 兜底其实是非常实用的方案,很多业务场景都能接受"短时间内数据可能不一致"------比如文章阅读量、商品销量、推荐位内容这种数据,迟几秒到几分钟更新用户基本感知不到,加上 TTL 后就完全够用了。

但对于一些数据一致性要求很强的业务,TTL 兜底还不够,需要进一步优化。


六、高一致性业务的进阶方案

6.1 哪些业务适合高一致性方案

在讲进阶方案之前,先看哪些业务需要"高概率一致性"。判断标准是:数据不一致会带来业务损失或资损。常见的有:

  • 用户余额 / 账户资金:余额不一致直接导致用户多扣钱或少扣钱,涉及资损。
  • 库存数量:电商秒杀场景下,库存数据不一致会导致超卖或少卖,超卖要赔款,少卖要少赚钱。
  • 订单状态:订单状态从"待支付"到"已支付"如果不一致,可能重复发货或漏发货。
  • 支付流水 / 对账数据:金融业务的核心数据,必须保证一致。
  • 票务 / 抢购剩余名额:和库存类似,超卖会有法律风险。
  • 优惠券 / 红包剩余数量:超发会带来直接资损。

这些业务的特点是:数据不一致 = 直接经济损失。对于这类业务,Cache Aside + TTL 已经不够用了,需要更进一步的保障。

6.2 延迟双删

回到 4.1 那个脏数据窗口------读请求 A 把旧数据回填到缓存后,写请求 B 也已经更新完数据库了,但 B 不会再回去清缓存,所以脏数据就一直留在那里。那如果让写请求 B "等一会儿再删一次缓存"呢?这样就能把读请求 A 在等待期间回填的脏数据再清掉。

这就是延迟双删的思路:在原有"更新数据库 + 删除缓存"的基础上,加一个睡眠等待 + 第二次删除。伪代码如下:

java 复制代码
public void updateWithDoubleDelete(String key, Object newValue) {
    db.update(key, newValue);              // 1. 更新数据库
    redis.del(key);                        // 2. 第一次删除缓存
    sleep(N_MS);                           // 3. 睡眠一段时间
    redis.del(key);                        // 4. 第二次删除缓存
}

流程图:
#mermaid-svg-vEKEnXQ19fQuYWwd{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-vEKEnXQ19fQuYWwd .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-vEKEnXQ19fQuYWwd .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-vEKEnXQ19fQuYWwd .error-icon{fill:#552222;}#mermaid-svg-vEKEnXQ19fQuYWwd .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-vEKEnXQ19fQuYWwd .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-vEKEnXQ19fQuYWwd .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-vEKEnXQ19fQuYWwd .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-vEKEnXQ19fQuYWwd .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-vEKEnXQ19fQuYWwd .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-vEKEnXQ19fQuYWwd .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-vEKEnXQ19fQuYWwd .marker{fill:#333333;stroke:#333333;}#mermaid-svg-vEKEnXQ19fQuYWwd .marker.cross{stroke:#333333;}#mermaid-svg-vEKEnXQ19fQuYWwd svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-vEKEnXQ19fQuYWwd p{margin:0;}#mermaid-svg-vEKEnXQ19fQuYWwd .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-vEKEnXQ19fQuYWwd .cluster-label text{fill:#333;}#mermaid-svg-vEKEnXQ19fQuYWwd .cluster-label span{color:#333;}#mermaid-svg-vEKEnXQ19fQuYWwd .cluster-label span p{background-color:transparent;}#mermaid-svg-vEKEnXQ19fQuYWwd .label text,#mermaid-svg-vEKEnXQ19fQuYWwd span{fill:#333;color:#333;}#mermaid-svg-vEKEnXQ19fQuYWwd .node rect,#mermaid-svg-vEKEnXQ19fQuYWwd .node circle,#mermaid-svg-vEKEnXQ19fQuYWwd .node ellipse,#mermaid-svg-vEKEnXQ19fQuYWwd .node polygon,#mermaid-svg-vEKEnXQ19fQuYWwd .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-vEKEnXQ19fQuYWwd .rough-node .label text,#mermaid-svg-vEKEnXQ19fQuYWwd .node .label text,#mermaid-svg-vEKEnXQ19fQuYWwd .image-shape .label,#mermaid-svg-vEKEnXQ19fQuYWwd .icon-shape .label{text-anchor:middle;}#mermaid-svg-vEKEnXQ19fQuYWwd .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-vEKEnXQ19fQuYWwd .rough-node .label,#mermaid-svg-vEKEnXQ19fQuYWwd .node .label,#mermaid-svg-vEKEnXQ19fQuYWwd .image-shape .label,#mermaid-svg-vEKEnXQ19fQuYWwd .icon-shape .label{text-align:center;}#mermaid-svg-vEKEnXQ19fQuYWwd .node.clickable{cursor:pointer;}#mermaid-svg-vEKEnXQ19fQuYWwd .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-vEKEnXQ19fQuYWwd .arrowheadPath{fill:#333333;}#mermaid-svg-vEKEnXQ19fQuYWwd .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-vEKEnXQ19fQuYWwd .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-vEKEnXQ19fQuYWwd .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-vEKEnXQ19fQuYWwd .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-vEKEnXQ19fQuYWwd .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-vEKEnXQ19fQuYWwd .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-vEKEnXQ19fQuYWwd .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-vEKEnXQ19fQuYWwd .cluster text{fill:#333;}#mermaid-svg-vEKEnXQ19fQuYWwd .cluster span{color:#333;}#mermaid-svg-vEKEnXQ19fQuYWwd div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-vEKEnXQ19fQuYWwd .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-vEKEnXQ19fQuYWwd rect.text{fill:none;stroke-width:0;}#mermaid-svg-vEKEnXQ19fQuYWwd .icon-shape,#mermaid-svg-vEKEnXQ19fQuYWwd .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-vEKEnXQ19fQuYWwd .icon-shape p,#mermaid-svg-vEKEnXQ19fQuYWwd .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-vEKEnXQ19fQuYWwd .icon-shape .label rect,#mermaid-svg-vEKEnXQ19fQuYWwd .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-vEKEnXQ19fQuYWwd .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-vEKEnXQ19fQuYWwd .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-vEKEnXQ19fQuYWwd :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 写请求到达

  1. 更新数据库
  2. 第一次删除缓存
  3. 睡眠 N 毫秒

等待可能的脏数据回填
4. 第二次删除缓存
结束

为什么需要第二次删除? 因为在第一次删除缓存后到数据库更新完成之间,可能有一个读请求已经把旧数据回填到缓存里了(参见 4.1 的脏数据窗口分析)。第二次删除就是来"清理战场"的------把这段时间内被回填的脏数据再清掉。

sleep 时间 N 怎么定? 这是延迟双删最玄学的部分。核心思路是:让睡眠时间略大于"一次读请求从查数据库到回填缓存"的耗时。具体来说:

  • 估算主从复制延迟:如果用了读写分离,主从复制延迟通常是百毫秒级。
  • 估算读请求回填耗时:查数据库 + 网络往返 + 写 Redis,通常在几十毫秒。
  • 加上几百毫秒的缓冲。

业内最佳实践是 500ms,对于大多数业务场景来说都够用。如果业务用了主从复制并且延迟较大,可以适当上调到 1 秒;如果是单机 Redis 且读写都在主库,500ms 已经偏保守了。

java 复制代码
// 业内常见配置
private static final long DOUBLE_DELETE_SLEEP_MS = 500L;

public void updateWithDoubleDelete(String key, Object newValue) {
    db.update(key, newValue);
    redis.del(key);
    sleep(DOUBLE_DELETE_SLEEP_MS);
    redis.del(key);
}

延迟双删的代价是写请求耗时增加了 N 毫秒(500ms 左右),所以不能用在写请求 RT 敏感的接口上。通常的做法是把第二次删除异步化------通过 MQ 或者线程池异步执行,写请求不用等。

延迟双删虽然能极大降低不一致概率,但还是有一个问题:删除缓存这个操作本身可能失败。无论是第一次删除还是第二次删除,只要 Redis 节点故障、网络抖动、key 被锁等情况发生,删除就可能失败。如果删除失败,数据库已经更新了,但缓存里的脏数据没被清掉。这就需要 MQ 重试登场了。

6.3 MQ 重试

把删除缓存的操作发送到 MQ,由消费者执行删除,失败后 MQ 会自动重试。MQ 的核心价值就是保证至少一次删除成功------无论删除操作失败多少次,MQ 都会一直重试,直到删除成功为止:
#mermaid-svg-Ol4WJSdIyEnrdlRM{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-Ol4WJSdIyEnrdlRM .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-Ol4WJSdIyEnrdlRM .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-Ol4WJSdIyEnrdlRM .error-icon{fill:#552222;}#mermaid-svg-Ol4WJSdIyEnrdlRM .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-Ol4WJSdIyEnrdlRM .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-Ol4WJSdIyEnrdlRM .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-Ol4WJSdIyEnrdlRM .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-Ol4WJSdIyEnrdlRM .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-Ol4WJSdIyEnrdlRM .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-Ol4WJSdIyEnrdlRM .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-Ol4WJSdIyEnrdlRM .marker{fill:#333333;stroke:#333333;}#mermaid-svg-Ol4WJSdIyEnrdlRM .marker.cross{stroke:#333333;}#mermaid-svg-Ol4WJSdIyEnrdlRM svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-Ol4WJSdIyEnrdlRM p{margin:0;}#mermaid-svg-Ol4WJSdIyEnrdlRM .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-Ol4WJSdIyEnrdlRM .cluster-label text{fill:#333;}#mermaid-svg-Ol4WJSdIyEnrdlRM .cluster-label span{color:#333;}#mermaid-svg-Ol4WJSdIyEnrdlRM .cluster-label span p{background-color:transparent;}#mermaid-svg-Ol4WJSdIyEnrdlRM .label text,#mermaid-svg-Ol4WJSdIyEnrdlRM span{fill:#333;color:#333;}#mermaid-svg-Ol4WJSdIyEnrdlRM .node rect,#mermaid-svg-Ol4WJSdIyEnrdlRM .node circle,#mermaid-svg-Ol4WJSdIyEnrdlRM .node ellipse,#mermaid-svg-Ol4WJSdIyEnrdlRM .node polygon,#mermaid-svg-Ol4WJSdIyEnrdlRM .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-Ol4WJSdIyEnrdlRM .rough-node .label text,#mermaid-svg-Ol4WJSdIyEnrdlRM .node .label text,#mermaid-svg-Ol4WJSdIyEnrdlRM .image-shape .label,#mermaid-svg-Ol4WJSdIyEnrdlRM .icon-shape .label{text-anchor:middle;}#mermaid-svg-Ol4WJSdIyEnrdlRM .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-Ol4WJSdIyEnrdlRM .rough-node .label,#mermaid-svg-Ol4WJSdIyEnrdlRM .node .label,#mermaid-svg-Ol4WJSdIyEnrdlRM .image-shape .label,#mermaid-svg-Ol4WJSdIyEnrdlRM .icon-shape .label{text-align:center;}#mermaid-svg-Ol4WJSdIyEnrdlRM .node.clickable{cursor:pointer;}#mermaid-svg-Ol4WJSdIyEnrdlRM .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-Ol4WJSdIyEnrdlRM .arrowheadPath{fill:#333333;}#mermaid-svg-Ol4WJSdIyEnrdlRM .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-Ol4WJSdIyEnrdlRM .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-Ol4WJSdIyEnrdlRM .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-Ol4WJSdIyEnrdlRM .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-Ol4WJSdIyEnrdlRM .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-Ol4WJSdIyEnrdlRM .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-Ol4WJSdIyEnrdlRM .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-Ol4WJSdIyEnrdlRM .cluster text{fill:#333;}#mermaid-svg-Ol4WJSdIyEnrdlRM .cluster span{color:#333;}#mermaid-svg-Ol4WJSdIyEnrdlRM div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-Ol4WJSdIyEnrdlRM .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-Ol4WJSdIyEnrdlRM rect.text{fill:none;stroke-width:0;}#mermaid-svg-Ol4WJSdIyEnrdlRM .icon-shape,#mermaid-svg-Ol4WJSdIyEnrdlRM .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-Ol4WJSdIyEnrdlRM .icon-shape p,#mermaid-svg-Ol4WJSdIyEnrdlRM .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-Ol4WJSdIyEnrdlRM .icon-shape .label rect,#mermaid-svg-Ol4WJSdIyEnrdlRM .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-Ol4WJSdIyEnrdlRM .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-Ol4WJSdIyEnrdlRM .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-Ol4WJSdIyEnrdlRM :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 是

写请求
更新数据库
第一次删除缓存
发送删除消息到 MQ
异步延迟 N ms
MQ 消费者
执行删除缓存
删除成功?
ACK 确认
等待重试

这里有一个非常关键的点:必须是删除缓存成功后再回 ACK 给 MQ,否则可能造成消息丢失。来看反面例子:
Redis缓存 消息队列 消费者 Redis缓存 消息队列 消费者 #mermaid-svg-aTCmwWVRalPzLiAV{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-aTCmwWVRalPzLiAV .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-aTCmwWVRalPzLiAV .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-aTCmwWVRalPzLiAV .error-icon{fill:#552222;}#mermaid-svg-aTCmwWVRalPzLiAV .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-aTCmwWVRalPzLiAV .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-aTCmwWVRalPzLiAV .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-aTCmwWVRalPzLiAV .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-aTCmwWVRalPzLiAV .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-aTCmwWVRalPzLiAV .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-aTCmwWVRalPzLiAV .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-aTCmwWVRalPzLiAV .marker{fill:#333333;stroke:#333333;}#mermaid-svg-aTCmwWVRalPzLiAV .marker.cross{stroke:#333333;}#mermaid-svg-aTCmwWVRalPzLiAV svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-aTCmwWVRalPzLiAV p{margin:0;}#mermaid-svg-aTCmwWVRalPzLiAV .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-aTCmwWVRalPzLiAV text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-aTCmwWVRalPzLiAV .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-aTCmwWVRalPzLiAV .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-aTCmwWVRalPzLiAV .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-aTCmwWVRalPzLiAV .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-aTCmwWVRalPzLiAV #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-aTCmwWVRalPzLiAV .sequenceNumber{fill:white;}#mermaid-svg-aTCmwWVRalPzLiAV #sequencenumber{fill:#333;}#mermaid-svg-aTCmwWVRalPzLiAV #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-aTCmwWVRalPzLiAV .messageText{fill:#333;stroke:none;}#mermaid-svg-aTCmwWVRalPzLiAV .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-aTCmwWVRalPzLiAV .labelText,#mermaid-svg-aTCmwWVRalPzLiAV .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-aTCmwWVRalPzLiAV .loopText,#mermaid-svg-aTCmwWVRalPzLiAV .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-aTCmwWVRalPzLiAV .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-aTCmwWVRalPzLiAV .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-aTCmwWVRalPzLiAV .noteText,#mermaid-svg-aTCmwWVRalPzLiAV .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-aTCmwWVRalPzLiAV .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-aTCmwWVRalPzLiAV .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-aTCmwWVRalPzLiAV .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-aTCmwWVRalPzLiAV .actorPopupMenu{position:absolute;}#mermaid-svg-aTCmwWVRalPzLiAV .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-aTCmwWVRalPzLiAV .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-aTCmwWVRalPzLiAV .actor-man circle,#mermaid-svg-aTCmwWVRalPzLiAV line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-aTCmwWVRalPzLiAV :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 消息已被确认,无法重试 缓存数据永久不一致 1. 接收到删除消息 2. 直接回 ACK(错误!) 3. 执行删除缓存 → 失败

正确顺序应该是:
Redis缓存 消息队列 消费者 Redis缓存 消息队列 消费者 #mermaid-svg-PovXDvtQcyXpVFsJ{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-PovXDvtQcyXpVFsJ .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-PovXDvtQcyXpVFsJ .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-PovXDvtQcyXpVFsJ .error-icon{fill:#552222;}#mermaid-svg-PovXDvtQcyXpVFsJ .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-PovXDvtQcyXpVFsJ .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-PovXDvtQcyXpVFsJ .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-PovXDvtQcyXpVFsJ .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-PovXDvtQcyXpVFsJ .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-PovXDvtQcyXpVFsJ .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-PovXDvtQcyXpVFsJ .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-PovXDvtQcyXpVFsJ .marker{fill:#333333;stroke:#333333;}#mermaid-svg-PovXDvtQcyXpVFsJ .marker.cross{stroke:#333333;}#mermaid-svg-PovXDvtQcyXpVFsJ svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-PovXDvtQcyXpVFsJ p{margin:0;}#mermaid-svg-PovXDvtQcyXpVFsJ .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-PovXDvtQcyXpVFsJ text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-PovXDvtQcyXpVFsJ .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-PovXDvtQcyXpVFsJ .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-PovXDvtQcyXpVFsJ .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-PovXDvtQcyXpVFsJ .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-PovXDvtQcyXpVFsJ #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-PovXDvtQcyXpVFsJ .sequenceNumber{fill:white;}#mermaid-svg-PovXDvtQcyXpVFsJ #sequencenumber{fill:#333;}#mermaid-svg-PovXDvtQcyXpVFsJ #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-PovXDvtQcyXpVFsJ .messageText{fill:#333;stroke:none;}#mermaid-svg-PovXDvtQcyXpVFsJ .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-PovXDvtQcyXpVFsJ .labelText,#mermaid-svg-PovXDvtQcyXpVFsJ .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-PovXDvtQcyXpVFsJ .loopText,#mermaid-svg-PovXDvtQcyXpVFsJ .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-PovXDvtQcyXpVFsJ .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-PovXDvtQcyXpVFsJ .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-PovXDvtQcyXpVFsJ .noteText,#mermaid-svg-PovXDvtQcyXpVFsJ .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-PovXDvtQcyXpVFsJ .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-PovXDvtQcyXpVFsJ .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-PovXDvtQcyXpVFsJ .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-PovXDvtQcyXpVFsJ .actorPopupMenu{position:absolute;}#mermaid-svg-PovXDvtQcyXpVFsJ .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-PovXDvtQcyXpVFsJ .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-PovXDvtQcyXpVFsJ .actor-man circle,#mermaid-svg-PovXDvtQcyXpVFsJ line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-PovXDvtQcyXpVFsJ :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 删除成功后再 ACK 如果删除失败,不回 ACK MQ 会自动重试 1. 接收到删除消息 2. 执行删除缓存 3. 回 ACK 确认

如果 MQ 重试几次(比如 3-5 次)还是失败,就应该触发告警,让运维或开发介入处理。常见的失败原因有:Redis 节点故障、网络抖动、key 被锁等。

MQ 重试方案的优点是可靠性高 ,缺点是对代码侵入性强------业务代码里要嵌入发送 MQ 消息的逻辑,并且要处理 MQ 本身的可用性问题。有没有更解耦的方案?有,就是 Canal。

6.4 Canal + MQ 方案

Canal 是阿里开源的一个组件,它伪装成 MySQL 的从库,订阅 MySQL 的 binlog 日志。只要 MySQL 里的数据有变更(INSERT、UPDATE、DELETE),binlog 就会记录下来,Canal 就能感知到。

整体架构如下:
#mermaid-svg-ZEuQIiGf1jFb3LgQ{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-ZEuQIiGf1jFb3LgQ .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-ZEuQIiGf1jFb3LgQ .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-ZEuQIiGf1jFb3LgQ .error-icon{fill:#552222;}#mermaid-svg-ZEuQIiGf1jFb3LgQ .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-ZEuQIiGf1jFb3LgQ .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-ZEuQIiGf1jFb3LgQ .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-ZEuQIiGf1jFb3LgQ .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-ZEuQIiGf1jFb3LgQ .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-ZEuQIiGf1jFb3LgQ .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-ZEuQIiGf1jFb3LgQ .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-ZEuQIiGf1jFb3LgQ .marker{fill:#333333;stroke:#333333;}#mermaid-svg-ZEuQIiGf1jFb3LgQ .marker.cross{stroke:#333333;}#mermaid-svg-ZEuQIiGf1jFb3LgQ svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-ZEuQIiGf1jFb3LgQ p{margin:0;}#mermaid-svg-ZEuQIiGf1jFb3LgQ .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-ZEuQIiGf1jFb3LgQ .cluster-label text{fill:#333;}#mermaid-svg-ZEuQIiGf1jFb3LgQ .cluster-label span{color:#333;}#mermaid-svg-ZEuQIiGf1jFb3LgQ .cluster-label span p{background-color:transparent;}#mermaid-svg-ZEuQIiGf1jFb3LgQ .label text,#mermaid-svg-ZEuQIiGf1jFb3LgQ span{fill:#333;color:#333;}#mermaid-svg-ZEuQIiGf1jFb3LgQ .node rect,#mermaid-svg-ZEuQIiGf1jFb3LgQ .node circle,#mermaid-svg-ZEuQIiGf1jFb3LgQ .node ellipse,#mermaid-svg-ZEuQIiGf1jFb3LgQ .node polygon,#mermaid-svg-ZEuQIiGf1jFb3LgQ .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-ZEuQIiGf1jFb3LgQ .rough-node .label text,#mermaid-svg-ZEuQIiGf1jFb3LgQ .node .label text,#mermaid-svg-ZEuQIiGf1jFb3LgQ .image-shape .label,#mermaid-svg-ZEuQIiGf1jFb3LgQ .icon-shape .label{text-anchor:middle;}#mermaid-svg-ZEuQIiGf1jFb3LgQ .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-ZEuQIiGf1jFb3LgQ .rough-node .label,#mermaid-svg-ZEuQIiGf1jFb3LgQ .node .label,#mermaid-svg-ZEuQIiGf1jFb3LgQ .image-shape .label,#mermaid-svg-ZEuQIiGf1jFb3LgQ .icon-shape .label{text-align:center;}#mermaid-svg-ZEuQIiGf1jFb3LgQ .node.clickable{cursor:pointer;}#mermaid-svg-ZEuQIiGf1jFb3LgQ .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-ZEuQIiGf1jFb3LgQ .arrowheadPath{fill:#333333;}#mermaid-svg-ZEuQIiGf1jFb3LgQ .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-ZEuQIiGf1jFb3LgQ .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-ZEuQIiGf1jFb3LgQ .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-ZEuQIiGf1jFb3LgQ .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-ZEuQIiGf1jFb3LgQ .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-ZEuQIiGf1jFb3LgQ .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-ZEuQIiGf1jFb3LgQ .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-ZEuQIiGf1jFb3LgQ .cluster text{fill:#333;}#mermaid-svg-ZEuQIiGf1jFb3LgQ .cluster span{color:#333;}#mermaid-svg-ZEuQIiGf1jFb3LgQ div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-ZEuQIiGf1jFb3LgQ .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-ZEuQIiGf1jFb3LgQ rect.text{fill:none;stroke-width:0;}#mermaid-svg-ZEuQIiGf1jFb3LgQ .icon-shape,#mermaid-svg-ZEuQIiGf1jFb3LgQ .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-ZEuQIiGf1jFb3LgQ .icon-shape p,#mermaid-svg-ZEuQIiGf1jFb3LgQ .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-ZEuQIiGf1jFb3LgQ .icon-shape .label rect,#mermaid-svg-ZEuQIiGf1jFb3LgQ .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-ZEuQIiGf1jFb3LgQ .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-ZEuQIiGf1jFb3LgQ .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-ZEuQIiGf1jFb3LgQ :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 删除成功后 ACK
业务服务
MySQL
binlog 日志
Canal
消息队列
缓存消费者
Redis

业务流程:

  1. 业务服务只负责更新 MySQL,完全不关心缓存。
  2. MySQL 把数据变更记录到 binlog。
  3. Canal 订阅 binlog,把变更事件发送到 MQ。
  4. MQ 消费者拿到变更事件后,执行删除缓存操作。
  5. 删除成功后再回 ACK 给 MQ(和 6.3 强调的关键点一致)。

Canal 方案的最大优势是业务解耦:业务代码里完全不用关心缓存一致性,只管写数据库就行。Canal + MQ 负责把缓存清理掉。这种方案对业务代码的侵入性几乎为零。

但 Canal 方案也带来了新的运维复杂度,需要重点关注的痛点有:

  • Canal 单点故障:Canal 本身如果挂了,所有的删除缓存操作都会失效。所以 Canal 必须要做高可用部署(Canal HA),通常借助 ZooKeeper 来做主备切换。
  • binlog 位点管理:Canal 通过记录 binlog 的位点(position)来标记"我读到哪里了"。如果 Canal 重启后位点丢失或回退,可能导致变更事件丢失或重复消费。位点要持久化到 ZooKeeper 或文件中。
  • 消息乱序问题:同一个 key 的多次变更如果路由到不同的 MQ 分区,可能出现"先更新后删除"的乱序,导致最终缓存状态错误。需要按 key 做分片路由,保证同一 key 的变更落到同一分区。
  • ACK 时序陷阱:和 6.3 一样,消费者必须先删除缓存成功再回 ACK,否则消息丢失。
  • 对 MySQL 版本敏感:Canal 依赖 binlog,不同 MySQL 版本的 binlog 格式(ROW / STATEMENT / MIXED)有差异。Canal 推荐使用 ROW 格式,能拿到变更前后的具体值。
  • 运维复杂度高:Canal + MQ + 监控 + 告警,整套链路比 Cache Aside 复杂得多,需要专人维护。
  • 数据延迟:从 MySQL 变更到缓存被清理,中间经过 Canal 解析、MQ 投递、消费处理,通常有几十毫秒到几百毫秒的延迟。在这个窗口内读请求可能读到旧数据,但这点延迟对绝大多数业务都是可接受的。

到这里,前六节讲的所有方案(Cache Aside + TTL、延迟双删 + MQ 重试、Canal + MQ)都属于高概率一致性方案 ------它们能把不一致的概率压到极低,但理论上仍有不一致的可能(线程调度、网络抖动、Redis 主从切换等奇葩场景)。对于"读到的数据必须是最新的"这类强一致业务,前面这些方案都不够,得换一种思路:用分布式锁强制读写互斥。这就是第七节要讲的方案。


七、强一致性业务:分布式锁

对于某些强一致性的业务,比如用户余额、支付流水、转账等,前面所有的最终一致性方案都不够------它们都允许在某个时间窗口内存在数据不一致。这类业务要求"读到的数据必须是最新的",那就只能上分布式锁了。

核心思路是:读写互斥。写请求在更新数据库 + 删除缓存期间,持有分布式锁;读请求在查数据库 + 回填缓存期间,也要先获取锁。这样读写无法同时进行,自然也就不存在脏缓存了。
#mermaid-svg-1AbhiAaQnqpg2RAh{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-1AbhiAaQnqpg2RAh .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-1AbhiAaQnqpg2RAh .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-1AbhiAaQnqpg2RAh .error-icon{fill:#552222;}#mermaid-svg-1AbhiAaQnqpg2RAh .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-1AbhiAaQnqpg2RAh .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-1AbhiAaQnqpg2RAh .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-1AbhiAaQnqpg2RAh .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-1AbhiAaQnqpg2RAh .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-1AbhiAaQnqpg2RAh .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-1AbhiAaQnqpg2RAh .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-1AbhiAaQnqpg2RAh .marker{fill:#333333;stroke:#333333;}#mermaid-svg-1AbhiAaQnqpg2RAh .marker.cross{stroke:#333333;}#mermaid-svg-1AbhiAaQnqpg2RAh svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-1AbhiAaQnqpg2RAh p{margin:0;}#mermaid-svg-1AbhiAaQnqpg2RAh .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-1AbhiAaQnqpg2RAh .cluster-label text{fill:#333;}#mermaid-svg-1AbhiAaQnqpg2RAh .cluster-label span{color:#333;}#mermaid-svg-1AbhiAaQnqpg2RAh .cluster-label span p{background-color:transparent;}#mermaid-svg-1AbhiAaQnqpg2RAh .label text,#mermaid-svg-1AbhiAaQnqpg2RAh span{fill:#333;color:#333;}#mermaid-svg-1AbhiAaQnqpg2RAh .node rect,#mermaid-svg-1AbhiAaQnqpg2RAh .node circle,#mermaid-svg-1AbhiAaQnqpg2RAh .node ellipse,#mermaid-svg-1AbhiAaQnqpg2RAh .node polygon,#mermaid-svg-1AbhiAaQnqpg2RAh .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-1AbhiAaQnqpg2RAh .rough-node .label text,#mermaid-svg-1AbhiAaQnqpg2RAh .node .label text,#mermaid-svg-1AbhiAaQnqpg2RAh .image-shape .label,#mermaid-svg-1AbhiAaQnqpg2RAh .icon-shape .label{text-anchor:middle;}#mermaid-svg-1AbhiAaQnqpg2RAh .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-1AbhiAaQnqpg2RAh .rough-node .label,#mermaid-svg-1AbhiAaQnqpg2RAh .node .label,#mermaid-svg-1AbhiAaQnqpg2RAh .image-shape .label,#mermaid-svg-1AbhiAaQnqpg2RAh .icon-shape .label{text-align:center;}#mermaid-svg-1AbhiAaQnqpg2RAh .node.clickable{cursor:pointer;}#mermaid-svg-1AbhiAaQnqpg2RAh .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-1AbhiAaQnqpg2RAh .arrowheadPath{fill:#333333;}#mermaid-svg-1AbhiAaQnqpg2RAh .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-1AbhiAaQnqpg2RAh .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-1AbhiAaQnqpg2RAh .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-1AbhiAaQnqpg2RAh .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-1AbhiAaQnqpg2RAh .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-1AbhiAaQnqpg2RAh .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-1AbhiAaQnqpg2RAh .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-1AbhiAaQnqpg2RAh .cluster text{fill:#333;}#mermaid-svg-1AbhiAaQnqpg2RAh .cluster span{color:#333;}#mermaid-svg-1AbhiAaQnqpg2RAh div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-1AbhiAaQnqpg2RAh .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-1AbhiAaQnqpg2RAh rect.text{fill:none;stroke-width:0;}#mermaid-svg-1AbhiAaQnqpg2RAh .icon-shape,#mermaid-svg-1AbhiAaQnqpg2RAh .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-1AbhiAaQnqpg2RAh .icon-shape p,#mermaid-svg-1AbhiAaQnqpg2RAh .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-1AbhiAaQnqpg2RAh .icon-shape .label rect,#mermaid-svg-1AbhiAaQnqpg2RAh .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-1AbhiAaQnqpg2RAh .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-1AbhiAaQnqpg2RAh .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-1AbhiAaQnqpg2RAh :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 是

写请求
获取分布式锁
更新数据库
删除缓存
释放锁
结束
读请求
获取分布式锁
查询缓存
命中?
返回缓存
查询数据库并回填
返回数据
释放锁
结束

伪代码:

java 复制代码
public void writeWithLock(String key, Object newValue) {
    String lockKey = "lock:" + key;
    String requestId = UUID.randomUUID().toString();
    // 获取分布式锁
    boolean locked = redis.set(lockKey, requestId, 10, TimeUnit.SECONDS, SetParams.nx());
    if (!locked) {
        // 获取锁失败,重试或抛异常
        throw new RuntimeException("系统繁忙,请稍后重试");
    }
    try {
        db.update(key, newValue);
        redis.del(key);
    } finally {
        // 释放锁(需要校验 requestId 防止误删)
        releaseLock(lockKey, requestId);
    }
}

public Object readWithLock(String key) {
    String lockKey = "lock:" + key;
    String requestId = UUID.randomUUID().toString();
    boolean locked = redis.set(lockKey, requestId, 10, TimeUnit.SECONDS, SetParams.nx());
    if (!locked) {
        // 短暂等待后重试,或返回降级数据
        Thread.sleep(50);
        return readWithLock(key);
    }
    try {
        Object value = redis.get(key);
        if (value != null) return value;
        value = db.query(key);
        redis.set(key, value, 30, TimeUnit.MINUTES);
        return value;
    } finally {
        releaseLock(lockKey, requestId);
    }
}

分布式锁能保证强一致性,但代价是并发量大幅下降------读写互斥意味着同一时间只有一个请求能操作这个 key,QPS 远低于 Cache Aside。这种方案是否使用,完全取决于业务:如果业务对一致性要求极高(比如资金),牺牲并发量是值得的;如果业务对一致性要求一般(比如文章阅读量),用分布式锁就是杀鸡用牛刀。

关于 Redis 分布式锁的更多坑(锁争抢、僵尸锁、锁过期、锁丢失),可以参考本系列的另一篇文章:一把 Redis 分布式锁,踩透四个坑:锁争抢、僵尸锁、锁过期、锁丢失


八、何时放弃缓存:绝对一致性业务

前面讲的所有方案------Cache Aside、延迟双删、Canal+MQ、分布式锁------本质上都在降低 不一致的概率,而不是消除它。即使上了分布式锁,也可能因为以下奇葩情况出现不一致:

  • 网络抖动:Redis 删除操作因为网络问题没到达 Redis 节点,或者到达了但响应丢失。
  • Redis 主从切换瞬间:主节点刚写入数据还没同步到从节点就挂了,从节点升主后数据丢失。
  • 客户端崩溃:业务服务在更新数据库后、删除缓存前崩溃,导致缓存里残留旧数据。
  • 时钟漂移:分布式系统里不同机器时钟不同步,导致 TTL 计算错误。
  • GC 停顿:JVM 长时间 STW(Stop The World)期间,持有的锁可能过期,其他请求趁虚而入。

对于绝对一致性业务,这些奇葩情况虽然概率极低,但一旦发生就是资损。所以对于零容忍 的业务,唯一的选择是:放弃缓存,直接读写数据库,靠数据库本身的并发能力支撑。

典型场景:

  • 资金对账:金融行业的对账系统,每天结算时必须保证账目绝对一致,差一分钱都要查清楚。
  • 合规审计:监管要求的审计数据,必须以数据库为准,不能有任何缓存层。
  • 跨系统转账:A 银行扣款、B 银行入账,这种跨系统的资金流转必须保证两端一致。
  • 法律取证数据:涉及法律取证的数据,必须保证数据原始性,不能经过缓存。

这种业务的并发量通常不高(对账系统每天结算一次就行),靠数据库本身的性能是能扛住的。如果并发量实在太高,那就不是缓存能解决的问题了,应该考虑数据库分库分表、读写分离、NewSQL 等方案。


九、方案选择决策树

把前面所有方案串起来,给一个选型决策树:
#mermaid-svg-ENtieYDln8DfQxfj{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-ENtieYDln8DfQxfj .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-ENtieYDln8DfQxfj .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-ENtieYDln8DfQxfj .error-icon{fill:#552222;}#mermaid-svg-ENtieYDln8DfQxfj .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-ENtieYDln8DfQxfj .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-ENtieYDln8DfQxfj .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-ENtieYDln8DfQxfj .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-ENtieYDln8DfQxfj .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-ENtieYDln8DfQxfj .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-ENtieYDln8DfQxfj .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-ENtieYDln8DfQxfj .marker{fill:#333333;stroke:#333333;}#mermaid-svg-ENtieYDln8DfQxfj .marker.cross{stroke:#333333;}#mermaid-svg-ENtieYDln8DfQxfj svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-ENtieYDln8DfQxfj p{margin:0;}#mermaid-svg-ENtieYDln8DfQxfj .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-ENtieYDln8DfQxfj .cluster-label text{fill:#333;}#mermaid-svg-ENtieYDln8DfQxfj .cluster-label span{color:#333;}#mermaid-svg-ENtieYDln8DfQxfj .cluster-label span p{background-color:transparent;}#mermaid-svg-ENtieYDln8DfQxfj .label text,#mermaid-svg-ENtieYDln8DfQxfj span{fill:#333;color:#333;}#mermaid-svg-ENtieYDln8DfQxfj .node rect,#mermaid-svg-ENtieYDln8DfQxfj .node circle,#mermaid-svg-ENtieYDln8DfQxfj .node ellipse,#mermaid-svg-ENtieYDln8DfQxfj .node polygon,#mermaid-svg-ENtieYDln8DfQxfj .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-ENtieYDln8DfQxfj .rough-node .label text,#mermaid-svg-ENtieYDln8DfQxfj .node .label text,#mermaid-svg-ENtieYDln8DfQxfj .image-shape .label,#mermaid-svg-ENtieYDln8DfQxfj .icon-shape .label{text-anchor:middle;}#mermaid-svg-ENtieYDln8DfQxfj .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-ENtieYDln8DfQxfj .rough-node .label,#mermaid-svg-ENtieYDln8DfQxfj .node .label,#mermaid-svg-ENtieYDln8DfQxfj .image-shape .label,#mermaid-svg-ENtieYDln8DfQxfj .icon-shape .label{text-align:center;}#mermaid-svg-ENtieYDln8DfQxfj .node.clickable{cursor:pointer;}#mermaid-svg-ENtieYDln8DfQxfj .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-ENtieYDln8DfQxfj .arrowheadPath{fill:#333333;}#mermaid-svg-ENtieYDln8DfQxfj .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-ENtieYDln8DfQxfj .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-ENtieYDln8DfQxfj .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-ENtieYDln8DfQxfj .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-ENtieYDln8DfQxfj .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-ENtieYDln8DfQxfj .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-ENtieYDln8DfQxfj .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-ENtieYDln8DfQxfj .cluster text{fill:#333;}#mermaid-svg-ENtieYDln8DfQxfj .cluster span{color:#333;}#mermaid-svg-ENtieYDln8DfQxfj div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-ENtieYDln8DfQxfj .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-ENtieYDln8DfQxfj rect.text{fill:none;stroke-width:0;}#mermaid-svg-ENtieYDln8DfQxfj .icon-shape,#mermaid-svg-ENtieYDln8DfQxfj .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-ENtieYDln8DfQxfj .icon-shape p,#mermaid-svg-ENtieYDln8DfQxfj .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-ENtieYDln8DfQxfj .icon-shape .label rect,#mermaid-svg-ENtieYDln8DfQxfj .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-ENtieYDln8DfQxfj .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-ENtieYDln8DfQxfj .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-ENtieYDln8DfQxfj :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 低(容忍分钟级不一致)
中(容忍秒级不一致)
高(要求强一致)


否(容忍理论不一致)
是(绝对一致)


希望业务解耦
开始选型
业务对一致性要求?
Cache Aside + TTL
写请求是否频繁?
是否零容忍?
Cache Aside 先更新DB再删缓存
延迟双删 + TTL
分布式锁
放弃缓存, 直接读写DB
删除缓存可能失败?

  • MQ 重试
    完成
    Canal + MQ

9.1 全方案对比

先说一个核心结论:只要不是绝对一致(即引入了缓存),就必须给缓存设置 TTL 兜底 。因为所有方案都有极端场景下的不一致可能------线程调度、网络抖动、Redis 主从切换、GC 停顿、节点宕机等奇葩情况一旦发生,前面方案设计的"删除时机"就可能失效,此时唯一能兜底的就是 TTL。TTL 是最后一道防线,不是可选选项

下面这张表把前面所有方案汇总在一起,重点对比"如果不一致真的发生了,它会持续多久才能被修复":

方案 不一致持续时长(如果发生) 是否需要 TTL 兜底 代码侵入 运维复杂度 典型业务场景
Cache Aside + TTL 持续到 TTL 过期(分钟级) 文章阅读量、商品销量、推荐位内容、热搜榜单
延迟双删 + TTL 正常情况下 500ms 后被第二次删除清掉;若第二次删除失败,持续到 TTL 过期 写多读多、对一致性有一定要求:评论数、点赞数、库存展示
延迟双删 + MQ 重试 正常情况下 500ms + MQ 重试间隔内清掉;若 MQ 重试几次都失败,持续到 TTL 过期 中高 一致性要求较高、能接受异步:订单状态、优惠券剩余数量
Canal + MQ 正常情况下 Canal 解析 + MQ 投递延迟(几十~几百毫秒)内清掉;若 Canal/MQ 故障,持续到 TTL 过期 业务代码希望解耦、有专业运维团队:用户中心数据、商品主信息
分布式锁 常态下不会发生;极端场景(GC 停顿导致锁过期、客户端崩溃)下持续到 TTL 过期 强一致业务:用户余额、账户资金、转账流水、支付扣款
放弃缓存 永远不会不一致 零容忍业务:资金对账、合规审计、跨行转账、法律取证

表里几个关键点说明:

  • 不一致持续时长:指的是"万一不一致真的发生了,要等多久才能自动恢复一致"。前五种方案在正常情况下都能在很短的时间内清掉脏数据(500ms / 几百毫秒 / 无窗口),但一旦发生异常(删除失败、Canal 挂了、锁过期),唯一兜底就是 TTL,所以 TTL 的时长就是不一致的最坏持续时长。
  • TTL 兜底是必需的:前五种方案无论设计得多精细,都不能 100% 保证一致。TTL 是最后一道防线,确保即使所有方案都失效,不一致也只持续到 TTL 过期。只有"放弃缓存"方案不需要 TTL,因为根本没有缓存这一层。
  • 一致性强度分三档
    • 高概率一致(Cache Aside / 延迟双删 / Canal + MQ):能压到极低概率,但理论上仍可能不一致,必须靠 TTL 兜底。
    • 强一致(分布式锁):通过锁强制互斥,常态下保证一致,但极端场景(GC 停顿、客户端崩溃)仍有风险,依然需要 TTL 兜底。
    • 绝对一致(放弃缓存):根本不引入缓存,数据库是唯一数据源,不存在不一致可能。
  • 代码侵入 vs 运维复杂度:往往是此消彼长------Cache Aside / 延迟双删对代码侵入大但运维简单;Canal + MQ 对代码侵入小但运维复杂度高。选型时要在两者之间做取舍。

9.2 选型建议

简单总结:

  • 绝大多数业务:Cache Aside + TTL 就够了。
  • 写请求频繁、对一致性有一定要求:延迟双删 + TTL,必要时加 MQ 重试。
  • 希望业务代码解耦、有专业运维团队:Canal + MQ。
  • 强一致业务(如资金):分布式锁,牺牲并发换一致。
  • 绝对一致业务(如对账、审计):放弃缓存,直接读写数据库。

十、总结

缓存一致性的核心矛盾是:同一份数据存在两个副本,更新其中一个时如何保证另一个跟着变。围绕这个矛盾,方案是层层递进的:

  1. 更新缓存策略行不通------无论是先更新缓存还是先更新数据库,并发写都会导致数据不一致。
  2. Cache Aside 是基础方案------改更新为删除,利用删除的幂等性解决写写并发问题。推荐"先更新数据库再删除缓存",因为不一致窗口极短。
  3. TTL 是兜底------给所有缓存设置 TTL,不一致最多持续到 TTL 过期。
  4. 延迟双删 + MQ 重试是进阶------通过第二次删除清理脏数据,MQ 保证删除操作的可靠性。sleep 时间业内通常取 500ms。
  5. Canal + MQ 是解耦方案------通过订阅 binlog 把缓存清理逻辑从业务代码中剥离,但带来 Canal 高可用、binlog 位点、消息乱序等运维复杂度。
  6. 分布式锁是强一致方案------读写互斥,牺牲并发换一致。
  7. 绝对一致业务放弃缓存------对账、审计、资金等零容忍场景,靠数据库本身支撑并发。

实际项目中,不要一上来就上最复杂的方案。先用 Cache Aside + TTL,遇到问题再加延迟双删、MQ 重试,再不行才考虑 Canal 或分布式锁。技术选型永远是够用就好,不是越复杂越好。

相关推荐
z123456789861 小时前
2026最新两款AI编程工具深度对比实测
java·数据库·ai编程
程序猿DD1 小时前
一个 API Key,统一调用大模型、生图和联网搜索
数据库·网关
yaoxin5211232 小时前
470. Java 反射 - Member 接口与 AccessFlag
java·开发语言·python
做个文艺程序员2 小时前
Linux第24篇:Java应用监控体系搭建:Prometheus+Grafana可视化运维
java·grafana·prometheus
TlSfoward2 小时前
抓包代理链路下的 TLS 指纹变化分析 TLSFOWARD抓包工具
数据库·爬虫·网络协议·搜索引擎·php
数智化管理手记2 小时前
账龄分析手工统计易遗漏?自动账龄分析工具怎么搭建
大数据·网络·数据库·人工智能·数据挖掘
FF2501_940228583 小时前
Grid 构建月历网格:7 列模板 + 日期占位算法
后端·华为·harmonyos·鸿蒙系统
神奇小汤圆3 小时前
线程池的 DiscardPolicy 为什么是「静默杀手」?一个生产事故的完整复盘
后端
大鸡腿同学3 小时前
个人博主 AI 剪辑实操:口播视频 3 步出片,省 80% 剪辑时间
后端
LabVIEW开发3 小时前
NI Package Manager安装LabVIEW时InternalServerError错误
网络·数据库·labview·labview知识·labview功能·labview程序