Go-Zero项目开发5: 数据库与缓存一致性问题及 go-zero 缓存机制解析

纲要

  • 一致性级别概述
    • 强一致性:要求各节点数据实时统一,常见于金融场景
    • 弱一致性:允许短暂不一致,适合社交、内容平台
    • 最终一致性:数据最终收敛到一致,常用于数据同步、日志统计
  • 弱一致性下的缓存更新策略
    • 更新缓存 vs 删除缓存的取舍
    • 先删除缓存再更新数据库的风险
    • 延迟双删方案及其局限性
    • 先更新数据库再删除缓存的优势与极端问题
  • 最终一致性的工程实践
    • 基于消息队列异步删除缓存
    • 基于数据库增量日志(如 canal)监听实现零侵入
  • 强一致性的保障手段
    • 串行化请求(如 Kafka 顺序消息)
    • 分布式锁(读/写共用锁)
    • 开关控制(类似分布式锁的变体)
  • go-zero 缓存机制源码分析
    • 模型层缓存配置与对象创建
    • 读操作流程:FindOne 如何利用缓存
    • 写操作流程:Update 如何先写库再删缓存
    • 内部分析:CacheNodeTakeDelCache
  • 与项目结合:用户服务的缓存实践

引言

在前面的文章中,我们完成了用户服务的注册、登录、搜索等功能,并通过 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 方法,其内部逻辑:

  1. 从缓存读取数据。
  2. 如果命中,直接返回结果。
  3. 如果 miss,调用传入的 queryFn 从数据库加载数据。
  4. 将数据库结果写入缓存,并设置过期时间。
  5. 返回数据。

这符合"读时回填"的策略,避免了启动时全量缓存预热。

写操作:UpdateDelete 流程

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 还是集群模式,并提供统一的 TakeSetDel 接口。读操作为防缓存击穿,可能会添加短暂的占位符(null 缓存);写操作失败则直接返回错误,不会删除缓存,保证缓存可用。

与项目结合:用户服务的缓存实践

在我们的即时通讯系统中,用户信息会被频繁查询(验证好友、展示资料等),非常适合缓存。按照 go-zero 的默认配置,只要在 user.yaml 中正确配置了 CacheRedis,模型层就会自动携带缓存逻辑:

yaml 复制代码
CacheRedis:
	Host: 192.168.1.10:6379 
	Pass: "yourpassword"

注册时执行 Insertgo-zero 会直接写入数据库但不操作缓存(因为 Insert 没有对应的缓存 key)。首次 FindOneFindOneByPhone 时,会从数据库加载并写入缓存,后续读取将命中 Redis,大幅降低数据库压力。

更新用户资料时,Update 方法会删除对应 uid 的缓存 key,保证下次读取能拿到最新数据。整个过程对业务代码完全透明,只需专注逻辑实现。

总结

  • 理解了强一致性、弱一致性、最终一致性的适用场景。
  • 分析了删除缓存策略下"先删后更"与"先更后删"的优劣,以及延迟双删、重试机制的作用。
  • 了解了通过消息队列和 binlog 订阅实现最终一致性的工程手段。
  • 深入 go-zero 模型缓存源码,明确了其读写分离的缓存策略:读时填充、写时失效
  • 结合用户服务,验证了这套机制在微服务中的实践价值。

掌握这些理论,可以更自信地在复杂业务中平衡性能与一致性,避免因缓存使用不当引发的数据问题。在后续的社交与 IM 服务中,我们将继续沿用这套缓存体系,并探讨高并发下的更多优化技巧。

相关推荐
布朗克1686 小时前
Go 入门到精通-18-文件操作
java·数据库·golang
ShoreKiten6 小时前
SQL注入之SQL Server增删改查
数据库·sql
Mico186 小时前
MySQL 8.0.35 源码编译安装笔记
数据库·笔记·mysql
蚰蜒螟6 小时前
从 Jedis 到内核:深入剖析 Redis BRPOP 的阻塞与唤醒机制
数据库·redis·缓存
脱胎换骨-军哥6 小时前
C++ 与 Go 生成质量比拼,谁是分布式主力语言
c++·分布式·golang
怒放de生命20107 小时前
【web3基础】go-zero创建rpc服务,创建配置文件(二)
开发语言·rpc·golang·web3
微露清风7 小时前
浅析 MySQL 索引
android·数据库·mysql
沈浩(种子思维作者)7 小时前
心理的研究,我从何来?
数据库·人工智能·量子计算
糖果店的幽灵7 小时前
【langgraph 从入门到精通graphApi 篇】节点与边 —— 图的骨架
java·数据库·人工智能·redis·langgraph