老王开了一家面馆,生意好到爆棚。为了让顾客不用每次跑到后厨问“今天有什么面”,他在前台挂了一块小黑板,写上招牌菜和价格——顾客抬眼就能看到,点餐速度快了十倍。这块小黑板,就是计算机里的缓存;后厨那本记录食材库存与价格的台账,就是源数据库。可问题随之而来:当番茄涨价、牛腩卖完,台账改了而小黑板没改,顾客就会指着黑板质问:“上面明明写 15 块,结账怎么变 18 块?”这就是缓存一致性问题的雏形:缓存里的副本数据,如何与源数据保持同步。 AI 推理服务同样被困在这个问题里:大模型输出结果、中间特征、热点请求的答案,每次重新计算既费时又费算力;但一旦模型更新、业务规则调整,旧缓存立刻变成“过期答案”。本篇文章用写穿透、写回、失效三种策略,拆解“源变之后缓存怎么跟上”的经典命题,再延伸到 Redis、CDN 与模型输出缓存构成的多级架构,帮你搭建一套又快又不“货不对板”的缓存体系。

岗位关联

掌握缓存一致性策略,是设计高吞吐、低延迟 AI 推理服务的必修课。它能让你在“数据新鲜度”与“响应速度”之间做出有理有据的工程决策,直接决定线上服务的稳定性与用户体验。

目录
  1. 可视化演示
  2. 缓存一致性基础
  3. 写穿透策略
  4. 写回策略
  5. 失效策略
  6. 多级缓存协同
  7. 延迟一致性权衡
  8. 实践选型策略
  9. AI 应用场景
  10. Python 演示
  11. 常见误区
  12. 面试题

可视化演示

动态展示写穿透/写回/写失效/写广播四种缓存一致性策略下,写操作与缓存状态(MESI)在多缓存节点间的传播过程,切换策略观察差异。

本节配有交互动效(进入视口后自动加载,约 3KB)。

缓存一致性基础

老王面馆的生意越来越红火,他发现顾客点餐太慢了——每个人都得把服务员拉到后厨门口问“今天还有牛肉吗?”于是他挂出前台小黑板,把菜品和价格写上去,顾客抬眼就能看到,点餐速度快了十倍。这块小黑板,就是计算机世界里的“缓存”。缓存的本质是“副本”:它复制源数据的一部分,放在离读者更近的地方,用空间换时间。

但副本有个天生的隐患:它会“漂移”。小黑板上的价格是台账的快照,只要台账变了而黑板没变,两者就对不上。计算机系统里同理:数据库是权威源,缓存是它的快照,源数据一更新,快照就成了旧数据。所谓缓存一致性,就是一套保证“副本跟上源”的规则。规则越严,数据越新鲜,但往往越慢;规则越松,性能越好,但可能读到旧值。这就是强一致与弱一致的分野。

为什么缓存值得如此大费周章?因为它带来的收益可以量化。设缓存命中率为 H(0 到 1),缓存读延迟为 T_cache,数据库读延迟为 T_DB,则平均读延迟为:

小黑板让九成顾客抬头就能看到答案(命中),只有一成顾客需要去后厨问(未命中回源)——平均点餐时间自然大幅缩短。但正如老王很快会发现的:黑板上的内容如果不更新,光有速度没有准确,迟早要出乱子。下一节,我们看老王如何应对“价格变了”这件事。

Tread=H×Tcache+(1−H)×TDB\displaystyle T_{\text{read}} = H \times T_{\text{cache}} + (1 - H) \times T_{\text{DB}}
命中率越高,平均读延迟越接近缓存延迟;命中率越低,越接近数据库延迟。这就是缓存价值的量化表达。
  1. 平均读延迟 T_read每次读请求从发出到拿到数据的平均耗时,是衡量缓存收益的核心指标。
    Tread\displaystyle T_{\text{read}}
  2. 命中率 H读请求在缓存中找到有效数据的比例,取值 0 到 1。H 越接近 1,说明缓存越“有用”。
    H∈[0,1]\displaystyle H \in [0, 1]
  3. 命中时的开销 T_cache命中时只需访问内存或 Redis,通常几十微秒到几毫秒,远小于数据库延迟。
    Tcache≪TDB\displaystyle T_{\text{cache}} \ll T_{\text{DB}}
  4. 未命中回源开销 T_DB未命中时需回源数据库,通常几毫秒到几十毫秒,伴随网络与磁盘 IO 开销。
    (1−H)×TDB\displaystyle (1 - H) \times T_{\text{DB}}

写穿透策略

有一天番茄批发价上涨,老王赶紧拿起粉笔,一边在后厨台账上记下“番茄面 18 元”,一边跑到前台把小黑板改成“番茄面 18 元”。两道工序一气呵成,顾客看到的永远是最新价格。这就是写穿透(Write-Through):每一次写操作都“穿透”缓存直达数据库,两边同步更新,缺一不可。

写穿透的执行流非常直观:应用先更新缓存,再更新数据库(或反过来),两个写都必须成功才算写完成。因为每一步写都是同步的,读方永远只能读到最新值——这是它最大的优点:强一致。但代价也摆在台面上:本该一次完成的写,现在要写两个地方。设缓存写延迟为 T_cache,数据库写延迟为 T_DB,则一次写操作的总延迟为:

老王每次调价都要“前台黑板 + 后厨台账”跑两趟,价格倒是永远准确了,可一天调十次价,他就得跑二十趟。写穿透的强一致,是用写路径的加倍延迟换来的——适合写少读多、对数据新鲜度近乎苛刻的场景,比如价格表、配置中心、权限名单。

Twrite=Tcache+TDB\displaystyle T_{\text{write}} = T_{\text{cache}} + T_{\text{DB}}
写延迟是两者之和:每笔写都要等最慢的那个存储完成。写频率越高,额外开销越明显。
  1. 写操作总延迟 T_write从应用发起写到缓存和数据库都确认收到的总耗时。
    Twrite\displaystyle T_{\text{write}}
  2. 缓存写延迟 T_cache写入 Redis 或本地缓存的耗时,通常很快(亚毫秒级)。
    Tcache\displaystyle T_{\text{cache}}
  3. 数据库写延迟 T_DB写入数据库并返回确认的耗时,涉及磁盘落盘与索引更新,通常较慢。
    TDB\displaystyle T_{\text{DB}}
  4. 求和关系两个写是同步完成的,总延迟是两者相加,而不是取最大值或平均值。
    Twrite=Tcache+TDB\displaystyle T_{\text{write}} = T_{\text{cache}} + T_{\text{DB}}

写回策略

老王后来学会偷懒了:顾客在柜台问“有没有新上的番茄牛腩面?”他先在黑板上把菜名加上,客人开开心心点单,等到晚上打烊,伙计才把黑板上新增的、改动的菜品,一股脑儿抄回后厨台账。这就是写回(Write-Back):先写缓存、快速响应,回头再异步批量同步到数据库。

写回策略中,每次写操作只更新缓存,并把这个 key 标记为“脏”(dirty)。系统在合适的时机——定时、攒够一批、或系统空闲时——把脏数据批量回写数据库。注意:在回写完成之前,数据库里还是旧值。所以它保证的是一致性中最温和的一档:最终一致。只要给足时间,数据终会一致,但中间存在一个“滞后窗口”。设一批共 n 个写请求,数据库批量写的开销被摊薄,则每个写请求的平均延迟约为:

写回让老王的“写”变得飞快——先写黑板,客人不等待;至于台账,打烊后统一补记。唯一的隐患是:万一打烊前小店失火,黑板上的新菜名就跟着灰飞烟灭了。在系统里,这个“失火”就是缓存节点宕机、进程崩溃。所以写回适合能容忍少量数据丢失、写频率极高的场景,比如访问日志、浏览计数、用户行为特征。

Twrite≈Tcache+TDBn\displaystyle T_{\text{write}} \approx T_{\text{cache}} + \frac{T_{\text{DB}}}{n}
当 n 足够大时,数据库写成本被摊薄到可忽略,写路径延迟几乎等于缓存写延迟。代价是:回写前若系统崩溃,这部分数据可能丢失。
  1. 平均写延迟 T_write把 n 个写请求的总开销均摊到每个请求上得到的结果。
    Twrite\displaystyle T_{\text{write}}
  2. 批量大小 n一次回写合并的写请求数量,n 越大,摊薄效果越明显。
    TDBn\displaystyle \frac{T_{\text{DB}}}{n}
  3. 缓存写延迟 T_cache写请求先落缓存,这一步是同步的,每个请求至少要付这个成本。
    Tcache\displaystyle T_{\text{cache}}
  4. 最终一致收敛回写完成后数据库与缓存收敛到相同值,收敛前存在一个一致性“空白期”。
    DB(t)=Cache,t≥tflush\displaystyle \text{DB}(t) = \text{Cache}, \quad t \ge t_{\text{flush}}

失效策略

老王的第三招最巧妙:当得知牛腩卖完时,他并不急着在小黑板上写“牛腩面 25 元”——反正也不一定有人问——而是直接把“牛腩面”三个字擦掉。顾客问起,伙计再去后厨问师傅,问到了就顺手把新价格写回黑板。这个“先擦掉、问到了再写”的路数,就是失效策略(Invalidation / Cache-Aside)。

失效策略的核心动作是“删”而不是“写”:当源数据更新时,系统主动删除缓存中对应的旧值(或通过消息通知各副本删除)。下次读请求发现缓存里没有这个 key(缓存未命中),就会回源数据库拉取最新值,并重新填充缓存。这个“读时发现缺失才去源拉取”的机制叫懒加载(Lazy Loading)。失效策略常与 TTL(Time To Live,存活时间)搭配:即使没收到删除通知,key 到期也会自动失效。假设源更新频率为 λ,缓存 TTL 设为 T,那么 T 时间内源被更新的概率——也就是缓存变陈的概率为:

“擦掉”比“改价”轻松多了——老王不用每次调价都跑前台,只有顾客真问起那道菜时才需要回后厨确认。对“读多写少”的面馆——大多数客人只点那几样招牌面——这一招既省力气又不会错得太离谱,成了老王最常用的策略。AI 推理缓存中的模型输出快照,也常靠这种“擦掉 + 回源”来应对模型更新。

Pstale(T)=1−e−λT\displaystyle P_{\text{stale}}(T) = 1 - e^{-\lambda T}
TTL 越长,数据变陈的概率越高;TTL 越短,缓存越新鲜但命中率越低。失效策略把一致性的成本从“每次写”转移到了“偶尔的读”上。
  1. 变陈概率 P_stale缓存中的数据在 TTL 窗口内已经与源不一致的概率。
    Pstale(T)\displaystyle P_{\text{stale}}(T)
  2. 源更新速率 λ单位时间内源数据被修改的次数,λ 越大,同样的 TTL 下数据越容易变陈。
    λ\displaystyle \lambda
  3. TTL 时长 T缓存条目允许存活的时间,到期后自动失效。
    T\displaystyle T
  4. 指数项 e^{-λT}在泊松更新假设下,T 时间内源恰好一次也没被更新的概率为 e^{-λT},所以变陈概率是 1 减去它。
    e−λT\displaystyle e^{-\lambda T}

多级缓存协同

生意越做越大,老王发现一块小黑板不够用了:熟客进门就问价,街坊路过要看招牌,外地游客得先看到街口的大广告牌才愿意进店。于是他把菜单抄了三份:伙计兜里的速记条(最快最贴身)、前台小黑板(给进店顾客看)、街口大广告牌(给整条街看)。三份副本各有各的速度和更新频率——这就是多级缓存。

在 AI 推理服务中,多级缓存对应三层:最靠近计算的是本地内存缓存(L1,进程内,微秒级);中间是 Redis 分布式缓存(L2,毫秒级,服务间共享,承接热点请求);最外层是 CDN 边缘缓存(L3,把响应缓存到离用户最近的节点)。此外,模型输出本身也是一种特殊缓存:相同或相似 prompt 的推理结果直接复用,避免重复计算。设 L1 命中率为 h₁,延迟为 T₁;L1 未命中时查 L2,命中率为 h₂,延迟为 T₂;两级都未命中才回源数据库,延迟为 T_DB,则平均读延迟为:

熟客进店直接报菜名,伙计兜里的速记条一翻就有(L1 命中);新客看小黑板(L2 命中);只有极少数人站在街口看完广告牌还要进店问东问西(回源数据库)。现实系统中,各级缓存的失效通知和 TTL 必须分层设计:L1 靠进程内消息或极短 TTL,L2 靠 Redis 主动失效,L3 靠 URL 版本号切换——才能既快又不至于错得离谱。

Tavg=h1T1+(1−h1)h2T2+(1−h1)(1−h2)TDB\displaystyle T_{\text{avg}} = h_1 T_1 + (1 - h_1) h_2 T_2 + (1 - h_1)(1 - h_2) T_{\text{DB}}
每一级缓存都在“拦截”上一级漏掉的请求。命中率越高,请求越早被廉价的一级接住,整体延迟越低。
  1. 第一项 h₁T₁请求在 L1 命中的概率乘以其延迟,对应“本地直接命中”的路径。
    h1T1\displaystyle h_1 T_1
  2. 第二项 (1-h₁)h₂T₂L1 未命中但 L2 命中的概率乘以其延迟,对应“回源到 Redis”的路径。
    (1−h1)h2T2\displaystyle (1 - h_1) h_2 T_2
  3. 第三项 (1-h₁)(1-h₂)T_DB两级都未命中才回源数据库的概率乘以其延迟,这是最贵但占比最小的路径。
    (1−h1)(1−h2)TDB\displaystyle (1 - h_1)(1 - h_2) T_{\text{DB}}
  4. 概率归一关系三条互斥路径的概率之和为 1,故总延迟是加权和而非简单平均。
    h1+(1−h1)h2+(1−h1)(1−h2)=1\displaystyle h_1 + (1 - h_1) h_2 + (1 - h_1)(1 - h_2) = 1

延迟一致性权衡

老王渐渐悟出一个道理:面和价,不可兼得。要想黑板上的价格永远最新,就得频繁跑前台改(写穿透,慢);要想改价飞快,台账就必然滞后一阵子(写回,有窗口);想省事只靠擦掉旧价,就得接受偶尔有顾客在柜台扑空(失效,读时回源)。每一种策略,都是老王在“速度”与“新鲜”之间选的平衡点。

工程上的权衡可以用一个代价模型刻画。设缓存条目的 TTL 为 T,源更新速率为 λ。TTL 越长,缓存命中率越高、平均读延迟越低;但缓存变陈的概率也越高,越可能返回过期数据。把“变陈的代价”记为 C_stale(返回过期数据导致的损失),把“缓存未命中回源的代价”记为 C_miss(多花的延迟与算力),则单位时间内的综合代价为:

老王最终定的规矩是:招牌面(卖得最多、改价最少)用长 TTL 加失效策略,几乎天天在小黑板上有货;时令菜(价格天天变)干脆不写黑板,客人问了伙计再去后厨问。把“一致性等级”按菜品(业务)的容忍度分开管理,才是老王的真正智慧——这也是生产系统中“分级缓存一致性”的朴素原型。

C(T)=Cstale⋅(1−e−λT)+Cmiss⋅e−λT\displaystyle C(T) = C_{\text{stale}} \cdot (1 - e^{-\lambda T}) + C_{\text{miss}} \cdot e^{-\lambda T}
当过期代价远大于回源代价时(如金融交易),应把 TTL 调小甚至关缓存;当回源代价远大于过期代价时(如大模型推理),应把 TTL 调大。工程上按业务容忍度选策略,而不是盲目追求“强一致”。
  1. 综合代价 C(T)以 T 为 TTL 时,单位时间“过期损失 + 回源开销”的期望和。
    C(T)\displaystyle C(T)
  2. 过期损失项变陈概率 (1-e^{-λT}) 乘以其代价 C_stale,衡量“返回旧数据”造成的伤害。
    Cstale⋅(1−e−λT)\displaystyle C_{\text{stale}} \cdot (1 - e^{-\lambda T})
  3. 回源开销项恰好没变陈、到期失效回源的概率 e^{-λT} 乘以回源代价 C_miss。
    Cmiss⋅e−λT\displaystyle C_{\text{miss}} \cdot e^{-\lambda T}
  4. 此消彼长的关系两项概率相加恒为 1,TTL 增大时第一项增大、第二项减小,导数为零处对应最优 TTL。
    dCdT=λe−λT(Cstale−Cmiss)\displaystyle \frac{dC}{dT} = \lambda e^{-\lambda T} (C_{\text{stale}} - C_{\text{miss}})

实践选型策略

经营半年,老王把自己的经验写成了一本《面馆经营手册》:什么样的菜适合写黑板、什么样的菜适合打烊再记账、什么样的菜干脆不写。选型不是拍脑袋,而是看“问的人多不多、改价勤不勤”。计算机系统里的缓存策略选型,遵循同样朴素的逻辑。

缓存策略选型的核心输入是读写比例 θ = R/W(读次数 R 与写次数 W 之比)和对数据新鲜度的要求。经验法则是:写多读少(θ 很小)时,写回策略能让写路径足够快;读写均衡时,写穿透保证强一致;读多写少(θ 很大)时,失效策略加 TTL 最划算,因为写次数少,失效通知的成本可以被忽略。选择规则可写成:

《面馆经营手册》最后一条写着:世上没有永远最好的策略,只有最适合当下客流的策略。缓存选型亦然——上线前要压测读写比,上线后要监控命中率与变陈率。当 AI 模型版本更新、业务规则调整时,还要记得像老王换菜牌一样,主动把旧缓存全部擦掉。

θ=RW,策略={写回,θ<θ1写穿透,θ1≤θ≤θ2失效 + TTL,θ>θ2\displaystyle\theta = \frac{R}{W}, \qquad\text{策略} = \begin{cases}\text{写回}, & \theta < \theta_1 \\[4pt]\text{写穿透}, & \theta_1 \le \theta \le \theta_2 \\[4pt]\text{失效 + TTL}, & \theta > \theta_2\end{cases}
阈值 θ₁、θ₂ 是经验参数,需结合延迟预算与一致性要求标定。AI 推理服务通常落在 θ > θ₂ 区间:推理结果读多写少,失效策略配合语义缓存命中率优化是主流方案。
  1. 读写比 θ单位时间内读请求数除以写请求数,刻画请求的整体形态。
    θ=RW\displaystyle \theta = \frac{R}{W}
  2. 写回区间 θ < θ₁写占主导时优先保证写路径延迟,接受最终一致。
    写回,θ<θ1\displaystyle \text{写回}, \quad \theta < \theta_1
  3. 写穿透区间读写均衡且对新鲜度要求高时,用强一致换确定性。
    θ1≤θ≤θ2\displaystyle \theta_1 \le \theta \le \theta_2
  4. 失效区间 θ > θ₂读占绝对主导时,用“删 + 回源”把一致性成本降到最低。
    失效 + TTL,θ>θ2\displaystyle \text{失效 + TTL}, \quad \theta > \theta_2

AI 应用场景

大模型语义缓存服务

阶段:在线推理加速

解决的问题:相同或相似的用户问题重复走完整推理链路,既消耗 GPU 算力又带来数秒延迟。传统无缓存方案下,每个请求都要重新计算,成本随并发线性增长。引入语义缓存(对用户问题的 embedding 做相似度检索,命中则直接复用历史推理输出),可以将常见问题的响应从秒级降到毫秒级。

具体案例:某智能客服系统将用户问题的 embedding 存入 Redis 向量索引,命中相似度阈值后直接返回标准答案。当业务 FAQ 更新或大模型版本升级时,通过消息队列广播“失效”通知,各节点删除对应的缓存条目,并用 TTL 兜底保证最终一致。这套方案与文中“失效策略 + 懒加载回源”一一对应:模型更新就像老王换菜牌,先擦旧、再等客人问。

AIGC 图片生成的边缘缓存

阶段:内容分发与边缘计算

解决的问题:用户生成的图片被反复请求时,每次都回源图片服务重新读取或处理,带宽与算力开销巨大。CDN 边缘缓存把图片与元数据缓存在离用户最近的节点,用 TTL 控制新鲜度。

具体案例:某 AI 绘画产品上线新模型后,旧风格图片的缓存未及时失效,用户仍看到旧画风。团队采用“版本号作为缓存 key 前缀”方案:模型 v2 上线即切换 key 前缀,旧 key 自然过期——这正是多级缓存协同中“CDN 边缘缓存 + 版本号失效”的实战形态,好比街口广告牌换新海报,旧海报让它自然褪色即可。

推荐系统中间特征缓存

阶段:特征工程与在线推理

解决的问题:用户 embedding、物品 embedding 在推荐链路中被反复读取,全量重算昂贵;但用户行为实时变化,缓存需要写回或失效配合才能兼顾性能与新鲜度。

具体案例:某推荐服务用 Redis 缓存用户 embedding。用户发生点击行为触发特征更新:特征服务先写 Redis(写回模式,标记脏数据),每 5 分钟批量回写离线特征库,保证线上特征与离线训练数据最终一致。选型依据正是文中公式 θ = R/W:点击写入远少于 embedding 读取,故采用“失效 + 定时批量回写”的组合,既保住读取延迟,又控制写入成本。

Python 演示

python78 行
import time  class CacheEntry:    def __init__(self, value, expire_at):        self.value = value        self.expire_at = expire_at  class ConsistencyCache:    """模拟三种缓存一致性策略的玩具缓存.    缓存对应前台小黑板, 数据库对应后厨台账.    """     def __init__(self, mode="invalidate", ttl=5.0, flush_interval=3.0):        self.mode = mode        self.ttl = ttl        self.flush_interval = flush_interval        self.store = {}   # 缓存区, 存 CacheEntry        self.db = {}      # 源数据, 权威值        self.dirty = {}   # 脏数据登记表, 写回策略专用        self.last_flush = time.time()     def _write_db(self, key, value):        """模拟数据库写入的耗时."""        time.sleep(0.05)        self.db[key] = value     def _flush(self):        """把脏数据批量回写数据库."""        for key, value in self.dirty.items():            self._write_db(key, value)        self.dirty.clear()        self.last_flush = time.time()     def write(self, key, value):        """按当前 mode 执行写操作."""        if self.mode == "write_through":            # 同步双写缓存与数据库, 强一致, 延迟为两者之和            self.store[key] = CacheEntry(value, time.time() + self.ttl)            self._write_db(key, value)        elif self.mode == "write_back":            # 只写缓存并登记脏数据, 稍后统一回写            self.store[key] = CacheEntry(value, time.time() + self.ttl)            self.dirty[key] = value            if time.time() - self.last_flush >= self.flush_interval:                self._flush()        else:  # invalidate            # 删缓存旧值, 下次读回源拉新            self.store.pop(key, None)            self._write_db(key, value)     def read(self, key):        """读缓存, 未命中或过期则回源."""        entry = self.store.get(key)        now = time.time()        if entry is None or entry.expire_at < now:            value = self.db.get(key)            if value is not None:                self.store[key] = CacheEntry(value, now + self.ttl)            return value        return entry.value  def run_demo(mode):    cache = ConsistencyCache(mode=mode)    cache.write("牛肉面", 15)    cache.write("牛肉面", 18)    cached = cache.read("牛肉面")    db_value = cache.db.get("牛肉面")    if db_value is None:        db_value = "旧值(尚未回写)"    print("[{}] 缓存读到: {}, 数据库: {}".format(mode, cached, db_value))  if __name__ == "__main__":    for mode in ("write_through", "write_back", "invalidate"):        run_demo(mode)
目的:用同一个玩具缓存类演示三种一致性策略的行为差异——写穿透立即双写、写回只标记脏数据、失效则删旧值靠读时回源。
  1. self.dirty = {}写回策略的脏数据登记表,记录“改了缓存但还没回写源”的条目,对应打烊后待补记的台账。
  2. if self.mode == "write_through":写穿透分支:缓存和数据库同步写,保证强一致。
  3. self.store[key] = CacheEntry(value, time.time() + self.ttl)写缓存并设置 TTL,TTL 是后续失效策略的兜底。
  4. self._write_db(key, value)同步写库,模拟数据库的固定写延迟。
  5. elif self.mode == "write_back":写回分支:只更新缓存,不立刻写库。
  6. self.dirty[key] = value把 key 标记为脏,等批量回写,这就是“先黑板后台账”的偷懒手法。
  7. if time.time() - self.last_flush >= self.flush_interval:每 3 秒触发一次批量回写,将脏数据摊薄到一批。
  8. else: # invalidate失效分支的核心:只删不写。
  9. self.store.pop(key, None)删除缓存旧值,下次读时自然回源。
  10. if entry is None or entry.expire_at < now:懒加载判断:未命中或 TTL 过期则回源数据库并重新填充。

常见误区

误区我以为设计缓存一致性,就是要让缓存里的数据和数据库里的数据在任意时刻都完全一样,分毫不差。达不到这个标准就是不合格的设计。

正解一致性是有强弱等级的。写穿透提供强一致,但要求每次写都双写,延迟高;写回和失效策略提供最终一致,允许在特定时间窗口内缓存短暂滞后。工程上按业务容忍度选择等级,而不是盲目追求“一模一样”。就像老王的面馆,时令菜允许问一下再确认,招牌菜则必须写准——不同数据可以有不同新鲜度要求。

为何常见:教材和面试题总强调“强一致”是黄金标准,初学者容易把“理想”当成“默认”,忽略现实中的性能代价。

误区写回延迟低、吞吐高,看起来完美,那生产系统直接全用写回策略不就行了?比写穿透和失效都高级。

正解写回有一个致命风险窗口:数据只写在缓存里、尚未回写数据库时,如果缓存宕机或进程崩溃,这部分数据就永远丢失了。订单金额、支付状态等不可丢数据绝不能用写回;只有日志、计数、画像特征这类可容忍少量丢失的数据才适合。选型不是选“最好”,而是选“最合适”。

为何常见:性能指标最直观,新手容易只盯着延迟数据,忽视数据安全与业务语义。

误区我以为设置 TTL = 5 分钟,就是每 5 分钟自动去数据库拉一次新数据,所以缓存内容永远是新的。

正解TTL 到期的动作只是“失效”:条目被标记为过期,下次读请求发现过期后才会回源数据库拉取最新值并重新填充(懒加载)。在到期到下一次读之间,数据并不会自动刷新;如果一直没人读,缓存就一直空着或留着旧值。这是“惰性过期”,目的是把刷新代价推迟到真正需要时。

为何常见:直觉上“过期 = 定时刷新”,但缓存设计的核心哲学恰恰是“按需加载”——不为没人读的数据白花算力。

误区缓存里的旧值碍事,那我先把它删掉,再去更新数据库,这样缓存里就不会有旧数据了。

正解先删缓存再写库有一个经典并发漏洞:线程 A 删掉缓存后、尚未写库时,线程 B 读缓存未命中,回源读到数据库旧值并写回缓存——于是旧值又回来了。正确的 Cache-Aside 顺序是“先更新数据库,再删缓存”,即使删除失败造成短暂不一致,也可以用 TTL 兜底收敛。这与老王“先改台账、再擦黑板”的顺序逻辑一致。

为何常见:人脑处理“清除脏数据”时容易只盯着缓存副本,忽略了并发读操作可能把旧值“填回去”。

误区大模型推理那么贵,把输出结果缓存起来,设个超大 TTL 甚至永不过期,反正答案又不会变。

正解模型输出只有在“模型版本、Prompt 模板、业务知识、用户权限”都不变时才有效。模型一升级、知识库一更新,旧缓存答案就是错的。生产实践常用“版本号加入缓存 key”或“版本更新时广播失效”,让旧缓存自然过期。要像老王换季换菜牌一样,主动擦旧写新。

为何常见:只看到推理算力贵,没意识到 AI 系统是持续演进的,缓存键的语义会随版本漂移。

面试题

Q1. 缓存一致性策略要解决的核心问题是?

单选题 · easy

  • A.缓存容量不够用如何扩容
  • B.缓存副本与源数据的同步← 正确答案
  • C.缓存被并发删除的冲突
  • D.缓存过期时间的精确计算

答案:B。缓存一致性聚焦“副本与源”之间的同步问题:源数据变化后,缓存副本如何跟上。选项 A 是容量规划,C 是并发控制,D 是 TTL 调优,都属于缓存工程的其他侧面。正如老王关心的是黑板如何跟上台账,而不是黑板本身的大小。

Q2. 写穿透(Write-Through)策略的核心特征是?

单选题 · easy

  • A.只写缓存不写数据库
  • B.先写缓存后异步写库
  • C.缓存与数据库同步双写← 正确答案
  • D.只删除缓存不写数据

答案:C。写穿透要求每次写操作同步完成“写缓存 + 写数据库”两步,因此读取方永远拿到最新值,属强一致。A 是“只写缓存”的错误做法,B 是写回策略,D 是失效策略。代价正如公式 T_write = T_cache + T_DB 所示,写延迟是两者之和。

Q3. 写回(Write-Back)策略的主要风险是?

单选题 · mid

  • A.每次读请求都要访问数据库
  • B.缓存未命中时延迟过高
  • C.源数据滞后且崩溃可能丢失← 正确答案
  • D.缓存容量占用率过高

答案:C。写回先把数据写进缓存并标记为脏,稍后批量回写数据库。在回写完成前,源数据滞后;若此时缓存节点宕机,脏数据就丢了。A、B 描述的是缓存未命中的通病,D 与容量相关,都不是写回特有的核心风险。好比老王打烊前失火,黑板上的新菜名就没了。

Q4. 失效(Invalidation)策略执行的核心动作是?

单选题 · easy

  • A.更新缓存值为新值
  • B.删除旧缓存后等待回源← 正确答案
  • C.延长缓存过期时间
  • D.同时写缓存与数据库

答案:B。失效策略在源更新时只“删除”缓存中的旧值,不主动写新值;下次读请求未命中后回源拉新,再填充缓存,即懒加载。A 是“更新”语义,C 是 TTL 调整,D 是写穿透。就像老王只把牛腩面擦掉,等客人问了再去后厨确认。

Q5. 多级缓存平均延迟公式 T_avg = h₁T₁ + (1-h₁)h₂T₂ + (1-h₁)(1-h₂)T_DB 中,h₂ 表示?

单选题 · mid

  • A.数据库查询的命中率
  • B.L1未命中时L2命中率← 正确答案
  • C.写缓存的成功概率
  • D.全部请求的缓存命中率

答案:B。h₂ 是条件命中率:在 L1 未命中、进入 L2 的请求中,被 L2 接住的比例。(1-h₁)h₂ 才是在全体请求中“L2 命中”的占比。A 把 h₂ 误当成数据库命中率,C 与写路径无关,D 混淆了整体与条件概率。

Q6. 关于 TTL 与数据新鲜度的关系,下列说法正确的是?

单选题 · hard

  • A.TTL越大数据就越新鲜
  • B.到期后缓存自动向源刷新
  • C.变陈概率高且命中率更高← 正确答案
  • D.只影响容量不影响一致性

答案:C。TTL 越大,数据变陈概率 P_stale = 1-e^{-λT} 越高(不够新鲜),但缓存命中率也越高(延迟更低),这就是权衡。A 说反了;B 是常见误解,TTL 到期只失效、不自动刷新;D 忽视了 TTL 正是控制一致性的关键旋钮。

Q7. AI 推理服务的模型输出缓存,最合理的失效触发方式是?

单选题 · mid

  • A.每次推理前随机删除缓存
  • B.模型版本更新时批量失效← 正确答案
  • C.缓存永不过期不失效
  • D.推理时重新计算覆盖缓存

答案:B。模型输出缓存的有效性绑定模型版本与 Prompt 模板。版本更新时,旧输出的语义已过时,应通过版本号 key 切换或广播失效批量清理。A 随机删除破坏命中率,C 会返回旧模型时代的错误答案,D 等于放弃缓存、失去加速意义。

Q8. 多级缓存架构一般包含哪些层级?

多选题 · mid

  • A.本地内存进程内缓存← 正确答案
  • B.Redis分布式热点缓存← 正确答案
  • C.CDN边缘节点缓存← 正确答案
  • D.浏览器端本地缓存

答案:ABC。典型多级缓存:L1 本地进程内存(最快)、L2 Redis 分布式共享(承接热点)、L3 CDN 边缘(离用户最近)。D 浏览器存储属于客户端侧,一般不纳入服务端多级缓存体系。它们的关系如同老王的速记条、小黑板与街口广告牌。

Q9. 以下哪些属于缓存失效策略的常见实现手段?

多选题 · hard

  • A.主动删除如消息通知← 正确答案
  • B.TTL定期过期失效← 正确答案
  • C.懒加载回源刷新← 正确答案
  • D.缓存预热批量预写

答案:ABC。失效策略的三种手段:源更新时通过消息队列主动通知各节点删除旧值;TTL 到期被动失效兜底;读未命中时懒加载回源并重新填充。D 的批量预写属于缓存预热,是“提前填充”而非“失效”,概念上不属于失效手段。

Q10. 关于缓存一致性的工程权衡,下列说法正确的有?

多选题 · extreme

  • A.强一致通常带来更高写延迟← 正确答案
  • B.弱一致可降低写路径延迟← 正确答案
  • C.读多写少适合失效策略← 正确答案
  • D.写多读少适合写穿透策略

答案:ABC。写穿透强一致,但写延迟 = T_cache + T_DB,代价高(A 对);写回类弱一致把库写入异步化、摊薄成本,延迟低(B 对);读多写少时失效通知次数少,收益大(C 对)。D 说反了:写多读少应优先选写回策略,若选写穿透则每次写都双写,主路径被拖慢。