缓存策略与失效

缓存(Cache)保存可以重复使用的数据或计算结果,用额外空间和一致性复杂度换取更低延迟、更少后端工作。设计起点是可测量的瓶颈与可接受的新鲜度,而不是在每个架构里先放一台 Redis。

旁路缓存的读取路径

放在哪一层,影响什么成本

位置主要收益主要代价
客户端或浏览器可以完全省去一次网络调用。客户端版本、容量和失效通知不容易统一控制。
CDN 边缘让可复用内容靠近用户,减轻源站流量。缓存键、权限隔离、区域副本与失效规则。
应用进程不需要访问远程缓存,延迟很低。每个实例各有副本,扩缩容和失效更分散。
共享缓存多个服务实例共用热点数据。增加网络依赖、节点容量与可用性管理。

公共文档、用户专属响应和带权限的任务日志不能无条件共用缓存键。缓存键不仅是资源编号,也可能需要租户、版本、查询参数或权限范围。

读取与写入模式

旁路缓存(Cache-aside)由应用先查缓存,未命中时访问数据库并回填。读穿(Read-through)则由缓存层或封装库负责回源,应用不必自己处理每次未命中。这是职责边界的区别,不是随便换一个 Redis 命令。

写穿(Write-through)在确认写入前同步更新持久层;写回(Write-back)先接受缓存写入,再异步刷入后端。后者可降低某些写入成本,但确认后的数据仍可能暴露在缓存丢失风险中。把应用独立写 Redis 和数据库两次,不会自动变成原子的写穿协议。

TTL 与淘汰解决不同问题

生存时间(TTL)描述何时过期;淘汰(Eviction)描述容量紧张时删除谁。LRU 偏向保留最近使用的数据,LFU 偏向保留高频数据,FIFO 根据进入顺序;实现可能使用近似策略。

选择策略要看热点持续多久、是否有一次性扫描和对象大小。TTL 也不是从业务修改时刻计算的天然陈旧度上限:旧读取可能在较晚时刻回填并重新开始计时。Redis 的策略与内存限制见官方淘汰说明。

数据库更新后,缓存怎样失效

常见旁路方案先提交数据库,再删除缓存,之后由读请求重新填充。它减少了显式双写的步骤,却并不提供跨数据库与缓存的原子性。

一个典型竞态是:请求 A 读到旧数据库值;请求 B 更新数据库并删缓存;A 随后把旧值写回缓存。仅说“更新后删缓存”没有消除这个窗口。可以根据业务需求使用版本化键、条件回填、变更事件驱动失效或更严格的读写协调,并明确失效失败后的补偿与 TTL 兜底。

对于要求精确余额、库存或配额的最终决策,缓存通常不应充当未经校验的事实源。能容忍几秒陈旧的展示数据则可能适合更简单的策略。

击穿、穿透、热点与整体故障

现象根因常见处理
热键到期后大量请求回源(Stampede)同一个结果被并发重建。Single-flight 合并回源、提前刷新、允许受控旧值。
不存在的键反复回源未命中结果也有高请求量。短时负缓存;适合时用布隆过滤器,但要处理误报与更新。
大量键同时过期生命周期高度同步。TTL 随机抖动、分批预热、限制重建并发。
单个热键压满节点热度集中,和键数量是否均匀无关。进程缓存、读取副本或业务拆分;另行处理一致性。
缓存服务不可用所有请求同时落回数据库。回源限流、并发上限、降级与熔断。

“缓存故障就直接访问数据库”只有在数据库仍有足够余量时才安全。故障策略需要和后端容量一起设计。

命中率如何变成后端负载

假设读取流量为 12,000 次/秒,请求命中率为 95%,理想稳态下回源约为 600 次/秒。缓存失效时可能重新接近 12,000 次/秒,远大于正常后端流量。这个算式还未考虑请求合并、多键查询和额外刷新。

平均命中率也会掩盖尾延迟:少数未命中请求仍可能很慢。应同时监控请求命中率、字节命中率、回源率、p95/p99、淘汰量、内存占用和热点分布。不能把命中和未命中的 p99 简单加权成系统 p99。

训练平台里的具体落点

已结束任务的不可变摘要、版本化配置和公开模型说明适合缓存。正在运行的进度需要明确刷新周期;故障恢复和配额分配应以受控状态为准。把尝试编号加入键,可以避免重试前后的状态互相覆盖;给缓存条目附带生成时间,也能帮助展示与排障区分新旧数据。

关联与来源

数据模型 · 一致性 · 容量估算

学习入口:Hello Interview · Caching(已登录核对书面章节)。本页以新鲜度、失效竞态和故障流量独立组织说明。