带团队这几年,我面试Java开发时有个惯用的“基础分水岭”问题,就是缓存驱逐和缓存过期。很多人简历上写着“熟悉缓存机制”,聊起分布式缓存、本地缓存、缓存穿透这些话题也能说上几句,但被问到“驱逐(Eviction)和过期(Expiration)在概念和触发机制上有什么本质区别”时,常常卡壳。这道题出自JCache(JSR-107)基础篇,一点都不偏门,它考察的是对缓存体系最底层两个概念的区分度。如果你能把这两个概念讲透,说明你对缓存的理解不是停留在“会用API”的层面,而是真的知道缓存引擎在背后做了什么。这篇文章就把这两个概念彻底掰开揉碎,从定义、触发机制、规范设计到实战中的坑,一次说清楚,不管是准备面试还是日常写代码调缓存,都应该会有收获。
1. 概念层面:这两个词要解决的问题根本不在一个维度上
很多人混淆驱逐和过期,原因很朴素——从外面看,两者都表现为“缓存里拿不到数据了”。但往深一层看,它们要解决的是完全不同的两类问题,一个管“数据能不能信”,一个管“空间够不够用”。
1.1 缓存过期:给数据贴一张保质期标签
过期(Expiration)本质上是数据生命周期管理,它回答的问题是:一条数据放进缓存之后,最多可以信任多久?
每条数据都有自己的存活时间(TTL,Time To Live),时间一到,这条数据在逻辑上就失效了。失效之后,缓存就不应该再把它当作有效数据返回给业务方,因为底层数据源可能已经变了。
典型场景很好理解:
- 短信验证码,5分钟有效期,超过5分钟必须作废。
- 登录Token,2小时有效期,超过2小时必须重新登录。
- 商品库存数量,30秒刷新一次,超过30秒数值可能不准。
这几个场景的共同点是,业务上有一个明确的“数据新鲜度”要求。过期机制就是用来保证业务不会拿到逻辑上已经作废的数据。它的核心驱动力是时间,是业务语义,和数据存储空间没有半毛钱关系。哪怕你的缓存只有10条数据,空间宽裕得很,该过期的照样过期。
用一个生活化的类比:过期就是冰箱里牛奶包装上的保质期。到了保质期,不管冰箱里还剩多少空位,这瓶牛奶都不能再喝了,必须扔掉。
1.2 缓存驱逐:给缓存容器定一条容量红线
驱逐(Eviction)本质上是存储空间管理,它回答的问题是:缓存容器容量有限,放不下新数据了,该让谁腾位置?
任何缓存都不可能无限存储。不管是本地内存缓存还是分布式缓存,容量都有上限。当你往缓存里写了一条新数据,但容器已经满了,就必须从已有的数据中挑一条或多条移除,给新数据腾地方。这个“挑谁移除”的过程,就是驱逐。
被驱逐的条目并不一定是“过期”或者“无效”的,它可能还很新鲜,甚至刚刚被访问过。只是因为空间不够了,必须有人让座。选择谁让座,就涉及驱逐策略:
- LRU(Least Recently Used):最近最少使用,优先淘汰最久没被访问的。
- LFU(Least Frequently Used):最不经常使用,优先淘汰访问频率最低的。
- FIFO(First In First Out):先进先出,按写入顺序淘汰。
- 随机淘汰:随机挑一个,简单粗暴。
继续用生活类比:驱逐就是电影院座位有限,来了新观众,必须有人站起来让座。让谁站?通常是那个最久没来电影院的人。这个人可没“过期”,票也是有效的,只是座位不够了而已。
1.3 一张表看清两者的本质差异
| 对比维度 | 缓存过期(Expiration) | 缓存驱逐(Eviction) |
|---|---|---|
| 核心目标 | 保证数据新鲜度与一致性 | 控制存储空间与内存占用 |
| 触发源 | 时间(TTL到期) | 容量(达到上限) |
| 触发时机 | 条目创建/更新/访问后计时到期 | 写入新条目且容器已满 |
| 触发位置 | 读取路径(惰性)或后台扫描(主动) | 写入路径(put时检查) |
| 被删条目的特征 | 逻辑上已失效 | 可能仍然有效,但被策略选中 |
| 是否需要业务语义 | 需要,TTL由业务场景决定 | 不需要,策略由缓存层面统一决定 |
| 数据能否“复活” | 到期后需要重新加载 | 被驱逐后再次访问也会重新加载 |
看完这张表,你应该能感觉到,这俩概念从“出身”开始就不是一回事。
2. 触发机制:到底是谁、在什么时间、按什么规则按下删除键
概念清楚了,接下来是硬核部分——触发机制。很多人挂在面试官手里,就是挂在细节上。
2.1 过期的触发:时间裁判,通常“懒”和“勤”双管齐下
过期机制的触发点建立在“时间检查”上,但不同的实现检查时机差别很大。
惰性删除(Lazy Expiration)
最常见的做法是,当业务代码发起一次get操作时,缓存引擎会先检查这个条目的生存时间。如果发现当前时间已经超过了创建(或最后更新/最后访问)时间加上TTL,就判定条目过期,移除它,然后返回一个miss(未命中)结果。
这种方式的优点是省CPU,没有后台线程扫描,不会因为“检查过期”这件事消耗额外资源。缺点也很明显:过期的条目如果不被访问,会一直躺在内存里,白白占着空间。比如一个TTL为60秒的条目,在第61秒时如果没有任何人get它,它依然物理存在于缓存中,只是逻辑上“应该死了”。
主动删除(Active Expiration)
为了弥补惰性删除的缺点,很多实现会启动一个后台线程,周期性地扫描缓存条目,把已经到达TTL的条目批量清理掉。扫描频率、每次扫描数量都需要权衡,扫得太频繁伤CPU,扫得太慢又意味着一批内存被无效数据占用。
事件驱动型过期:还有更精细的做法,比如用时间轮或者优先队列维护“即将过期”的条目列表,等到时间到了就触发回调,精准删除。
实际工程中,采用“惰性删除 + 主动删除”组合的比较多:惰性删除保证读取的正确性,主动删除负责回收内存。这也是很多缓存框架和KV存储引擎的通用设计思路。
关键点在于:过期的触发和“容量够不够”没有任何关系。哪怕缓存空荡荡,到了时间照样触发过期;哪怕缓存已经爆满,没到TTL的条目也不会因为“时间到了”而被删除。时间,是过期机制唯一的裁判。
2.2 驱逐的触发:空间警察,只在容量满时上班
驱逐机制的触发点建立在“容量判断”上,而且绝大多数情况下发生在写路径上。
流程大概是这样的:
- 业务方发起put操作,写入新条目。
- 引擎首先把新条目写入缓存容器。
- 写入完成后,检查当前容器大小是否超过配置的容量上限(比如最多10000条,或者最大占用512MB)。
- 如果超过了,触发驱逐过程:根据驱逐策略选出“受害者”条目,从容器中移除,直到容量回到红线以内。
- 如果没超过,不需要驱逐,新条目正常驻留。
可以看到,驱逐是典型的“写时触发”(也有的实现支持后台异步巡检,但主流路径还是写时驱动)。它的核心目的是在写入新数据时腾出空间,保证缓存容器不会无限膨胀。
再看驱逐策略。LRU的实现思路是维护访问顺序,每次访问条目就把它的优先级提到“最近使用”的位置,驱逐时从“最久未使用”的一端开始删。LFU则是给每个条目维护一个访问计数器,驱逐时优先删计数最低的。不过LFU有个经典问题——一个条目在某个时段被高频访问,计数器涨得很高,之后再也没有人访问它,但因为计数高,它一直赖在缓存里不走,形成了“数据老龄化”。所以工程上往往还要给计数器做衰减,或者引入时间窗口。
在高并发场景下,精确的LRU实现本身也有代价,因为每次访问都要更新顺序结构,是一个竞争热点。很多高性能KV存储会采用“近似LRU”:不维护全量的精确访问顺序,而是每次驱逐时只在缓存里随机采样一小部分条目,从样本里挑最久未用的那个删掉。这种牺牲一定精确度换取吞吐量的做法,是分布式缓存和大内存本地缓存里非常常见的工程折中。我见过一些面试者提到LRU时只答“淘汰最久没用的”,能主动说出“采样近似LRU在超高并发下的必要性”的,确实凤毛麟角,但一旦说出来,面试官的眼神就会变。
2.3 触发机制的本质差别:一个是“到期作废”,一个是“被迫让座”
把两者放在一起看,区别就非常鲜明了:
- 过期,是条目“该死了”,它的生命走到尽头,逻辑上已经失效,删除它只是走个流程。
- 驱逐,是条目“不该死但不得不走”,它本身是有效数据,只是缓存容器再也装不下了。如果空间足够,它完全可以继续工作。
结果导向地看,两者都是“get不到数据了”,但原因完全不同。这个区别一旦想明白,后续排查线上缓存问题就会清晰很多。
再深入一步:有没有可能出现“因过期而驱逐”的混合机制?有。有些缓存引擎的驱逐策略会考虑条目的剩余生存时间,比如优先驱逐“即将过期”的条目,而不是只按LRU或LFU来。这种做法本质上是把过期信息当作驱逐策略的一项输入,让两类机制协同工作,而不是互相替代。这一点对后面的实战排坑特别重要,先把印象留在这里。
3. 回到JCache(JSR-107)规范:接口设计里藏着设计者的答案
前面讲的都是通用概念,现在把话题拉回到标题里提到的JCache规范。JCache是Java语言层面的缓存访问规范,它定义了一套统一的接口,让你不关心底层是本地缓存还是分布式缓存,都能用同一套代码操作。规范里对“过期”和“驱逐”的处理方式,本身就是理解这两个概念本质差异的一份非常标准的参考答案。
3.1 JCache是什么
JSR-107,也就是JCache,是Java社区制定的一套缓存API规范。它解决了什么问题?在早年,Java生态里的缓存实现五花八门,有的用Map加手动过期,有的引入重量级缓存框架,接口各不相同,切换缓存实现等于重写业务代码。JCache把这些统一了。
规范里最核心的几个接口:
- CachingProvider:缓存提供方入口,负责创建和管理CacheManager。
- CacheManager:缓存管理器,负责创建和管理多个Cache。
- Cache:单个缓存实例,相当于一个带丰富语义的Map。
- Entry:缓存条目,包含key和value。
- CacheLoader、CacheWriter:分别负责从底层数据源加载数据、把数据写回底层数据源。
在这个体系里,过期的语义被规范定义得非常细致,而驱逐的语义则相对“放权”给了具体实现。这种设计上的差异,恰好对应了两个概念在缓存体系中的不同定位。
3.2 ExpiryPolicy:规范里的一等公民
在JCache规范里,过期策略是通过ExpiryPolicy接口来定义的。它是一个一等公民级别的抽象,直接体现在配置API中。
先看接口定义:
public interface ExpiryPolicy { Duration getExpiryForCreation(); Duration getExpiryForAccess(); Duration getExpiryForUpdate(); }这三个方法分别对应缓存条目生命周期中的三个关键状态变更时刻:
- getExpiryForCreation:条目刚创建(写入)时,返回它应该存活多久。
- getExpiryForAccess:条目被读取命中时,返回是否重置生存时间。
- getExpiryForUpdate:条目被更新(覆盖写入)时,返回新的生存时间。
如果某个方法返回null,表示这个状态变更不改变当前条目的过期时间。
规范还预置了几个现成的策略实现:
| 策略类 | 行为 |
|---|---|
| EternalExpiryPolicy | 永不过期 |
| CreatedExpiryPolicy | 条目创建后固定存活指定时长 |
| ModifiedExpiryPolicy | 条目每次被更新后,重新计算存活时长 |
| AccessedExpiryPolicy | 条目每次被访问后,重新计算存活时长(滑动过期) |
在实际项目里,CreatedExpiryPolicy和AccessedExpiryPolicy用的最多。前者适合验证码、Token这种“创建后固定时间作废”的场景;后者适合“只要一直有人用就一直续命”的场景。
看一个实际的配置代码:
import javax.cache.configuration.MutableConfiguration; import javax.cache.expiry.CreatedExpiryPolicy; import javax.cache.expiry.Duration; import javax.cache.expiry.AccessedExpiryPolicy; import java.util.concurrent.TimeUnit; // 固定TTL:创建后5分钟过期 MutableConfiguration<String, String> fixedConfig = new MutableConfiguration<>(); fixedConfig.setExpiryPolicyFactory( CreatedExpiryPolicy.factoryOf(Duration.of(TimeUnit.MINUTES, 5)) ); // 滑动TTL:每次访问后重新计算5分钟 MutableConfiguration<String, String> slidingConfig = new MutableConfiguration<>(); slidingConfig.setExpiryPolicyFactory( AccessedExpiryPolicy.factoryOf(Duration.of(TimeUnit.MINUTES, 5)) );你甚至可以自定义策略,实现ExpiryPolicy接口:
public class VideoTokenPolicy implements ExpiryPolicy { @Override public Duration getExpiryForCreation() { return Duration.of(TimeUnit.MINUTES, 10); } @Override public Duration getExpiryForAccess() { // 读取不刷新过期时间 return null; } @Override public Duration getExpiryForUpdate() { // 更新重新给10分钟 return Duration.of(TimeUnit.MINUTES, 10); } }从这段接口设计就能看出,过期机制在规范中的地位是“明确、通用、内置”,它被设计为每个缓存条目独立维护生命周期,并且可以在不同的状态变更节点上动态调整。这种“逐条目、逐状态”的过期语义,是过期机制区别于驱逐的核心标签。
3.3 Eviction:规范里没设标准接口,让实现自由发挥
有意思的是,JSR-107规范对驱逐却没有定义类似ExpiryPolicy这样的标准接口。规范只是建议实现提供相关的配置能力和统计信息,至于驱逐策略具体怎么做、怎么配,基本上交给每个实现去发挥。
为什么规范要这样区分对待?原因并不难理解:
- 过期机制的核心特征是“时间”,时间对所有缓存实现来说是一样的,不存在性能和架构上的显著差异,所以适合抽象成统一规范。
- 驱逐机制和底层数据结构、存储引擎的耦合度极高。本地内存缓存可以用精确的LRU链表,分布式缓存可能要维护每个节点的容量水位,不同实现的性能和取舍完全不同。如果规范强行统一驱逐API,反而会限制各实现发挥自己的优势。
所以你在实际使用JCache时,可能会发现:设置过期策略的API是规范的、所有实现通用的;而配置驱逐策略的方法,往往藏在具体实现自己扩展的配置类里。
当然,这不代表规范完全不管驱逐。JCache包含管理(Management)和统计(Statistics)相关的MBean定义,你可以通过标准的JMX方式拿到缓存的各种统计指标,其中就包括驱逐相关的计数。
驱逐配置在代码里通常长这样(以某个支持JCache规范的缓存实现为例,API形式可能略有差异,但思路通用):
MutableConfiguration<String, String> config = new MutableConfiguration<>(); config.setExpiryPolicyFactory( CreatedExpiryPolicy.factoryOf(Duration.of(TimeUnit.SECONDS, 60)) ); // 驱逐相关配置通常由具体实现提供扩展入口 // config.setMaxEntries(10000); // config.setEvictionPolicy(EvictionStrategy.LRU);注意,我在代码注释中写了“由具体实现提供”几个字,这一点极其重要。使用JCache规范编程时,如果你的代码里出现了setMaxEntries这样的调用,它大概率不是标准接口,而是具体实现暴露出来的扩展方法。这也是为什么很多项目虽然用了“标准JCache API”,但功能上严重依赖特定实现的原因。面试的时候如果能把这个细节讲出来,很加分。
3.4 在引擎内部,它们是如何协同工作的
我们再把视角放到缓存引擎内部,看看一次完整的读写流程里,过期和驱逐分别在哪个环节生效。
一次put操作大致经历:
- 根据ExpiryPolicy的getExpiryForCreation计算出新条目应存活的时间。
- 将新条目写入存储容器,并记录其到期时间戳。
- 判断当前容器容量是否超过上限。
- 如果超过,执行驱逐策略,移除一个或多个条目。
- 写入完成,发出事件通知。
一次get操作大致经历:
- 根据key找到对应条目。
- 检查当前时间是否已超过该条目的到期时间。
- 已过期:移除该条目,返回miss,触发CacheLoader(如果有配置)回源加载。
- 未过期:返回value,并调用ExpiryPolicy的getExpiryForAccess,决定是否更新到期时间。
- 如果容器容量在此之前已经超限,某些实现可能还会借机触发一次驱逐。
从流程上可以看出,过期检查出现在读路径上,驱逐检查出现在写路径上,两者是并行的、互不替代的机制。一个完整的缓存配置,既要有合理的过期策略来保证数据新鲜度,也要有合理的驱逐策略来约束内存占用。只配其中一个,另外一个迟早给你挖坑。
4. 实战五大坑:配置错一个,线上就要抖三抖
概念和技术规范讲清楚了,下面聊聊我这么多年在各种项目里见到的真实问题。这五个坑,每一个我都亲眼见过它们在线上引发事故,每一个的根源都可以追溯到“没分清驱逐和过期”。
4.1 坑一:为了“防OOM”把所有key的TTL设得很短
这是我见过最多的一种误用。某团队发现服务的本地缓存内存占用越来越高,眼看要OOM,负责人一拍脑袋:把缓存TTL从1小时改成1分钟,数据过期了自然就释放了。
结果呢?缓存命中率从90%一路跌到不到30%,底层数据库的查询量暴增,接口的P99延迟直接翻了好几倍。为什么?因为TTL解决的是“数据该不该信”,不是“内存够不够放”。你把TTL改短,只会让大批仍有业务价值的数据提前作废,业务请求无法命中缓存,全部穿透到下游,下游根本扛不住。
正确的做法是:内存问题用驱逐策略解决。设置maxEntries,配置合理的LRU策略,当容量达到上限时,自动把最不常用的数据腾出去。TTL保持业务上合适的时间,不要拿它当容器清理工具。
4.2 坑二:只设过期不设驱逐,内存照样爆掉
反过来,也有团队对TTL有迷之信任,认为“设了过期就不会OOM”。
这里有两个盲区:
第一,惰性过期机制下,过期的条目如果不被访问,它就会一直躺在内存里“装死”。后台清理线程如果没开或者扫描频率不合理,内存压力并不会因为条目标记过期而自动减轻。
第二,如果你用的是滑动过期策略,热点数据每次被访问都会续期,相当于它永远不会过期。如果一个热点key对应一个几百KB的大对象,这个对象可以长期占着空间,把其他冷数据全部挤掉。如果容器还没配驱逐策略,那内存最终还是会涨爆。
所以结论很简单:过期和驱逐不是二选一,它们是两道不同的防线,必须同时存在。过期负责逻辑有效性的判断,驱逐负责物理空间的红线控制。
4.3 坑三:驱逐后的“信息丢失”没有兜底
有一些业务代码是这样写的:先查缓存,如果缓存里有值,直接返回;如果缓存里没有值,就当作“数据不存在”,返回空结果。
这种做法在纯“过期”场景下勉强还能接受,因为TTL是业务自己定的,你可以保证“没过期就一定有值,过期了说明业务上不需要了”。但一旦引入驱逐策略,这个假设就崩了。
被驱逐的key没有过期,它的值在业务上依然是有效的,只是缓存容器把它挤掉了。业务方再去get,拿到是miss,然后返回空结果——这就是一个潜在的数据错误。更常见的是,业务方会在缓存miss后重新加载数据,但如果加载逻辑没有做好并发控制,大量请求同时miss,就形成了缓存击穿。
正确的做法是:不要把缓存当作数据源本身,而是当作加速层。缓存miss是正常现象,必须依赖CacheLoader或者业务侧的显式回源逻辑去加载真正有效的数据。被驱逐的key和过期的key,对业务而言都应该走“加载”而不是直接返回空。
4.4 坑四:热点Key + 滑动过期,内存被“永久热点”占满
这个坑相当隐蔽。某项目用滑动过期给用户会话数据续期,理论上一个用户只要持续活跃,他的会话一直有效。看起来没问题,但实际运行一段时间后发现,活跃用户只有几万人,缓存却被塞得满满当当,驱逐次数异常频繁。
原因在于,活跃用户的会话每次访问都续期,这些条目的“最近访问时间”永远是最新的,LRU驱逐策略几乎永远选不中它们。结果就是:少数热点条目垄断了缓存空间,大量虽然没过期但访问频率低的冷数据反复被驱逐和重新加载,I/O消耗反而更大。
说到底,这还是过期和驱逐协同的问题。滑动过期让热点条目“永生”,而常规的LRU驱逐又特别容易保留“最近访问过”的条目,两者叠加之后,空间被少数条目锁死。解法不外乎几种:给单条目的最大容量设限制;对超大value单独设置缓存,不让它与其他数据混在一起;或者选用考虑TTL信息的混合驱逐策略,优先淘汰剩余生存时间短的条目。
4.5 排查实录:怎么快速判断线上问题是驱逐还是过期引发的
当线上缓存命中率出现异常时,第一步不是去看代码,而是先判断到底发生了什么。这是排查方向的问题,方向错了,后面全白费。
先看监控指标,把下面几个数据拉出来:
| 指标 | 含义 | 判断口径 |
|---|---|---|
| 缓存命中率 | 命中次数 / 总访问次数 | 下降说明有效条目不满足业务访问 |
| 驱逐次数 | 触发驱逐的累计次数 | 持续上涨说明容量压力大 |
| 过期次数 | 因过期被移除的条目数 | 持续上涨说明TTL设置偏短 |
| 缓存Size曲线 | 当前有效条目数/内存占用 | 持续上涨说明存在条目不清理的情况 |
判断逻辑并不复杂:
- 如果驱逐次数很高、命中率下降、同时size被压在容量红线附近,说明是容量问题,请调大容量或优化驱逐策略,而不是去动TTL。
- 如果过期次数很高、命中率下降、但size远没有到容量上限,说明是TTL偏短,业务数据经常性提前作废,需要调大TTL或改用滑动过期。
- 如果过期次数不高、驱逐次数也不高,但size持续上涨,说明你的后端扫描频率不够,大量的“逻辑过期但物理占用”条目在堆积,需要检查后台清理任务。
还有一个特别实用的小技巧:在开发环境中给缓存访问加日志,当get返回miss时,如果框架支持记录miss的原因(EXpired/EVICTED),就把原因打到日志里。线上环境不用全量开启,按比例采样即可。拿到一份实际miss原因的分布数据之后,“过期问题还是驱逐问题”就不再是靠猜的了。
5. 面试作答思路:怎样把这题答出“深度”
如果你是在准备面试,下面这套答题框架可以直接用。它的核心优势在于层次分明,从定义到触发再到规范,层层递进,每层都能体现你对缓存体系的理解。
5.1 一个可复用的四层答题框架
第一层,先亮定义。过期是时间维度的数据有效期管理,驱逐是空间维度的容量管理。一句话就可以定调,显示出你对两者定位的清晰认识。
第二层,讲触发机制。过期的触发点是时间检查,常见的是读路径上的惰性检查加后台主动扫描;驱逐的触发点是容量判断,主要在写入路径上发生,容器满时按策略淘汰条目。
第三层,结合规范。在JCache里,过期有标准的ExpiryPolicy接口,逐条目、按状态决定生命周期;驱逐则没有统一的标准API,主要由具体实现承载。这能反映出你不只会背Redis命令,还了解Java缓存规范的设计取舍。
第四层,补充工程经验。讲一个你自己做过的实际调优或者你观察到的线上事故,说明两者如何协同。哪怕只是把上面“坑四”的滑动过期配合LRU导致热点独占空间的案例讲出来,都会让面试官觉得你不是纸上谈兵。
5.2 面试官眼中的加分细节
除了框架,还有几个细节能让你在众多候选人中明显脱颖而出:
第一,主动提出惰性删除和主动删除的权衡。能说出“惰性删除省CPU但占用内存,主动删除费CPU但内存释放及时”,说明你理解缓存实现不是只有一种路径。
第二,提到近似LRU的工程折中。在高并发场景下,精确LRU的维护成本高,不少高性能实现会采用采样淘汰,这会是一个让面试官记住你的亮点。
第三,提出驱逐策略可以感知过期信息。回答中如果能提到“某些引擎的驱逐策略会把剩余TTL纳入考量,优先淘汰即将过期的条目”这个混合机制,基本就触到了这两个概念协同的最高点。
第四,联系到缓存加载器。主动提到CacheLoader在错过缓存后负责回源加载,说明你理解缓存miss之后的完整链路,而不只是看内存里的那一下。
5.3 为什么要避开这些“扣分回答”
有些回答虽然表面沾边,但恰恰会暴露概念不清:
- “过期是被删了,驱逐也是被删了嘛”——这是最典型的扣分回答,只看到表象。
- “驱逐是过期的一种”——直接判死刑,前面所有铺垫都白费。
- “设了过期就不会OOM”——说明完全不理解过期和物理内存释放是两回事。
反过来,你只需要在概念上把握住“时间”和“空间”这两个维度的对立统一,把这题答出区分度就完全不难了。
最后多说一句我个人的体会。很多人研究缓存时总喜欢研究那些花哨的高级用法,比如xx技术的缓存、异步刷新之类的,但我一直觉得,把“过期”和“驱逐”这两个最基础的概念吃透,才是真正能把缓存调好的人。你以为你在处理缓存问题时是在调参数,其实你是在权衡“数据新鲜”和“内存成本”这两个基本矛盾。概念通了,看什么缓存框架都会很快,遇到线上诡异问题也能快速定位方向。面试只是顺便检验一下你有没有这个概念而已。