缓存|计算机基础篇
# 一、缓存特征
30 秒速记
- 缓存命中表示请求直接从缓存取得响应;命中率越高,缓存被有效使用的程度通常越高。
- 缓存多占用容量有限的内存,因此不能无限保存数据,达到空间上限后必须为新数据腾出位置。
FIFO按进入顺序淘汰,优先移除最早写入的数据,适用于更关注新数据的场景。LRU优先移除距离上次访问最久的数据,目标是让近期活跃的热点数据继续留在缓存中。LFU根据一段时间内的访问次数决策,优先淘汰使用频率最低的数据。- 淘汰策略本质上是在有限空间中选择保留对象;进入时间、最近访问时间和访问频次分别对应不同的业务偏好。
缓存的核心特征是复用已有结果,但容量有限,因此需要关注命中情况和淘汰策略。 请求能直接从缓存取得响应就是命中,命中率越高,通常说明缓存被复用得越充分。空间不足时,FIFO 偏向淘汰最早写入的数据,LRU 保留近期活跃数据,LFU 更看重访问频次。具体选择取决于业务更在意数据新旧、近期热点还是长期高频,不能只看策略名称。
面试官追问
追问 1资讯流页面只强调展示较新的内容,缓存容量达到上限后,评审者主张用 LFU 保留历史热点,你会接受吗?
不应仅因历史访问频繁就选择 LFU,这可能让较旧内容继续占用空间,与保留新内容的目标冲突。若业务价值主要取决于写入先后,可优先评估 FIFO;但它不识别真正热点,仍需用实际流量验证命中效果。
追问 2前端实现了容量为 3 的缓存,依次写入 A、B、C,读取 A 后再写入 D;测试要求淘汰 B,你会选什么策略?
该预期符合 LRU,因为读取 A 后它已不是最久未使用的条目,写入 D 时应淘汰 B。实现必须在命中时刷新访问顺序;若读取不更新位置,实际行为会退化为接近 FIFO。
追问 3商品搜索从稳定热点变成短期突发流量后,团队仍坚持按累计访问次数淘汰,你会提醒什么风险?
累计计数式 LFU 可能继续保留已经降温的历史热点,使新热点难以及时进入缓存。需要明确统计窗口、计数衰减和平局规则;若实现没有衰减机制,访问模式变化越快,历史包袱越明显。
追问 4线上声称启用了 LRU,但日志显示刚被频繁读取的键仍最先淘汰,你会怎样定位?
先核对命中路径是否确实更新了最近访问位置,再检查覆盖写入、过期删除和容量淘汰是否共用一致的顺序结构。若只在 set 时调整位置,读取热点仍可能被当成旧数据;并发更新还可能破坏记录顺序。
追问 5接口既有长期稳定热点,又会周期性扫描大量一次性键,架构评审在 LRU 与 LFU 间争论,你怎么取舍?
单纯 LRU 容易被扫描流量污染,而缺少衰减的 LFU 可能长期保留过时热点。应按生产访问序列回放,对比命中率、淘汰键和延迟,再决定是否需要窗口或衰减机制;更复杂的策略也会增加状态维护成本。
# 命中率
30 秒速记
- 请求无需绕过缓存获取结果,而是由缓存直接响应时,记为一次缓存命中。
- 命中率用于描述请求被缓存承接的程度,是观察缓存是否得到有效利用的核心特征。
- 命中率提升通常意味着更多请求能够复用缓存中的结果。
- 题干只说明命中率与缓存利用程度的关系,没有给出具体计算公式、统计周期或合理阈值。
缓存命中率表示可统计请求中,有多少能够由缓存直接返回有效结果。 常见口径是“命中次数除以可统计请求总数”,但过期、绕过和刷新请求是否计入分母,要以具体监控定义为准。命中率高通常能减少回源,不过不一定直接等于延迟更低,还要看缓存访问和失效维护的成本。排查时我会把 hit、miss、expired 和回源量放在一起看,避免整体比例掩盖局部问题。
当某个请求能够通过访问缓存而得到响应时,称为缓存命中。
缓存命中率越高,缓存的利用率也就越高。
原理拆解: 请求到达缓存层后,系统先依据缓存键查找条目,再检查条目是否存在、是否过期以及是否满足当前请求的复用条件。只有缓存能够直接提供有效结果,且无需继续访问源站、数据库或计算服务时,才算命中;未找到、已过期、主动绕过缓存或必须回源校验后才能取得结果,通常计为未命中。常见统计口径是 命中次数 / 可统计请求总数,但分母是否包含绕过请求、错误请求和刷新请求必须由具体系统定义,不能脱离监控口径直接比较。
具体例子: 某接口在一个统计周期内收到 1000 次可统计请求,其中 800 次由缓存直接返回,按上述口径命中率为 80%。若热点数据集中在少数缓存键上,即使缓存条目数量不多,也可能获得较高命中率;反过来,大量低频且互不重复的请求即使都写入缓存,后续没有复用,命中率仍然较低。提升命中率通常能减少回源次数,但并不自动等于响应更快或成本更低,还要考虑缓存访问耗时、序列化开销和失效维护成本。
边界与排查: 命中率必须结合统计周期、请求维度和缓存层级理解。整体命中率可能掩盖某个接口、租户或缓存键前缀的低命中;多级缓存中,本地缓存未命中而分布式缓存命中,也要明确是分别统计还是合并统计。过长的过期时间可以抬高命中率,却可能返回陈旧数据;缓存穿透、键设计不稳定、容量淘汰和集中失效则会使命中率下降。
工程验证: 监控时应同时采集 hit、miss、bypass、expired、evicted 和回源量,并按接口与缓存键类别分组。将命中率曲线与缓存延迟、源站负载、错误率及数据新鲜度对照:若命中率升高但延迟未下降,应检查缓存本身是否变慢;若命中率突然下降且回源量同步上升,应排查批量过期、发布后键格式变化、缓存容量不足或节点故障。
面试官追问
追问 1运营后台显示缓存命中率从 92% 降到 70%,请求量保持稳定,负责人据此断言数据库压力一定上升,你会同意吗?
不能仅凭命中率下降断言数据库压力一定上升,只能确认按当前口径由缓存直接返回的请求占比降低。未命中请求也可能进入下一级缓存、源站或计算服务,应结合回源量、缓存层级和统计分母确认影响。
追问 2某接口一个周期有 1000 次可统计请求,其中 800 次由缓存直接返回,监控代码应如何记录命中率?
按给定口径应记录为 800 / 1000 = 80%,前提是这 1000 次都属于约定的可统计请求。还要分别采集 hit、miss、bypass 和 expired,否则改变分母范围后,同一个百分比无法稳定比较。
追问 3多级缓存改造后,本地缓存命中率下降、分布式缓存命中率上升,产品要求用一个总命中率证明优化成功,你会怎么处理?
应先明确本地命中、分布式命中是分别统计还是合并统计,不能直接把两个比例相加。即使总命中率上升,也要对照缓存延迟、回源量和源站负载;较慢的下级缓存可能让命中增加却未改善响应时间。
追问 4发布后命中率在几分钟内骤降,回源量同步上升,但请求规模没有明显变化,你会优先排查哪些信号?
优先检查发布前后缓存键格式、批量过期时间、容量淘汰和缓存节点状态,并按接口及键前缀拆分曲线。若 expired 集中上升更像批量失效,若 evicted 激增则偏向容量不足;仍需结合日志确认,不能只看整体比例。
追问 5业务要求通过延长过期时间把命中率推高,内容页又必须及时展示更新,你会如何取舍?
延长过期时间可能提高复用概率,却会扩大返回陈旧数据的窗口,因此不能只以命中率作为目标。应同时约束数据新鲜度,并观察缓存延迟、回源量和失效维护成本;若内容时效优先,就要接受一定的未命中或验证开销。
追问 6两个团队都报告 90% 命中率,一个按接口统计,另一个按全部缓存层合并统计,评审时能直接比较吗?
不能直接比较,因为请求维度、缓存层级、统计周期和分母定义均可能不同。应统一是否包含绕过、错误与刷新请求,并明确条件校验后的响应如何计数;口径未对齐时,数值高低不代表缓存利用程度更好。
# 最大空间
30 秒速记
- 缓存一般使用内存承载,而内存容量相较磁盘更受限制,因此缓存必须设置可控的空间上限。
- 空间上限决定缓存最多能够保留多少数据,不能按数据增长速度无限扩张。
- 现有缓存数据达到容量边界后,新数据写入需要以移除部分旧数据为前提。
- 淘汰是容量受限后的必要处理,但题干没有指定应删除哪些数据或采用哪种淘汰算法。
- 扩大缓存空间可以延后淘汰发生,却仍受可用内存限制,不能从根本上取消容量边界。
缓存必须设置最大空间,因为它通常占用容量有限的内存,不能随着数据量无限增长。 这个上限可以按条目数、估算字节数或运行时内存指标控制;写入超限时,需要先淘汰旧数据再接纳新值。我一般还会为业务对象、运行时堆和突发流量留出余量,避免缓存没到配置上限,进程就已经出现内存压力。比如容量为 2 的 LRU 缓存写入第三项时会淘汰最久未访问的数据,但扩大容量只能降低淘汰频率,不能取消边界。
缓存通常位于内存中,内存的空间通常比磁盘空间小的多,因此缓存的最大空间不可能非常大。
当缓存存放的数据量超过最大空间时,就需要淘汰部分数据来存放新到达的数据。
原理拆解: 缓存空间上限本质上是一项资源预算,可以按条目数、估算字节数或运行时提供的内存指标控制。写入链路通常是:检查键是否已存在,计算写入后的占用量,若超过上限则执行淘汰,释放足够空间后再接纳新值。上限并不等于进程可用内存总量,还要为业务对象、运行时堆、网络缓冲区和突发流量保留余量,否则缓存尚未达到配置值,进程就可能因内存压力频繁回收甚至退出。
最小验证: 输入依次为 a、b、再次读取 a、写入 c,验证容量为 2 的 LRU 缓存会淘汰最久未使用的 b,而不是简单删除最早创建的 a。
class LRUCache {
constructor(maxEntries) {
if (!Number.isInteger(maxEntries) || maxEntries < 1) {
throw new RangeError('maxEntries must be a positive integer');
}
this.maxEntries = maxEntries;
this.store = new Map();
}
get(key) {
if (!this.store.has(key)) return undefined;
const value = this.store.get(key);
this.store.delete(key);
this.store.set(key, value);
return value;
}
set(key, value) {
if (this.store.has(key)) this.store.delete(key);
this.store.set(key, value);
while (this.store.size > this.maxEntries) {
const oldestKey = this.store.keys().next().value;
this.store.delete(oldestKey);
}
}
keys() {
return [...this.store.keys()];
}
}
const cache = new LRUCache(2);
cache.set('a', 1);
cache.set('b', 2);
cache.get('a');
cache.set('c', 3);
console.log(cache.keys());
console.log(cache.get('b'));
get 通过删除后重新插入更新访问顺序;超限循环删除 Map 中最早的键。预期输出为 [ 'a', 'c' ] 和 undefined。该实现限制的是条目数:若单个值大小差异很大,它不能代表真实内存占用,也没有处理过期时间和并发一致性。
边界与排查: 容量调大只会降低淘汰频率,并不会消除边界;容量过小则可能形成持续写入、立即淘汰的缓存抖动。工程验证应同时观察命中率、淘汰次数、写入失败、进程堆占用和延迟分位数,并用接近生产的数据尺寸做压测。若命中率随容量增加仍无明显改善,应排查访问是否缺乏局部性、键是否被随机参数打散,或数据是否在再次访问前已经过期,而不是继续盲目扩容。
