假设你是一家网红餐厅的主厨。每天开业前,你会提前把最畅销的几道菜备好食材、甚至提前炖好高汤——这样客人一进门就能立刻上菜,不用等半小时。但餐厅后厨的储藏室就那么大,你不能把菜市场整个搬进来。于是你每天都要决定:哪些食材值得占用宝贵的冷藏空间?新进的食材到了,该把哪份旧食材扔出去? 这正是缓存系统的日常。缓存就是互联网世界里的“后厨储藏室”——把热数据提前放进去叫预热,空间不够时决定扔谁叫淘汰。管理好这两件事,系统响应速度就能从“现点现做”变成“瞬间上桌”。理解缓存预热与淘汰,你就能设计出既快又不浪费内存的高性能系统。

岗位关联

对 AI 从业者而言,缓存管理是提升系统性能与降低算力成本的核心技能,掌握预热与淘汰策略,能让你的推理服务吞吐翻倍、延迟骤降,是走向资深架构师的必修课。

目录
  1. 可视化演示
  2. 缓存生命周期
  3. 预热的多种方式
  4. 主动淘汰策略
  5. 被动淘汰策略
  6. 工程落地实践
  7. 监控与调优
  8. AI 应用场景
  9. Python 演示
  10. 常见误区
  11. 面试题

可视化演示

动态演示缓存预热与淘汰的核心机制,通过总览/传输/转换/输出四种模式展示关键概念与交互流程。

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

缓存预热与淘汰:缓存预热 缓存预热的工程意义与冷启动问题 缓存预热策略分类与实现路径 核心机制可视化 缓存预热与淘汰整体架构与关键组件

缓存生命周期

想象餐厅后厨的储藏室,食材从采购上门、被取用做菜、到最后过期扔掉,整个流程就是一个生命周期。缓存中的数据也是这样:加载(数据写入缓存)→ 使用(被请求命中)→ 驱逐(被淘汰出缓存)。理解这个生命周期,是管理缓存的第一步。

生命周期管理的关键指标是命中率——也就是客人点的菜里,有多大比例能直接从储藏室取用,而不用临时去菜市场买。命中率越高,系统响应越快,下游数据库的压力也越小。命中率公式为:

预热和淘汰正是管理这个生命周期两端的手段:预热让更多数据在加载阶段就位,淘汰在使用结束后让位给新数据。餐厅里,聪明的老板会记录哪些菜点得多、哪些食材总被浪费,从而决定备料和清理的策略——这正是缓存管理者每天都在做的事。

理解了生命周期,我们来看看如何让数据高效地“就位”。

命中率=命中次数总访问次数=命中次数命中次数+未命中次数\text{命中率} = \frac{\text{命中次数}}{\text{总访问次数}} = \frac{\text{命中次数}}{\text{命中次数} + \text{未命中次数}}
命中率 90% 意味着 100 次请求中有 90 次直接从缓存拿数据,只有 10 次需要查数据库,这就是缓存存在的最大价值。
  1. 分子——命中次数指那些在缓存中找到了所需数据的请求数量,相当于客人点的菜恰好是已备好的食材,这是我们需要尽量拉高的数值。
    命中次数\text{命中次数}
  2. 分母——总访问次数所有请求的总和,无论成败。相当于这一天里客人的全部点单数,它由业务流量决定,我们无法直接控制。
    总访问次数=命中次数+未命中次数\text{总访问次数} = \text{命中次数} + \text{未命中次数}
  3. 整体——比率命中与总访问的比值,衡量系统响应速度与后端压力的综合表现。命中率越高,意味着越多请求走了“快速通道”。
    命中次数命中次数+未命中次数\dfrac{\text{命中次数}}{\text{命中次数} + \text{未命中次数}}

预热的多种方式

餐厅开业前,主厨会根据历史订单数据提前备好最热门的菜。系统也是同理:在流量高峰来临前,预先将热点数据写入缓存,这个过程就是缓存预热。它解决的是“冷启动”问题——刚启动的缓存是空的,如果此时直接迎接流量,所有请求都会穿透到数据库,可能直接把数据库压垮。

全量预热适合数据量可控的场景:把全部数据一次性加载到缓存,相当于开业前把整个菜单的食材都备齐,简单但可能浪费空间。增量预热则分批进行——像餐厅按周计划备货,只加载近期会用到的高频数据,节省空间但需要更细致的规划。

触发方式也有讲究。时间触发是到了预设时间(如每天早上 6 点)自动预热,适合流量有周期性规律的场景;事件触发则是当某个事件发生时(如商品上架、模型发布)立刻预热相关数据,适合实时性要求高的场景。

预热的方式虽多,但核心思路始终是“把好钢用在刀刃上”。那么,当缓存空间满了,如何决定“扔掉哪块食材”?这就要看淘汰策略了。

预热覆盖率=已预热的热点数据量预测期的热点数据总量\text{预热覆盖率} = \frac{\text{已预热的热点数据量}}{\text{预测期的热点数据总量}}
预热覆盖率衡量的是:该热的数据有多少比例提前放进了缓存。覆盖率越高,高峰期的数据库压力就越小。
  1. 分子——已预热数据量已经成功加载进缓存的预测热点数据条数。相当于今天已经备好的畅销菜食材种类数。
    已预热的热点数据量\text{已预热的热点数据量}
  2. 分母——热点总量根据历史数据预测的、即将被高频率访问的数据总量。相当于预估今天畅销菜的总数。
    预测期的热点数据总量\text{预测期的热点数据总量}
  3. 比例——覆盖水平覆盖率越高,说明我们的“备货”越充分。但要注意,预测不准确时,预热也可能“备错菜”,所以要持续校正热点列表。
    预热预测\frac{\text{预热}}{\text{预测}}

主动淘汰策略

想象后厨储藏室只有五个冷藏格,新食材送到时冰箱已经满了,你必须立刻扔掉一样旧食材。扔掉哪一样?这就引出了主动淘汰策略的核心问题。三种最经典的策略,对应三种不同的“扔东西”逻辑。

LRU(最近最少使用):把“最久没被用过”的先扔。逻辑是:如果一道菜很久没人点了,以后大概率也不会有人点了。LRU 用一个双向链表维护访问顺序,每次访问把数据移到链表头部,淘汰时删除链表尾部。它的优势在于实现简单、对突发流量适应快。

LFU(最不经常使用):把“用得最少”的先扔。它统计每个数据的访问次数,淘汰时删掉次数最少的。逻辑是:点得越多的菜说明越受欢迎,要多备料;冷门的直接放弃。LFU 对长期热点非常友好,但实现更复杂,需要维护频率计数。

FIFO(先进先出):最简单的逻辑——谁先进来谁先走,不考虑访问情况。就像餐厅按采购日期摆货,日期最早的食材先清掉,不管它是否还是畅销品。实现成本最低,但误杀率最高。

主动淘汰策略中,没有绝对的最优解。LRU 适合访问模式波动大的系统,LFU 适合热点集中的场景,FIFO 则适合对成本极端敏感的场景。餐厅老板的经验是:没有最好的策略,只有最适合自己客流模式的策略。说完主动淘汰,再来看看“到期自动扔掉”的被动淘汰策略。

LRU 中数据被淘汰的条件=缓存已满∧该数据是链表中访问时间最久远的\text{LRU 中数据被淘汰的条件} = \text{缓存已满} \land \text{该数据是链表中访问时间最久远的}
LRU 只关心“最后一次访问距现在多久”,LFU 关心“一共被访问过多少次”,FIFO 则完全凭“啥时候来的”。
  1. 缓存已满只有当存储空间全部被占用时,才会触发淘汰。空间没满时,新数据直接放入即可。
    cache_size=capacity\text{cache}\_{size} = \text{capacity}
  2. 最久未访问在双向链表中,头部的数据最新被访问,尾部的数据最久未被访问。淘汰时直接断开尾部节点。
    tail→prev\text{tail} \rightarrow \text{prev}
  3. 替换动作删除尾部节点,把新数据插入头部,完成一次完整的替换。
    evict(tail)→insert(head)\text{evict}\left(\text{tail}\right) \rightarrow \text{insert}\left(\text{head}\right)

被动淘汰策略

预制菜都有保质期,食材过期了必须扔掉,不然客人会吃坏肚子。缓存数据同样有“保质期”——TTL(存活时间)。设置了 TTL 的数据,到期后除非被再次更新,否则就应该在合适的时间被清理。被动淘汰策略的核心就是管理这些“过期食材”。

惰性删除是最“懒”的方式:每次访问数据时,先检查它是否过期了,过期了才删除。就像餐厅取食材时看一眼保质期,过期了就直接扔垃圾桶。这种方式的优点是操作简单、不占额外资源,缺点是过期数据会一直占着冷藏格,直到被访问才释放。

定期扫描删除则主动一些:每隔一段时间,系统会扫一遍缓存,把过期的数据批量清理。像每周五餐厅大扫除,把所有临期食材统一处理。优点是过期数据能及时释放空间,缺点是需要额外调度的 CPU 开销,扫描间隔需要权衡。

内存压力驱逐是最底线的方案:当缓存内存占用超过阈值时,不管数据是否过期,直接从所有数据中挑一些淘汰,优先淘汰过期或低价值的。这就像是冰箱突然断电,你必须把最不重要的食材先送人。

主动与被动淘汰策略的配合,就像餐厅既“定期整理冰箱”又“取用时顺手看保质期”。理解了两大类淘汰策略,我们进入工程实践:真实项目中该如何设计一套完整的缓存管理方案?

剩余存活时间=TTL−(当前时间−写入时间)\text{剩余存活时间} = \text{TTL} - (\text{当前时间} - \text{写入时间})
剩余存活时间小于等于 0 时,数据视为过期,可被惰性删除或定期扫描清理。
  1. TTL数据创建时设定的总存活时长,如 3600 秒。相当于食品包装上标注的“保质期 X 天”。
    TTL=3600 s\text{TTL} = 3600\,\text{s}
  2. 已存活时间当前时间减去写入时间,即数据已经存在了多久。相当于食品生产日期到今天的间隔。
    current−write_time\text{current} - \text{write}\_{time}
  3. 差值判定差值小于等于 0 说明已过期。惰性删除在访问时做此判断,定期删除在扫描时做此判断。
    TTL−(current−write_time)≤0\text{TTL} - (\text{current} - \text{write}\_{time}) \le 0

工程落地实践

回到餐厅的比喻。一个真正的餐厅老板不会只用一种策略管理所有食材——肉类可能提前 2 天备货,蔬菜则是当天清晨现买。缓存设计也是一样,要根据业务场景组合使用预热和淘汰策略。

业务场景选型是第一步:读多写少、热点集中的页面(如商品详情)适合全量预热 + LFU 淘汰;时序性强的数据(如最新新闻)适合增量预热 + FIFO 淘汰;登录态数据则适合 TTL 过期 + 惰性删除。没有通用的银弹,只有匹配业务的合身方案。

缓存放大效应是必须警惕的陷阱:当多个数据项同时过期,且流量正好打进来时,所有请求会同时穿透到数据库——这正是“餐厅午市高峰时冷藏柜里的食材恰好全过期了”的灾难场景。工程上通过给过期时间加随机偏移(TTL+随机数)来避免这类“缓存雪崩”。

缓存穿透则是另一个典型的失败场景:请求的数据在缓存和数据库中都不存在,每次请求都直接打到数据库。解决办法包括布隆过滤器先拦截、或者在缓存中为“不存在”也做短暂标记。而预热失败降级则是最后一道保险:如果预热任务失败了,系统要能自动降级为“热点数据按需加载”,而不是让所有请求裸奔到数据库。

工程落地的核心原则是:先把策略想清楚,再动手写代码。预热失败怎么降级、缓存雪崩怎么避免、穿透了怎么兜底——这些都在上线前设计好。最后,让我们聊聊投产之后的事:如何持续监控并让缓存策略不断进化。

缓存穿透率=请求的数据在缓存与存储层均不存在的请求数总请求数\text{缓存穿透率} = \frac{\text{请求的数据在缓存与存储层均不存在的请求数}}{\text{总请求数}}
穿透率越高,说明无效请求对存储层的冲击越大。理想情况下应通过布隆过滤器等手段让穿透率趋近于零。
  1. 分子——穿透请求数那些“缓存没有、数据库也没有”的请求,每次都要白白查询一次存储层。相当于客人点了一道菜单上根本没有的菜,你还得去厨房翻一遍确认没有。
    穿透请求数\text{穿透请求数}
  2. 分母——总请求数所有进来的请求总量,无论有效无效。
    总请求数\text{总请求数}
  3. 比值——冲击水平穿透率越高,存储层的无效消耗越大。用布隆过滤器前置拦截,可以极大压缩这个比例。
    穿透请求数总请求数\frac{\text{穿透请求数}}{\text{总请求数}}

监控与调优

餐厅老板每天打烊后都会翻看账本:哪些菜卖得多、哪些食材剩了。缓存系统投产后的日常,就是持续的观测与调优。命中率观测是一切调优的基础,我们需要按时间维度追踪命中率曲线,识别出高峰低谷的规律,判断预热配置是否合理。

抖动与颠簸是缓存调优中最需要警惕的现象。抖动指命中率在短时间内大幅波动,可能源于热点数据骤变或过期时间集中在同一时刻;颠簸则更为严重——淘汰策略在选择淘汰对象时反复犯错,导致刚淘汰的数据立刻又要访问,系统陷入了“刚扔又捡”的死循环。颠簸的典型诱因是缓存容量过小、与数据工作集大小不匹配。

参数自动调优是工程能力的分水岭:通过记录不同参数组合(TTL 长度、LFU 阈值、扫描间隔)下的命中率与延迟数据,用自动化脚本或强化学习寻找最优配置,而不是靠人工拍脑袋调参。

监控调优是让缓存系统“越用越聪明”的过程。合理的容量规划能让命中率稳定在高位。至此,我们走完了缓存生命周期管理的全链路——从预热加载到淘汰清理,再到持续优化。这套方法论,正是现代 AI 推理服务和互联网高并发系统的性能基石。

缓存空间利用率=已缓存的有效数据量缓存总容量=1−被淘汰但又被再次访问的数据量缓存总容量\text{缓存空间利用率} = \frac{\text{已缓存的有效数据量}}{\text{缓存总容量}} = 1 - \frac{\text{被淘汰但又被再次访问的数据量}}{\text{缓存总容量}}
空间利用率越接近 1,说明缓存中几乎没有“僵尸数据”。如果大量被淘汰的数据又立刻被访问,说明容量或策略需要调整了。
  1. 有效数据量缓存中真实的、会被再次访问的数据量。相当于冷藏格里“下周还会用的菜”的占比。
    有效数据量\text{有效数据量}
  2. 总容量缓存存储空间的物理上限。
    缓存总容量\text{缓存总容量}
  3. 无效占用被淘汰后又被请求的数据,意味着淘汰决策失误,白白浪费了一次替换成本。
    被淘汰但又被访问的数据量\text{被淘汰但又被访问的数据量}

AI 应用场景

大模型推理服务的 KV Cache 预热

阶段:模型推理服务部署与优化

解决的问题:大语言模型推理时,每次请求都要重新计算 Key-Value 缓存(KV Cache),导致首字延迟高、GPU 算力浪费。通过预热常用的系统提示词(system prompt)和共享上下文,可以大幅减少重复计算。

具体案例:某 AI 客服系统上线了固定 prompts 的 KV Cache 预热功能。系统启动时预先为高频 prompt 计算好 KV Cache 并存入 GPU 显存,用户请求到达时直接复用缓存结果。实测首 token 延迟从 890ms 降低到 120ms,吞吐量提升 4 倍,显存利用率提高了 35%。

特征存储缓存中的淘汰策略

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

解决的问题:在线推荐系统的特征数据库访问压力巨大,每个请求需查询数十个特征,未命中缓存时延特征服务的 SLA。采用 LRU + TTL 混合策略后,把高频特征留在内存,低频特征自动过期,兼顾精度与成本。

具体案例:某短视频推荐团队为特征存储配置了分层缓存:一级本地缓存采用 LRU(容量 2GB),二级分布式缓存采用 LFU + TTL(容量 50GB)。特征命中率从 82% 提升到 96%,在线推理时延 P99 从 45ms 降到 18ms,存储成本反而下降了 20%。

模型版本发布时的缓存预热

阶段:模型更新与 A/B 测试

解决的问题:新模型版本上线时,如果预热策略不当,会让新版本的推理流量直接打到底层存储,拖垮整体服务。通过事件触发的预热机制,在模型发布前把热门请求的推理结果预先填满缓存,实现平滑切换。

具体案例:某智能翻译团队发布新模型时,先在影子环境将前一天的 Top 10 万热句翻译结果预热到缓存,再切流 1% 验证,逐步放量到 100%。整个发布过程中翻译服务的 P99 延迟始终稳定在 200ms 以内,没有出现任何因缓存冷启动引发的故障。

Python 演示

python85 行
import timeimport randomfrom collections import OrderedDict  class LFUCache:    """LFU 最近最不经常使用缓存:频率最低且最久未用的先淘汰"""     def __init__(self, capacity: int):        self.capacity = capacity        self.cache = {}        self.freq_map = {}   # freq_count -> OrderedDict(key, None)        self.min_freq = 0        self._hit_count = 0        self._miss_count = 0     def _incr_freq(self, key: int) -> None:        freq = self.cache[key]        del self.freq_map[freq][key]        if not self.freq_map[freq]:            del self.freq_map[freq]            if self.min_freq == freq:                self.min_freq += 1        self.cache[key] = freq + 1        self.freq_map.setdefault(freq + 1, OrderedDict())[key] = None     def _evict(self) -> None:        keys = self.freq_map[self.min_freq]        old_key, _ = keys.popitem(last=False)        del self.cache[old_key]        if not keys:            del self.freq_map[self.min_freq]     def get(self, key: int) -> int:        if key not in self.cache:            self._miss_count += 1            return -1        self._hit_count += 1        self._incr_freq(key)        return 1     def put(self, key: int) -> None:        if self.capacity <= 0:            return        if key in self.cache:            self._incr_freq(key)            return        if len(self.cache) >= self.capacity:            self._evict()        self.cache[key] = 1        self.freq_map.setdefault(1, OrderedDict())[key] = None        self.min_freq = 1     def hit_rate(self) -> float:        total = self._hit_count + self._miss_count        if total == 0:            return 0.0        return self._hit_count / total  def warm_up(cache: LFUCache, hot_keys: list) -> None:    """缓存预热:把热点数据预先放入缓存"""    for key in hot_keys:        cache.put(key)  def simulate() -> None:    cache = LFUCache(capacity=5)    hot_keys = [1, 2, 3]    warm_up(cache, hot_keys)     for _ in range(200):        if random.random() < 0.7:            cache.get(random.choice(hot_keys))        else:            new_key = random.randint(10, 30)            cache.put(new_key)            cache.get(new_key)     print(f"命中率: {cache.hit_rate():.2%}")    print(f"缓存内容: {sorted(cache.cache.keys())}")  if __name__ == "__main__":    simulate()
目的:实现了一个带预热功能的 LFU 缓存——先用 warm_up 把热点数据预加载,再模拟随机访问,测试预热对命中率的影响。
  1. self.freq_map.setdefault(freq + 1, OrderedDict())[key] = None把 key 挂到更高一档的频率桶中,OrderedDict 保证同频率内按访问先后排序
  2. old_key, _ = keys.popitem(last=False)从最小频率桶中弹出最早插入的 key,这是 LFU 的淘汰核心
  3. hot_keys = [1, 2, 3]模拟预热阶段,相当于餐厅开业前提前备好的畅销菜
  4. if random.random() < 0.7:模拟 70% 的访问都集中在热点数据上,让预热的效果体现出来

常见误区

误区LRU 和 LFU 都是基于访问频率的淘汰策略,效果差不多,选哪个主要看个人偏好。

正解两者对访问模式的适应能力完全不同。LRU 只记“最后一次访问的时间”,适合突发热点(如新闻爆点);LFU 记录“累计访问次数”,适合稳定的长期热点(如畅销商品)。如果流量模式是突发性的,用 LFU 会保留很多早已过气的“昨日爆款”;如果热点是稳定型的,用 LRU 又容易误杀高频率数据。选错策略,命中率可能相差 20 个百分点。

为何常见:初学者往往只记住了“LRU 是最近最少使用、LFU 是最不经常使用”的文字定义,而没有理解两者对“时间窗口”和“累计频率”的根本分歧。

误区预热越彻底越好,最稳妥的方案是把数据库里的所有数据都一次性加载到缓存中,这样命中率一定最高。

正解全面加载会带来三大问题:启动时间过长(可能几个小时)、内存浪费严重(大量数据根本不会被访问)、缓存与数据库的一致性难度增大。正确的预热策略是分层分级的:先用历史访问日志识别出 Top N 热点数据,做全量预热;对中等热度数据做增量预热;对冷数据完全不预热,等请求到了再按需加载。预热覆盖率保持在 80% 左右、而不是 100%,往往是最优解。

为何常见:把“命中率”当作唯一目标,忽略了启动速度、内存成本和一致性开销。现实中系统是戴着镣铐跳舞的。

误区只要给数据设置了 TTL 过期时间,系统就会在到期那一刻自动把数据删掉,不需要额外的清理机制。

正解TTL 只是给数据打了一个“过期标记”,真正执行清理还需要主动机制。最常用的是“惰性删除 + 定期删除”的组合:惰性删除在每次访问时检查数据是否过期,过期才删;定期删除每 100ms 随机抽查一批 key,删掉其中过期的。两者配合才能确保过期数据不会长期占用内存。如果你只设置了 TTL 而没有开启删除策略,Redis 内存最终会被过期数据占满。

为何常见:TTL 的名称容易让人误以为“过期即消失”,但实际上缓存的清理是一个需要消耗资源的主动过程,设计上都是“懒人哲学”——能晚一点做就晚一点做。

误区只要选对了淘汰策略(比如 LRU 或 LFU),缓存就能挡住所有无效请求,不会出现缓存穿透问题。

正解淘汰策略解决的是“缓存空间满了怎么挪位置”的问题,而缓存穿透是“请求的数据根本不存在于任何一层”的问题——两者不在一个维度。防止穿透要使用布隆过滤器、缓存空值标记、参数校验等手段。淘汰策略管的是“缓存里有的数据怎么筛”,穿透防护管的是“缓存和数据库里都没有的数据怎么拦”。

为何常见:初学者把所有缓存问题都混为一谈,没有建立“缓存生命周期不同阶段对应不同问题”的系统思维。

误区缓存命中率是衡量缓存系统好坏的唯一标准,应该不断调优让它接近 100%。

正解命中率需要和成本做权衡。把缓存容量扩大一倍,命中率可能只从 90% 提升到 92%;再扩大一倍,可能只到 92.5%。这是一个边际收益递减的过程。真实场景中,90% 命中率配合合理的容量,往往比 98% 命中率但内存成本翻三倍的方案更符合 ROI。更关键的是,过大的缓存还会导致 GC 压力增大、启动预热时间过长。工程上不是追求命中率最高,而是追求命中率与成本的最佳平衡点。

为何常见:技术指标容易让人产生“越高越好”的思维惯性。但工程是代价与收益的权衡艺术。

面试题

Q1. 缓存预热的主要目的是什么?

单选题 · easy

  • A.增加缓存的总容量
  • B.提前将热点数据载入缓存← 正确答案
  • C.减少缓存的过期时间
  • D.提升数据库的写入速度

答案:B。预热的核心是让热点数据在流量到来前就位,解决“冷启动”问题。选项 A 错在预热不改变物理容量;C 错在预热与 TTL 无关;D 错在预热主要保护数据库免受读压力冲击。

Q2. 以下哪种淘汰策略依据“数据被访问的次数”做决策?

单选题 · easy

  • A.FIFO
  • B.LRU
  • C.LFU← 正确答案
  • D.TTL

答案:C。LFU(Least Frequently Used)统计累计访问频率,淘汰频率最低的数据。FIFO 按进入顺序淘汰,LRU 按最后一次访问时间淘汰,TTL 是过期时间机制而非淘汰策略。

Q3. LRU 缓存中,数据被访问后会发生什么?

单选题 · mid

  • A.数据被立即删除
  • B.数据移动到链表头部← 正确答案
  • C.数据移动到链表尾部
  • D.数据的过期时间被清零

答案:B。LRU 用链表维护访问顺序,刚刚被访问的数据意味着“最近在用”,移到头部表示最新鲜。链表尾部是最久未访问的,空间不足时优先淘汰。A 是过期删除而非 LRU 行为;C 恰好相反;D 是 TTL 的逻辑,LRU 本身不管理过期时间。

Q4. 以下哪些属于被动淘汰策略?

多选题 · mid

  • A.TTL 过期清除← 正确答案
  • B.LRU 链表挪移
  • C.惰性删除← 正确答案
  • D.定期扫描清理← 正确答案

答案:ACD。被动淘汰是“到期自动清理”的机制,TTL 设置过期时间、惰性删除在访问时检查过期、定期扫描是主动清理工具。LRU 属于主动淘汰策略,它按访问顺序而非时间过期来驱逐数据。

Q5. 缓存雪崩的根本原因通常是什么?

单选题 · mid

  • A.缓存容量设置得过大
  • B.大量数据同时过期且流量集中← 正确答案
  • C.缓存淘汰策略选择了 LFU
  • D.数据库写入性能不足

答案:B。当大量 key 设了相同的 TTL,在同一时刻过期,且此时恰好有高流量涌入,请求会穿透到数据库形成雪崩。工程解法包括过期时间加随机偏移、多级缓存等。A 是容量问题而非雪崩成因;C 是策略选择;D 是数据库问题,均非雪崩直接诱因。

Q6. 在以下哪种业务场景中,LFU 策略优于 LRU?

单选题 · hard

  • A.突发热点新闻的访问
  • B.长期稳定高频的商品详情页← 正确答案
  • C.顺序读取的日志数据
  • D.一次性导入的历史数据

答案:B。LFU 适合访问模式稳定的场景,长期热点数据的累计计数高,能稳定驻留缓存。突发热点适合 LRU,日志和一次性数据两者都不适合缓存。商品详情页呈现“少数商品占据绝大多数访问”的幂律分布,LFU 每次访问都加固热点地位,效果最好。

Q7. 关于缓存放大效应和击穿,以下说法正确的是?

多选题 · hard

  • A.缓存放大是指缓存占用内存过多
  • B.缓存击穿指单个热点 key 过期瞬间的并发穿透← 正确答案
  • C.给过期时间增加随机偏移可缓解雪崩← 正确答案
  • D.使用哨兵 key 可以避免热点失效← 正确答案

答案:BCD。缓存击穿是单个热点 key 在过期瞬间被大量请求同时打穿,哨兵 key 用永不过期的互斥锁保护重建过程。随机偏移让不同 key 的过期时间错开,避免同时失效。A 的“缓存放大”实际指多个缓存副本或更新时的写放大问题,并非内存占用。

Q8. 命中率公式的意义是什么?

单选题 · easy

  • A.衡量缓存算法的空间复杂度
  • B.衡量请求从缓存满足的比例← 正确答案
  • C.衡量缓存数据的压缩比例
  • D.衡量缓存预热需要的时间

答案:B。命中率 = 命中次数 ÷ 总访问次数,反映多少比例的请求直接由缓存服务。它是缓存系统最核心的绩效指标。其余选项分别涉及空间、压缩和时间,都与命中率定义无关。

Q9. 关于预热覆盖率与命中率的关系,哪些描述正确?

多选题 · hard

  • A.覆盖率越高,命中率一定越高
  • B.覆盖率提升到 100% 是最优方案
  • C.覆盖率只统计热点数据的预加载比例← 正确答案
  • D.覆盖率合理即可,追求 100% 会浪费内存← 正确答案

答案:CD。覆盖率是“已预热热点数据量 ÷ 预测热点总量”,只关心热点数据。追求 100% 覆盖意味着加载了大量边缘数据,边际收益低且浪费内存,所以 C、D 正确。A 过于绝对,如果热点预测不准,覆盖率再高也无效;B 忽略了成本约束。

Q10. 在 LFU 实现中引入“访问时间衰减”的目的是?

单选题 · extreme

  • A.让过期数据更快被扫描清除
  • B.避免某个 key 因历史高频而永久占据缓存← 正确答案
  • C.降低 LFU 的总体内存开销
  • D.提升命中率计算的精度

答案:B。LFU 最典型的问题是“历史荣誉”陷阱:一个曾经非常热、但现在已冷却的 key,因累计计数高而永不淘汰。引入时间衰减后,频率计数随时间下降,让系统能追赶新热点。A 的时间扫描是另一个维度;C 内存开销变化不大;D 衰减反而降低统计精度,它的价值在于时效性而非精确性。