纲要
- 一致性级别概述
- 强一致性:要求各节点数据实时统一,常见于金融场景
- 弱一致性:允许短暂不一致,适合社交、内容平台
- 最终一致性:数据最终收敛到一致,常用于数据同步、日志统计
- 弱一致性下的缓存更新策略
- 更新缓存 vs 删除缓存的取舍
- 先删除缓存再更新数据库的风险
- 延迟双删方案及其局限性
- 先更新数据库再删除缓存的优势与极端问题
- 最终一致性的工程实践
- 基于消息队列异步删除缓存
- 基于数据库增量日志(如
canal)监听实现零侵入
- 强一致性的保障手段
- 串行化请求(如 Kafka 顺序消息)
- 分布式锁(读/写共用锁)
- 开关控制(类似分布式锁的变体)
go-zero缓存机制源码分析- 模型层缓存配置与对象创建
- 读操作流程:
FindOne如何利用缓存 - 写操作流程:
Update如何先写库再删缓存 - 内部分析:
CacheNode的Take与DelCache
- 与项目结合:用户服务的缓存实践
引言
在前面的文章中,我们完成了用户服务的注册、登录、搜索等功能,并通过 goctl model 生成了数据模型。生成代码会默认引入 go-zero 的缓存层,以 redis 作为媒介缓存数据库查询结果。这自然引出一个关键问题:缓存与数据库之间如何维持一致性? 本文将从理论到实践,分析常见的一致性方案,并结合 go-zero@latest 源码探究其内部缓存机制的设计思想。
一致性级别概述
分布式中,缓存与数据库的一致性可分为三个级别:
| 级别 | 特点 | 典型场景 |
|---|---|---|
| 强一致性 | 任意时刻所有节点看到的数据完全相同 | 金融交易、在线支付 |
| 弱一致性 | 允许短暂的不一致,不同线程可能看到新旧数据 | 社交动态、内容展示 |
| 最终一致性 | 数据随时间推移最终收敛到一致 | 日志统计、异步同步 |
互联网业务大多采用弱一致性或最终一致性,权衡性能与数据准确性。
弱一致性下的缓存更新策略
缓存更新核心围绕两个动作:更新缓存 与 删除缓存。两者优劣对比如下:
| 策略 | 优点 | 缺点 |
|---|---|---|
| 更新缓存 | 命中率高,减少数据库压力 | 并发写入容易造成数据覆盖,复杂度高 |
| 删除缓存 | 实现简单,避免并发写冲突 | 可能造成缓存击穿,增大数据库瞬时压力 |
实际业务中更倾向删除缓存,本文后续策略均基于此。
先删除缓存,再更新数据库
DB Cache 线程2 线程1 DB Cache 线程2 线程1 #mermaid-svg-Onfn9HCWLUMewz1T{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-Onfn9HCWLUMewz1T .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-Onfn9HCWLUMewz1T .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-Onfn9HCWLUMewz1T .error-icon{fill:#552222;}#mermaid-svg-Onfn9HCWLUMewz1T .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-Onfn9HCWLUMewz1T .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-Onfn9HCWLUMewz1T .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-Onfn9HCWLUMewz1T .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-Onfn9HCWLUMewz1T .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-Onfn9HCWLUMewz1T .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-Onfn9HCWLUMewz1T .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-Onfn9HCWLUMewz1T .marker{fill:#333333;stroke:#333333;}#mermaid-svg-Onfn9HCWLUMewz1T .marker.cross{stroke:#333333;}#mermaid-svg-Onfn9HCWLUMewz1T svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-Onfn9HCWLUMewz1T p{margin:0;}#mermaid-svg-Onfn9HCWLUMewz1T .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-Onfn9HCWLUMewz1T text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-Onfn9HCWLUMewz1T .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-Onfn9HCWLUMewz1T .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-Onfn9HCWLUMewz1T .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-Onfn9HCWLUMewz1T .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-Onfn9HCWLUMewz1T #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-Onfn9HCWLUMewz1T .sequenceNumber{fill:white;}#mermaid-svg-Onfn9HCWLUMewz1T #sequencenumber{fill:#333;}#mermaid-svg-Onfn9HCWLUMewz1T #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-Onfn9HCWLUMewz1T .messageText{fill:#333;stroke:none;}#mermaid-svg-Onfn9HCWLUMewz1T .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-Onfn9HCWLUMewz1T .labelText,#mermaid-svg-Onfn9HCWLUMewz1T .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-Onfn9HCWLUMewz1T .loopText,#mermaid-svg-Onfn9HCWLUMewz1T .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-Onfn9HCWLUMewz1T .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-Onfn9HCWLUMewz1T .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-Onfn9HCWLUMewz1T .noteText,#mermaid-svg-Onfn9HCWLUMewz1T .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-Onfn9HCWLUMewz1T .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-Onfn9HCWLUMewz1T .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-Onfn9HCWLUMewz1T .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-Onfn9HCWLUMewz1T .actorPopupMenu{position:absolute;}#mermaid-svg-Onfn9HCWLUMewz1T .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-Onfn9HCWLUMewz1T .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-Onfn9HCWLUMewz1T .actor-man circle,#mermaid-svg-Onfn9HCWLUMewz1T line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-Onfn9HCWLUMewz1T :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 开始更新数据库(可能延时/失败) 缓存中仍是旧数据,不一致! 删除缓存查询缓存(未命中)读取旧数据回写旧数据更新数据库为新值
问题:在线程1更新完成前,线程2可能将旧数据回填至缓存,导致脏数据长期存在。
改进方案:延迟双删
线程1在更新数据库后,休眠一段时间,再执行一次缓存删除。
DB Cache 线程2 线程1 DB Cache 线程2 线程1 #mermaid-svg-6IvMM8bq9AV5siPj{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-6IvMM8bq9AV5siPj .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-6IvMM8bq9AV5siPj .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-6IvMM8bq9AV5siPj .error-icon{fill:#552222;}#mermaid-svg-6IvMM8bq9AV5siPj .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-6IvMM8bq9AV5siPj .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-6IvMM8bq9AV5siPj .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-6IvMM8bq9AV5siPj .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-6IvMM8bq9AV5siPj .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-6IvMM8bq9AV5siPj .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-6IvMM8bq9AV5siPj .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-6IvMM8bq9AV5siPj .marker{fill:#333333;stroke:#333333;}#mermaid-svg-6IvMM8bq9AV5siPj .marker.cross{stroke:#333333;}#mermaid-svg-6IvMM8bq9AV5siPj svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-6IvMM8bq9AV5siPj p{margin:0;}#mermaid-svg-6IvMM8bq9AV5siPj .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-6IvMM8bq9AV5siPj text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-6IvMM8bq9AV5siPj .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-6IvMM8bq9AV5siPj .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-6IvMM8bq9AV5siPj .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-6IvMM8bq9AV5siPj .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-6IvMM8bq9AV5siPj #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-6IvMM8bq9AV5siPj .sequenceNumber{fill:white;}#mermaid-svg-6IvMM8bq9AV5siPj #sequencenumber{fill:#333;}#mermaid-svg-6IvMM8bq9AV5siPj #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-6IvMM8bq9AV5siPj .messageText{fill:#333;stroke:none;}#mermaid-svg-6IvMM8bq9AV5siPj .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-6IvMM8bq9AV5siPj .labelText,#mermaid-svg-6IvMM8bq9AV5siPj .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-6IvMM8bq9AV5siPj .loopText,#mermaid-svg-6IvMM8bq9AV5siPj .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-6IvMM8bq9AV5siPj .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-6IvMM8bq9AV5siPj .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-6IvMM8bq9AV5siPj .noteText,#mermaid-svg-6IvMM8bq9AV5siPj .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-6IvMM8bq9AV5siPj .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-6IvMM8bq9AV5siPj .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-6IvMM8bq9AV5siPj .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-6IvMM8bq9AV5siPj .actorPopupMenu{position:absolute;}#mermaid-svg-6IvMM8bq9AV5siPj .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-6IvMM8bq9AV5siPj .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-6IvMM8bq9AV5siPj .actor-man circle,#mermaid-svg-6IvMM8bq9AV5siPj line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-6IvMM8bq9AV5siPj :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 休眠 N 毫秒 保证后续请求读取新数据 删除缓存更新数据库再次删除缓存
该方案仍可能因第二次删除失败而残留旧数据,通常配合重试机制,此时已偏向最终一致性。
先更新数据库,再删除缓存
Cache DB 线程2 线程1 Cache DB 线程2 线程1 #mermaid-svg-uQK5Oh7ck2sqw249{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-uQK5Oh7ck2sqw249 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-uQK5Oh7ck2sqw249 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-uQK5Oh7ck2sqw249 .error-icon{fill:#552222;}#mermaid-svg-uQK5Oh7ck2sqw249 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-uQK5Oh7ck2sqw249 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-uQK5Oh7ck2sqw249 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-uQK5Oh7ck2sqw249 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-uQK5Oh7ck2sqw249 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-uQK5Oh7ck2sqw249 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-uQK5Oh7ck2sqw249 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-uQK5Oh7ck2sqw249 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-uQK5Oh7ck2sqw249 .marker.cross{stroke:#333333;}#mermaid-svg-uQK5Oh7ck2sqw249 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-uQK5Oh7ck2sqw249 p{margin:0;}#mermaid-svg-uQK5Oh7ck2sqw249 .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-uQK5Oh7ck2sqw249 text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-uQK5Oh7ck2sqw249 .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-uQK5Oh7ck2sqw249 .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-uQK5Oh7ck2sqw249 .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-uQK5Oh7ck2sqw249 .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-uQK5Oh7ck2sqw249 #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-uQK5Oh7ck2sqw249 .sequenceNumber{fill:white;}#mermaid-svg-uQK5Oh7ck2sqw249 #sequencenumber{fill:#333;}#mermaid-svg-uQK5Oh7ck2sqw249 #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-uQK5Oh7ck2sqw249 .messageText{fill:#333;stroke:none;}#mermaid-svg-uQK5Oh7ck2sqw249 .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-uQK5Oh7ck2sqw249 .labelText,#mermaid-svg-uQK5Oh7ck2sqw249 .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-uQK5Oh7ck2sqw249 .loopText,#mermaid-svg-uQK5Oh7ck2sqw249 .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-uQK5Oh7ck2sqw249 .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-uQK5Oh7ck2sqw249 .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-uQK5Oh7ck2sqw249 .noteText,#mermaid-svg-uQK5Oh7ck2sqw249 .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-uQK5Oh7ck2sqw249 .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-uQK5Oh7ck2sqw249 .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-uQK5Oh7ck2sqw249 .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-uQK5Oh7ck2sqw249 .actorPopupMenu{position:absolute;}#mermaid-svg-uQK5Oh7ck2sqw249 .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-uQK5Oh7ck2sqw249 .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-uQK5Oh7ck2sqw249 .actor-man circle,#mermaid-svg-uQK5Oh7ck2sqw249 line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-uQK5Oh7ck2sqw249 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 理想情况:线程2在删除后访问,未命中,读新值 更新数据库删除缓存
优势在于更新数据库和删除缓存是一个原子任务,若删除失败可内部重试,不会出现线程2回填旧数据的问题。但存在极端情况:
- 线程2 在缓存未命中时读取数据库(此时线程1 尚未更新数据库),读到了旧值。
- 线程1 更新数据库并删除缓存,但删除缓存发生在线程2 回写缓存之前。
- 线程2 将旧值回写到缓存,导致缓存为旧数据。
DB Cache 线程2 线程1 DB Cache 线程2 线程1 #mermaid-svg-pvYTlVRSkX2gmBsK{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-pvYTlVRSkX2gmBsK .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-pvYTlVRSkX2gmBsK .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-pvYTlVRSkX2gmBsK .error-icon{fill:#552222;}#mermaid-svg-pvYTlVRSkX2gmBsK .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-pvYTlVRSkX2gmBsK .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-pvYTlVRSkX2gmBsK .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-pvYTlVRSkX2gmBsK .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-pvYTlVRSkX2gmBsK .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-pvYTlVRSkX2gmBsK .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-pvYTlVRSkX2gmBsK .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-pvYTlVRSkX2gmBsK .marker{fill:#333333;stroke:#333333;}#mermaid-svg-pvYTlVRSkX2gmBsK .marker.cross{stroke:#333333;}#mermaid-svg-pvYTlVRSkX2gmBsK svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-pvYTlVRSkX2gmBsK p{margin:0;}#mermaid-svg-pvYTlVRSkX2gmBsK .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-pvYTlVRSkX2gmBsK text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-pvYTlVRSkX2gmBsK .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-pvYTlVRSkX2gmBsK .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-pvYTlVRSkX2gmBsK .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-pvYTlVRSkX2gmBsK .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-pvYTlVRSkX2gmBsK #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-pvYTlVRSkX2gmBsK .sequenceNumber{fill:white;}#mermaid-svg-pvYTlVRSkX2gmBsK #sequencenumber{fill:#333;}#mermaid-svg-pvYTlVRSkX2gmBsK #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-pvYTlVRSkX2gmBsK .messageText{fill:#333;stroke:none;}#mermaid-svg-pvYTlVRSkX2gmBsK .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-pvYTlVRSkX2gmBsK .labelText,#mermaid-svg-pvYTlVRSkX2gmBsK .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-pvYTlVRSkX2gmBsK .loopText,#mermaid-svg-pvYTlVRSkX2gmBsK .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-pvYTlVRSkX2gmBsK .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-pvYTlVRSkX2gmBsK .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-pvYTlVRSkX2gmBsK .noteText,#mermaid-svg-pvYTlVRSkX2gmBsK .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-pvYTlVRSkX2gmBsK .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-pvYTlVRSkX2gmBsK .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-pvYTlVRSkX2gmBsK .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-pvYTlVRSkX2gmBsK .actorPopupMenu{position:absolute;}#mermaid-svg-pvYTlVRSkX2gmBsK .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-pvYTlVRSkX2gmBsK .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-pvYTlVRSkX2gmBsK .actor-man circle,#mermaid-svg-pvYTlVRSkX2gmBsK line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-pvYTlVRSkX2gmBsK :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 缓存又被重置为旧值 查询缓存(未命中)读取旧数据更新数据库删除缓存回写旧数据
该场景严格条件下才会出现,但概率较低。实践中多采用先更新数据库再删除缓存,配合重试或消息队列。
最终一致性的工程实践
基于消息队列
缓存 异步Worker 消息队列 数据库 请求A(写) 缓存 异步Worker 消息队列 数据库 请求A(写) #mermaid-svg-5ptFmrzH7YbUuWW4{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-5ptFmrzH7YbUuWW4 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-5ptFmrzH7YbUuWW4 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-5ptFmrzH7YbUuWW4 .error-icon{fill:#552222;}#mermaid-svg-5ptFmrzH7YbUuWW4 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-5ptFmrzH7YbUuWW4 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-5ptFmrzH7YbUuWW4 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-5ptFmrzH7YbUuWW4 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-5ptFmrzH7YbUuWW4 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-5ptFmrzH7YbUuWW4 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-5ptFmrzH7YbUuWW4 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-5ptFmrzH7YbUuWW4 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-5ptFmrzH7YbUuWW4 .marker.cross{stroke:#333333;}#mermaid-svg-5ptFmrzH7YbUuWW4 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-5ptFmrzH7YbUuWW4 p{margin:0;}#mermaid-svg-5ptFmrzH7YbUuWW4 .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-5ptFmrzH7YbUuWW4 text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-5ptFmrzH7YbUuWW4 .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-5ptFmrzH7YbUuWW4 .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-5ptFmrzH7YbUuWW4 .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-5ptFmrzH7YbUuWW4 .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-5ptFmrzH7YbUuWW4 #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-5ptFmrzH7YbUuWW4 .sequenceNumber{fill:white;}#mermaid-svg-5ptFmrzH7YbUuWW4 #sequencenumber{fill:#333;}#mermaid-svg-5ptFmrzH7YbUuWW4 #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-5ptFmrzH7YbUuWW4 .messageText{fill:#333;stroke:none;}#mermaid-svg-5ptFmrzH7YbUuWW4 .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-5ptFmrzH7YbUuWW4 .labelText,#mermaid-svg-5ptFmrzH7YbUuWW4 .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-5ptFmrzH7YbUuWW4 .loopText,#mermaid-svg-5ptFmrzH7YbUuWW4 .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-5ptFmrzH7YbUuWW4 .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-5ptFmrzH7YbUuWW4 .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-5ptFmrzH7YbUuWW4 .noteText,#mermaid-svg-5ptFmrzH7YbUuWW4 .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-5ptFmrzH7YbUuWW4 .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-5ptFmrzH7YbUuWW4 .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-5ptFmrzH7YbUuWW4 .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-5ptFmrzH7YbUuWW4 .actorPopupMenu{position:absolute;}#mermaid-svg-5ptFmrzH7YbUuWW4 .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-5ptFmrzH7YbUuWW4 .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-5ptFmrzH7YbUuWW4 .actor-man circle,#mermaid-svg-5ptFmrzH7YbUuWW4 line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-5ptFmrzH7YbUuWW4 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 更新数据投递缓存删除任务消费任务删除缓存
优点:解耦写入与缓存操作,失败可重复消费。缺点:存在短暂延迟,代码有侵入(需主动发送 MQ 消息)。
基于数据库增量日志
利用 Canal 等工具监听 MySQL binlog,数据变更后自动触发缓存更新/删除,实现零业务侵入。
#mermaid-svg-GgpYXlZRmHa8uei3{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-GgpYXlZRmHa8uei3 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-GgpYXlZRmHa8uei3 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-GgpYXlZRmHa8uei3 .error-icon{fill:#552222;}#mermaid-svg-GgpYXlZRmHa8uei3 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-GgpYXlZRmHa8uei3 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-GgpYXlZRmHa8uei3 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-GgpYXlZRmHa8uei3 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-GgpYXlZRmHa8uei3 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-GgpYXlZRmHa8uei3 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-GgpYXlZRmHa8uei3 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-GgpYXlZRmHa8uei3 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-GgpYXlZRmHa8uei3 .marker.cross{stroke:#333333;}#mermaid-svg-GgpYXlZRmHa8uei3 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-GgpYXlZRmHa8uei3 p{margin:0;}#mermaid-svg-GgpYXlZRmHa8uei3 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-GgpYXlZRmHa8uei3 .cluster-label text{fill:#333;}#mermaid-svg-GgpYXlZRmHa8uei3 .cluster-label span{color:#333;}#mermaid-svg-GgpYXlZRmHa8uei3 .cluster-label span p{background-color:transparent;}#mermaid-svg-GgpYXlZRmHa8uei3 .label text,#mermaid-svg-GgpYXlZRmHa8uei3 span{fill:#333;color:#333;}#mermaid-svg-GgpYXlZRmHa8uei3 .node rect,#mermaid-svg-GgpYXlZRmHa8uei3 .node circle,#mermaid-svg-GgpYXlZRmHa8uei3 .node ellipse,#mermaid-svg-GgpYXlZRmHa8uei3 .node polygon,#mermaid-svg-GgpYXlZRmHa8uei3 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-GgpYXlZRmHa8uei3 .rough-node .label text,#mermaid-svg-GgpYXlZRmHa8uei3 .node .label text,#mermaid-svg-GgpYXlZRmHa8uei3 .image-shape .label,#mermaid-svg-GgpYXlZRmHa8uei3 .icon-shape .label{text-anchor:middle;}#mermaid-svg-GgpYXlZRmHa8uei3 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-GgpYXlZRmHa8uei3 .rough-node .label,#mermaid-svg-GgpYXlZRmHa8uei3 .node .label,#mermaid-svg-GgpYXlZRmHa8uei3 .image-shape .label,#mermaid-svg-GgpYXlZRmHa8uei3 .icon-shape .label{text-align:center;}#mermaid-svg-GgpYXlZRmHa8uei3 .node.clickable{cursor:pointer;}#mermaid-svg-GgpYXlZRmHa8uei3 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-GgpYXlZRmHa8uei3 .arrowheadPath{fill:#333333;}#mermaid-svg-GgpYXlZRmHa8uei3 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-GgpYXlZRmHa8uei3 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-GgpYXlZRmHa8uei3 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-GgpYXlZRmHa8uei3 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-GgpYXlZRmHa8uei3 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-GgpYXlZRmHa8uei3 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-GgpYXlZRmHa8uei3 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-GgpYXlZRmHa8uei3 .cluster text{fill:#333;}#mermaid-svg-GgpYXlZRmHa8uei3 .cluster span{color:#333;}#mermaid-svg-GgpYXlZRmHa8uei3 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-GgpYXlZRmHa8uei3 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-GgpYXlZRmHa8uei3 rect.text{fill:none;stroke-width:0;}#mermaid-svg-GgpYXlZRmHa8uei3 .icon-shape,#mermaid-svg-GgpYXlZRmHa8uei3 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-GgpYXlZRmHa8uei3 .icon-shape p,#mermaid-svg-GgpYXlZRmHa8uei3 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-GgpYXlZRmHa8uei3 .icon-shape .label rect,#mermaid-svg-GgpYXlZRmHa8uei3 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-GgpYXlZRmHa8uei3 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-GgpYXlZRmHa8uei3 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-GgpYXlZRmHa8uei3 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 更新/插入
binlog
解析事件
修改缓存
应用服务
MySQL
Canal 监听
缓存更新Worker
Redis
该方式解耦彻底,扩展性强,但需额外维护中间件。
强一致性的保障手段
串行化
将并行的读写请求通过消息队列(如 Kafka 分区顺序)串行执行,保证同一数据的操作有序。
分布式锁
缓存 数据库 分布式锁(Redis/ZK) 业务进程 缓存 数据库 分布式锁(Redis/ZK) 业务进程 #mermaid-svg-ozgHCvYf9pyLX3wc{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-ozgHCvYf9pyLX3wc .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-ozgHCvYf9pyLX3wc .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-ozgHCvYf9pyLX3wc .error-icon{fill:#552222;}#mermaid-svg-ozgHCvYf9pyLX3wc .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-ozgHCvYf9pyLX3wc .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-ozgHCvYf9pyLX3wc .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-ozgHCvYf9pyLX3wc .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-ozgHCvYf9pyLX3wc .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-ozgHCvYf9pyLX3wc .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-ozgHCvYf9pyLX3wc .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-ozgHCvYf9pyLX3wc .marker{fill:#333333;stroke:#333333;}#mermaid-svg-ozgHCvYf9pyLX3wc .marker.cross{stroke:#333333;}#mermaid-svg-ozgHCvYf9pyLX3wc svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-ozgHCvYf9pyLX3wc p{margin:0;}#mermaid-svg-ozgHCvYf9pyLX3wc .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-ozgHCvYf9pyLX3wc text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-ozgHCvYf9pyLX3wc .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-ozgHCvYf9pyLX3wc .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-ozgHCvYf9pyLX3wc .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-ozgHCvYf9pyLX3wc .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-ozgHCvYf9pyLX3wc #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-ozgHCvYf9pyLX3wc .sequenceNumber{fill:white;}#mermaid-svg-ozgHCvYf9pyLX3wc #sequencenumber{fill:#333;}#mermaid-svg-ozgHCvYf9pyLX3wc #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-ozgHCvYf9pyLX3wc .messageText{fill:#333;stroke:none;}#mermaid-svg-ozgHCvYf9pyLX3wc .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-ozgHCvYf9pyLX3wc .labelText,#mermaid-svg-ozgHCvYf9pyLX3wc .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-ozgHCvYf9pyLX3wc .loopText,#mermaid-svg-ozgHCvYf9pyLX3wc .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-ozgHCvYf9pyLX3wc .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-ozgHCvYf9pyLX3wc .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-ozgHCvYf9pyLX3wc .noteText,#mermaid-svg-ozgHCvYf9pyLX3wc .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-ozgHCvYf9pyLX3wc .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-ozgHCvYf9pyLX3wc .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-ozgHCvYf9pyLX3wc .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-ozgHCvYf9pyLX3wc .actorPopupMenu{position:absolute;}#mermaid-svg-ozgHCvYf9pyLX3wc .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-ozgHCvYf9pyLX3wc .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-ozgHCvYf9pyLX3wc .actor-man circle,#mermaid-svg-ozgHCvYf9pyLX3wc line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-ozgHCvYf9pyLX3wc :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} alt获取成功获取超时/失败 获取锁更新数据删除缓存释放锁快速失败或自旋重试
读写操作均需获取同一把锁,保证数据强一致,但吞吐量下降明显。
go-zero 缓存机制源码分析
回到我们项目中通过 goctl model 生成的数据模型,它内置了缓存能力。在 common/model/usermodel.go 中可以看到类似代码:
go
var (
cacheUserPrefix = "cache:user:"
)
type (
defaultUserModel struct {
table string
conn sqlx.SqlConn
cache cache.CacheNode
}
UserModel interface {
Insert(ctx context.Context, data *User) (sql.Result, error)
FindOne(ctx context.Context, uid string) (*User, error)
Update(ctx context.Context, data *User) error
Delete(ctx context.Context, uid string) error
}
)
func NewUserModel(conn sqlx.SqlConn, c cache.CacheConf) UserModel {
return &defaultUserModel{
table: "`user`",
conn: conn,
cache: cache.NewNode(conn, c, nil, nil),
}
}
这里的 cache.CacheNode 就是 go-zero 提供的缓存抽象,内部封装了 Redis 操作和数据库读取。
读操作:FindOne 流程
生成代码中 FindOne 的实现大致如下:
go
func (m *defaultUserModel) FindOne(ctx context.Context, uid string) (*User, error) {
userKey := fmt.Sprintf("%s%v", cacheUserPrefix, uid)
var resp User
err := m.cache.Take(ctx, &resp, userKey, func(conn sqlx.SqlConn, v interface{}) error {
query := fmt.Sprintf("select %s from %s where uid = ? limit 1", userRows, m.table)
return conn.QueryRowCtx(ctx, v, query, uid)
})
switch err {
case nil:
return &resp, nil
case sqlx.ErrNotFound:
return nil, ErrNotFound
default:
return nil, err
}
}
核心在于 cache.Take 方法,其内部逻辑:
- 从缓存读取数据。
- 如果命中,直接返回结果。
- 如果 miss,调用传入的
queryFn从数据库加载数据。 - 将数据库结果写入缓存,并设置过期时间。
- 返回数据。
这符合"读时回填"的策略,避免了启动时全量缓存预热。
写操作:Update 与 Delete 流程
go-zero 的更新和删除操作采用先更新数据库,再删除缓存的策略。以更新为例:
go
func (m *defaultUserModel) Update(ctx context.Context, data *User) error {
userKey := fmt.Sprintf("%s%v", cacheUserPrefix, data.Uid)
_, err := m.cache.Exec(ctx, func(conn sqlx.SqlConn) (result sql.Result, err error) {
query := fmt.Sprintf("update %s set ... where uid = ?", m.table)
return conn.ExecCtx(ctx, query, data.Uid)
}, userKey)
return err
}
Exec 方法内部首先执行数据库操作,当数据库操作成功时,调用 DelCache 删除缓存。
这种设计与我们前面分析的最佳实践一致:写操作不更新缓存,仅使其失效,下次读取时再通过 Take 自动加载新数据。
内部实现细节
CacheNode 会根据传入的 CacheConf 决定使用单节点 Redis 还是集群模式,并提供统一的 Take、Set、Del 接口。读操作为防缓存击穿,可能会添加短暂的占位符(null 缓存);写操作失败则直接返回错误,不会删除缓存,保证缓存可用。
与项目结合:用户服务的缓存实践
在我们的即时通讯系统中,用户信息会被频繁查询(验证好友、展示资料等),非常适合缓存。按照 go-zero 的默认配置,只要在 user.yaml 中正确配置了 CacheRedis,模型层就会自动携带缓存逻辑:
yaml
CacheRedis:
Host: 192.168.1.10:6379
Pass: "yourpassword"
注册时执行 Insert,go-zero 会直接写入数据库但不操作缓存(因为 Insert 没有对应的缓存 key)。首次 FindOne 或 FindOneByPhone 时,会从数据库加载并写入缓存,后续读取将命中 Redis,大幅降低数据库压力。
更新用户资料时,Update 方法会删除对应 uid 的缓存 key,保证下次读取能拿到最新数据。整个过程对业务代码完全透明,只需专注逻辑实现。
总结
- 理解了强一致性、弱一致性、最终一致性的适用场景。
- 分析了删除缓存策略下"先删后更"与"先更后删"的优劣,以及延迟双删、重试机制的作用。
- 了解了通过消息队列和
binlog订阅实现最终一致性的工程手段。 - 深入
go-zero模型缓存源码,明确了其读写分离的缓存策略:读时填充、写时失效。 - 结合用户服务,验证了这套机制在微服务中的实践价值。
掌握这些理论,可以更自信地在复杂业务中平衡性能与一致性,避免因缓存使用不当引发的数据问题。在后续的社交与 IM 服务中,我们将继续沿用这套缓存体系,并探讨高并发下的更多优化技巧。