引入 Redis 缓存层后,"数据库更新了缓存却没跟上"就成了绕不开的一致性难题。本文按"问题 → 朴素方案为何失败 → Cache Aside → 先删 vs 后删 → TTL 兜底 → 延迟双删 + MQ 重试 → Canal + MQ → 分布式锁 → 何时放弃缓存"的进阶路线,系统讲透更新缓存与删除缓存的取舍、并发读写场景下的脏数据窗口、ACK 时序、Canal 痛点以及强一致业务的选型逻辑,并配多张流程图辅助理解。
一、引入缓存层后的"原罪":数据一致性问题
在讲一致性之前,先回顾一下 Redis 缓存层在系统里的位置。业务数据最终落在 MySQL 的磁盘上,而 Redis 作为内存数据库挡在 MySQL 之前,承担"挡子弹"的角色------读请求优先查 Redis,命中就直接返回,未命中再查 MySQL 并回写 Redis。
这套流程在读多写少的场景下工作得很好,但只要数据库里的数据被更新,问题就来了:
数据库里的数据变了,Redis 里的缓存还是旧的,怎么办?
这就是缓存一致性问题。它的本质是:同一份数据存在两个副本(数据库一份、缓存一份),更新其中一份时另一份怎么跟着变。这是一个典型的双写问题,只要引入了缓存层就必然存在,区别只在于"如何把不一致的时间窗口压缩到可以接受的范围"。
围绕这个问题,需要做两个决策:
- 缓存侧的操作选择 :更新数据库时,对缓存是更新 还是删除?
- 操作顺序:先操作缓存还是先操作数据库?
下面按"正常思维"逐步推演,看看每种选择会带来什么问题,最终为什么会演变成 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 回填的旧数据
理论上也存在不一致。但和方案一不一样的是,这个场景发生概率极低,原因有两个:
- 缓存写入速度远高于数据库:数据库的更新操作要走磁盘 IO、加锁、事务提交,耗时通常是毫秒级;而 Redis 的删除操作是内存操作,耗时微秒级。读请求 A 要恰好卡在"查完数据库、还没回填"这个极短的时间窗口内被切走线程,概率非常小。
- 需要恰好在这个窗口内发生线程调度: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;} 写请求到达
- 更新数据库
- 第一次删除缓存
- 睡眠 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
业务流程:
- 业务服务只负责更新 MySQL,完全不关心缓存。
- MySQL 把数据变更记录到 binlog。
- Canal 订阅 binlog,把变更事件发送到 MQ。
- MQ 消费者拿到变更事件后,执行删除缓存操作。
- 删除成功后再回 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。
- 强一致业务(如资金):分布式锁,牺牲并发换一致。
- 绝对一致业务(如对账、审计):放弃缓存,直接读写数据库。
十、总结
缓存一致性的核心矛盾是:同一份数据存在两个副本,更新其中一个时如何保证另一个跟着变。围绕这个矛盾,方案是层层递进的:
- 更新缓存策略行不通------无论是先更新缓存还是先更新数据库,并发写都会导致数据不一致。
- Cache Aside 是基础方案------改更新为删除,利用删除的幂等性解决写写并发问题。推荐"先更新数据库再删除缓存",因为不一致窗口极短。
- TTL 是兜底------给所有缓存设置 TTL,不一致最多持续到 TTL 过期。
- 延迟双删 + MQ 重试是进阶------通过第二次删除清理脏数据,MQ 保证删除操作的可靠性。sleep 时间业内通常取 500ms。
- Canal + MQ 是解耦方案------通过订阅 binlog 把缓存清理逻辑从业务代码中剥离,但带来 Canal 高可用、binlog 位点、消息乱序等运维复杂度。
- 分布式锁是强一致方案------读写互斥,牺牲并发换一致。
- 绝对一致业务放弃缓存------对账、审计、资金等零容忍场景,靠数据库本身支撑并发。
实际项目中,不要一上来就上最复杂的方案。先用 Cache Aside + TTL,遇到问题再加延迟双删、MQ 重试,再不行才考虑 Canal 或分布式锁。技术选型永远是够用就好,不是越复杂越好。