缓存策略与失效
缓存(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(已登录核对书面章节)。本页以新鲜度、失效竞态和故障流量独立组织说明。