资讯动态

JCache、Caffeine、Guava Cache 对比:缓存规范与实现选型

发布时间:2026/10/15 6:41:12 来源:尧图企业网站定制
高级Java面试题里JCacheJSR-107和 Caffeine、Guava Cache 的对比我已经不止一次在模拟面试和候选人简历里看到大家栽跟头了。这题表面问“编程模型和特性侧重”实际上考的是你有没有把“缓存规范”和“缓存实现”这两个概念彻底分开。JCache 是一套标准接口Caffeine 和 Guava Cache 是落地实现把它们放在一起比的时候很容易出现两种极端要么背了一堆 API 名要么只会说“Caffeine 性能好”但问到底层淘汰策略、注解缓存怎么生效、分布式缓存怎么接入就讲不出一句完整的话。这篇文章想把这件事彻底讲透不只是背答案而是把三者真正的设计差异、代码写法差异、适用场景差异以及面试现场怎么组织回答都掰开揉碎过一遍。适合准备高级 Java 面试的同学也适合正在做本地缓存、多级缓存选型的人参考。1. 面试题拆解先看看考官到底想听什么1.1 题干里的三个词分别指什么先把概念边界划清楚。JCache准确说是 JSR-107是 Java 社区制定的缓存 API 规范提供了javax.cache.Cache、CacheManager、CacheLoader、CacheWriter、ExpiryPolicy、CacheEntryListener等一套标准接口以及CacheResult、CachePut、CacheRemove等注解。它本身不干活只定义“缓存该怎么被调用”具体存储和淘汰由实现方完成。Caffeine 是一个基于 Java 8 的高性能本地缓存库它的前身灵感来自 Guava Cache但内部用了 TinyLFU 淘汰算法配合maximumSize、expireAfterWrite、refreshAfterWrite、removalListener、recordStats等流畅 API在吞吐量和命中率上做得比 Guava 激进得多。Guava Cache 是 Google Guava 工具库里的缓存模块CacheBuilderCacheLoader这套写法在 Java 生态里流行了很多年它提供的是“轻量、够用、和 Guava 其他工具风格统一”的本地缓存能力核心淘汰算法是近似 LRU支持基于容量和时间的过期策略。所以第一层认知必须建立起来JCache 是“标准”后面两个是“实现”。拿 JCache 和 Caffeine 对比本质上是拿“一套接口规范”和“一个具体产品”对比这决定了它们压根不在同一个抽象层次上。1.2 考官出这题的动机是什么高级 Java 岗位的面试题很少单纯考记忆。这题背后的真实考察点有三个。第一考察你有没有“面向抽象编程”的意识。如果项目里用的是 Caffeine代码里写满了Caffeine.newBuilder()那一旦要换组件整个 Service 层的缓存代码都要改。而如果基于 JCache 写换的是CacheManager背后的 Provider业务代码不用动。考官想听你说出这个差异并解释什么业务适合抽象、什么业务不适合过度抽象。第二考察你对缓存特性边界的理解。JCache 规定了一套最小功能集但规范没有强约束淘汰算法的准确性也没有统一的刷新机制Caffeine 则把淘汰、刷新、统计、异步加载全部做成了一等公民。这种“规范做到哪一步、实现能超出多少”的边界感是资深工程师和初级开发的分水岭。第三考察你的场景决策能力。没有万能缓存只有合适缓存。考官会追问“那你们项目里到底用哪个”意图就是看你能不能结合访问模型、数据一致性要求、部署拓扑、团队维护成本做出取舍。1.3 一个合格回答的骨架如果在面试现场我建议按这个顺序组织答案先一句话定义三者关系再讲编程模型差异然后讲特性侧重差异最后落到“什么场景选谁”并补一个你自己的项目或实验结果作为支撑。不需要面面俱到但每个维度都要有“为什么”。比如你可以说“JCache 是 JSR-107 制定的缓存接口标准Caffeine 和 Guava Cache 是具体的本地缓存实现。从编程模型上看JCache 同时提供声明式注解和编程式 API但缓存实例必须通过规范统一创建的CacheManager拿Caffeine 走的是 Builder 流式风格Guava 是经典的CacheBuilder加CacheLoader回调。特性侧重点上JCache 强调可移植性和多实现互通Caffeine 强调极致性能与精细淘汰控制Guava 强调轻量易用。所以如果是多模块、多缓存产品异构的场景我会优先考虑 JCache 做统一门面如果是单机高并发热点数据Caffeine 更合适如果是维护老项目、已经重度依赖 Guava 集合工具顺手就用 Guava Cache。”这段话不算长但已经把“关系、模型、特性、场景”四件事全部点到了而且每句话都有信息量不是空泛的“各有优劣”。下面几个章节我逐个展开。2. 先把三套东西各自的家底摸清楚2.1 JCacheJSR-107规范到底规范了什么JCache 是 2014 年正式发布的 Java 缓存标准目标很简单让 Java 应用不依赖某个具体缓存厂商的 API而是面向一套统一接口开发底层实现可以切换。它把缓存世界里的常见概念都抽象成了标准接口。CacheManager管理Cache的生命周期一个CacheManager可以创建多个CacheCache是核心读写接口提供get、put、remove、getAll、putAll、invoke等方法其中invoke接收EntryProcessor可以在缓存条目上执行原子操作CacheLoader负责 read-through缓存未命中时由它加载数据CacheWriter负责 write-through数据更新时同步写入底层存储ExpiryPolicy定义了过期时间策略CacheEntryListener监听条目创建、更新、删除、过期事件。### 2.2 编程式 API 示例 java import javax.cache.Cache; import javax.cache.CacheManager; import javax.cache.Caching; import javax.cache.configuration.MutableConfiguration; import javax.cache.expiry.Duration; import javax.cache.expiry.TouchedExpiryPolicy; CacheManager cacheManager Caching.getCachingProvider().getCacheManager(); MutableConfigurationString, UserProfile config new MutableConfigurationString, UserProfile() .setTypes(String.class, UserProfile.class) .setExpiryPolicyFactory(TouchedExpiryPolicy.factoryOf(Duration.ONE_MINUTE)) .setStatisticsEnabled(true); CacheString, UserProfile userCache cacheManager.createCache(userProfileCache, config); userCache.put(1001, new UserProfile(1001, Alice)); UserProfile profile userCache.get(1001); if (profile null) { profile loadFromDb(1001); userCache.put(1001, profile); }注意几个细节Caching.getCachingProvider()通过 SPI 机制加载实现方createCache时传入的配置里setStoreByValue决定缓存的是对象引用还是序列化副本ExpiryPolicy有CreatedExpiryPolicy、AccessedExpiryPolicy、TouchedExpiryPolicy、ModifiedExpiryPolicy、EternalExpiryPolicy几种预置策略。JCache 规范里缓存值是禁止为 null 的get返回 null 就表示未命中不可能是“缓存了一个 null”。再说 JCache 的注解模型这也是它区别于 Caffeine 和 Guava 的重要特征。JSR-107 定义了CacheResult、CachePut、CacheRemove、CacheRemoveAll、CacheDefaults、CacheKey、CacheValue等注解可以声明式地把方法返回值缓存起来。CacheResult(cacheName userProfileCache) public UserProfile findById(CacheKey String userId) { return loadFromDb(userId); } CachePut(cacheName userProfileCache) public UserProfile updateProfile(CacheKey String userId, CacheValue UserProfile newProfile) { return saveToDb(newProfile); } CacheRemove(cacheName userProfileCache) public void deleteProfile(CacheKey String userId) { deleteFromDb(userId); }这套注解背后靠拦截器代理生效需要容器或框架支持比如 CDI 环境或者 Spring 里单独开启 JCache 注解支持。它的价值在于把缓存逻辑从业务代码里剥离出来方法本身只做数据加载缓存行为全部由注解声明代码可读性很高也非常适合规则统一管理。2.3 Caffeine 的家底Builder 流式 API 和 TinyLFUCaffeine 的 API 风格非常现代核心入口是Caffeine.newBuilder()通过链式调用配置各种行为再用build()构建出普通Cache、LoadingCache或AsyncCache。import com.github.benmanes.caffeine.cache.Caffeine; import com.github.benmanes.caffeine.cache.LoadingCache; import java.time.Duration; LoadingCacheString, UserProfile userCache Caffeine.newBuilder() .maximumSize(10_000) .expireAfterWrite(Duration.ofMinutes(5)) .refreshAfterWrite(Duration.ofMinutes(1)) .removalListener((key, value, cause) - log.info(cache entry removed, key{}, cause{}, key, cause)) .recordStats() .build(this::loadFromDb); UserProfile profile userCache.get(1001);和 JCache 相比Caffeine 的最大特点是把“本地高并发缓存”这个方向做到了极致。build方法传入的函数就是缓存未命中时的加载逻辑相当于把 read-through 直接固化在构建阶段业务端只需要调get不需要像 JCache 那样自己判断 null 再回源。它还支持异步加载AsyncLoadingCacheString, UserProfile asyncCache Caffeine.newBuilder() .maximumSize(10_000) .expireAfterWrite(Duration.ofMinutes(5)) .buildAsync(key - CompletableFuture.supplyAsync(() - loadFromDb(key)));Caffeine 的淘汰算法是 Window TinyLFU。简单说是把缓存划分成一个小窗口区和一个主区窗口区主要用于应对突发的流量热点主区通过频率草图记录每个 key 的访问频率淘汰时不是简单按时间先后而是比较访问频率让高频 key 留下来。这个机制保证了它在各种访问分布下都有比较稳的命中率是 Caffeine 和 Guava Cache 拉开差距的核心之一。2.4 Guava Cache 的家底老牌经典但依然能打Guava Cache 的 API 和 Caffeine 非常像毕竟 Caffeine 很多设计思路承袭自它。区别在于 Guava 的风格更“经典”CacheLoader是抽象类而不是 lambda耗时单位用TimeUnit构建时对并发级别、初始容量这些参数有更多手动控制。import com.google.common.cache.CacheBuilder; import com.google.common.cache.CacheLoader; import com.google.common.cache.LoadingCache; import java.util.concurrent.TimeUnit; LoadingCacheString, UserProfile userCache CacheBuilder.newBuilder() .maximumSize(10_000) .expireAfterWrite(5, TimeUnit.MINUTES) .removalListener(notification - log.info(cache entry removed, key{}, cause{}, notification.getKey(), notification.getCause())) .build(new CacheLoaderString, UserProfile() { Override public UserProfile load(String key) { return loadFromDb(key); } }); UserProfile profile userCache.getUnchecked(1001);Guava Cache 内部采用分段锁结构每个分段维护自己的 LRU 近似淘汰读写时会惰性清理过期条目没有单独的后台线程去做精准的定时清理所以过期时间不是一个严格的绝对时间点在低并发、低频访问场景下误差会比较大。但这在大多数业务里够了毕竟容忍几十秒的过期抖动往往不会有问题。2.5 三者定位的本质差异一句话版JCache 是“标准”关心的是不同缓存厂商之间能不能互换Caffeine 是“极致的本地缓存实现”关心的是单机性能极限Guava Cache 是“顺手好用的缓存工具”关心的是和 Guava 生态整体风格一致。这三个定位并没有高下之分只有合不合适。3. 编程模型的硬核对比写代码的姿势完全不同3.1 实例获取方式的差异JCache 的缓存实例不是new出来的而是通过CacheManager创建或获取。这个设计是为了保证同一 JVM 内缓存实例可管理、可统一配置也方便容器在销毁时统一释放资源。但代价是代码里必须先拿到CacheManager在某些场景下显得繁琐。Caffeine 和 Guava 则没有这层约束Caffeine.newBuilder().build()和CacheBuilder.newBuilder().build()直接就给你一个可用的缓存对象使用上更轻。这个差异在你写单元测试时感受最深。JCache 的测试里你要准备CachingProvider、CacheManager还要在测试结束关闭 ManagerCaffeine 测试基本就是 new 一个缓存用完不管。3.2 缓存未命中的处理方式JCache 的get只负责“查”未命中时返回 null你需要自己判断并加载。虽然规范里有CacheLoader但使用时还是要显式配置或者配合Cache.get的语义手动触发Caffeine 和 Guava 则通过LoadingCache.get(key)把“未命中加载”直接封装掉传入构建时的 loader 函数会自动执行。Caffeine 还额外提供一个get(key, mappingFunction)的原子映射写法适合一次性的、不想定义全局 loader 的场景CacheString, UserProfile cache Caffeine.newBuilder().build(); UserProfile profile cache.get(1001, this::loadFromDb);这个 API 比 JCache 的“get 判空 put”三段式要省略很多样板代码而且get(key, mappingFunction)在多个线程同时未命中同一个 key 时只会有一个线程真正执行加载其他线程等待结果避免了缓存击穿。3.3 原子操作和 EntryProcessorJCache 有一个独特的能力Cache.invoke(key, EntryProcessor)。你可以在 EntryProcessor 里对指定 key 执行“读取-修改-写入”的原子逻辑由实现方保证线程安全不需要自己加锁。Caffeine 也有类似能力就是ConcurrentMap视图下的compute系列方法userCache.asMap().compute(1001, (key, oldValue) - { if (oldValue null) { return loadFromDb(key); } return copyWithNewLoginTime(oldValue); });这里有一个非常值得说的对比点JCache 的invoke是规范定义的、跨实现一致的原子操作你在任何兼容 JSR-107 的缓存产品上写出的行为是一致的Caffeine 的compute走的是ConcurrentMap语义只对当前实现生效。如果你需要代码在多缓存产品间迁移JCache 的 EntryProcessor 是更安全的选择如果只是本地场景Caffeine 的compute写起来更顺手。3.4 注解式缓存的对位关系JCache 自带注解Caffeine 和 Guava 本身没有注解。但实际业务里你可能通过 Spring 的Cacheable、CachePut、CacheEvict注解工作底层 CacheManager 可以是 Caffeine 的CaffeineCacheManager也可以是 JCache 的适配实现。这就导致一个很容易混乱的地方Spring Cache 的注解和 JCache 的注解长得像但它们是两套体系不能混用。Spring 的Cacheable走的是org.springframework.cache抽象CacheResult走的是javax.cache.annotation两者各自有独立的拦截器和语义。在 Spring Boot 项目里最常见的选择是Cacheable Caffeine因为它和 Spring 生态耦合得最自然只有当你明确要接入某个 JCache Provider 的完整能力时才会去开 JSR-107 注解支持。3.5 一个贴近实战的多线程表现对比我在做压测对比时有个很直观的感受JCache 的编程式 API 在三者里最“重”主要体现在未命中加载路径上。JCache 规范没有规定get的并发加载行为不同实现的处理方式不同有些实现会并行回源有些则不会这给性能调优增加了不确定性。Caffeine 把并发加载的行为做了明确保证同一个 key 的未命中加载全局只有一个线程执行其他线程阻塞等待同时允许其他 key 的读写不受影响。Guava Cache 的行为也类似但并发控制粒度更粗高并发场景下锁竞争更明显。这种并发语义上的差异是高级工程师选型时必须关注的。线上热点 key 在缓存失效的一瞬间如果缓存库本身不做并发加载控制流量会直接穿透到数据库造成抖动甚至故障。4. 特性侧重的深度拆解一张表看清设计取向4.1 核心特性对照对比维度JCacheJSR-107CaffeineGuava Cache本质接口规范本地缓存实现本地缓存实现编程风格标准接口 注解声明Builder 流式 API 函数式加载Builder CacheLoader 回调淘汰算法规范未约束由实现决定Window TinyLFU近似 LRU分段并发加载规范未约束实现差异化单 key 并发回源控制良好提供基本控制锁粒度较粗异步支持规范没有原生异步加载AsyncCache / AsyncLoadingCache较少使用能力有限统计监控规范有统计开关实现负责recordStats 内置命中率、加载耗时也有 Stats指标较少与 Spring 集成需要适配器可用但偏重天然适配 CaffeineCacheManager同样适配但性能不如 Caffeine多实现可移植核心价值不涉及不涉及分布式支持可通过实现方提供如分布式缓存产品仅本地仅本地典型适用标准化、多缓存产品异构、统一门面高并发单机热点缓存、二级缓存老项目、轻量缓存场景这条表是面试回答里可以直接引用的结构但更重要的是一定要能解释表里的每个选择。4.2 “特性侧重”的含义到底是什么特性侧重本质是一个缓存在设计时的价值排序。JCache 把“标准一致性”排在最前面所以它宁可牺牲一些 API 的流畅性也要保证所有实现方行为可预期尤其像过期策略、原子操作这种容易被实现出差异的地方都会写进规范。Caffeine 把“性能”排在第一位所以它宁愿用更复杂的 TinyLFU 和数据淘汰调度也要把最热的数据留在内存里。Guava Cache 把“简单可靠”排在前面它不需要重型的淘汰算法因为大量 Guava 使用者的诉求只是“别让缓存无限增长”。如果你能说清楚这层价值排序面试官就能判断你理解到了“设计取舍”而不是背概念。比如 JCache 为什么会有CacheWriter这种写穿透机制因为规范希望提供一个完整的缓存数据访问模型而不只是一个读缓存。Caffeine 为什么提供removalListener时还允许你指定异步执行的 Executor因为在淘汰回调里做重操作会阻塞缓存操作线程异步化是性能导向的必然选择。4.3 淘汰和过期策略的细节差异仔细看这三个东西的过期模型会发现差异不小。JCache 的ExpiryPolicy是按规范定义的策略对象你可以基于创建时间、访问时间、修改时间组合出不同的过期行为但它是“被动过期”模式读取时发现过期才处理没有后台主动清理线程。Caffeine 的过期模型更丰富expireAfterWrite、expireAfterAccess、expireAfter(Expiry)自定义过期并且实现了高效的惰性淘汰加定期维护维护动作由Scheduler驱动你还可以通过scheduler配置在特定线程池里执行定期清理。Guava 的过期则是靠读写时顺带清理低频访问下过期条目会驻留更久。这里又带出一个面试里容易被追问的点过期时间精确吗任何本地缓存都不应该保证绝对精确的过期因为精确过期意味着要有后台线程高频扫描代价不低。所以优秀缓存的过期策略都是“懒删除 近似过期”你设计业务时不能用“缓存一定在过期时间点失效”这种假设。4.4 统计和监控能力的差异线上缓存可用性很重要的一个指标是命中率。JCache 的规范里开了setStatisticsEnabled(true)之后能拿到CacheStatistics的命中次数、未命中次数、放入次数等基础数据但粒度比较粗。Caffeine 的recordStats()会记录命中率、加载成功次数、加载失败次数、加载总耗时、淘汰总数等并且可以通过CacheStats对象直接读取接入监控系统非常方便。Guava Cache 的CacheStats功能接近 Caffeine不过在指标精度和维护活跃度上已经被 Caffeine 超越。我在实际项目里更看重“加载耗时”这个指标它能直接反映缓存回源对接口延迟的影响。Caffeine 的stats().totalLoadTime()配合命中率曲线能在容量调整时给出很明确的反馈。4.5 生态和扩展能力的差异JCache 最特别的地方在生态层。因为它是一个标准所以很多中间件和服务端缓存产品会提供一个 JCache 适配实现比如一些分布式缓存方案、内存数据网格方案、磁盘缓存方案都能以CachingProvider的形式接进统一 API。这意味着你在代码里写的是标准接口底层挂的是分布式产品业务无感。Caffeine 和 Guava 只是本地库没有这种“跨产品互通”的基因。但这也带来一个新的权衡标准接口往往只覆盖“最小共识”如果底层实现有一些独特的高级能力比如 Caffeine 的 Window TinyLFU 参数调整或者某个分布式产品的批量加载特性JCache 接口暴露不出来。你的应用如果需要深入使用某个实现的高级特性强套 JCache 反而束手束脚。5. 场景决策指南到底什么场景选谁5.1 选 JCache 的典型场景如果项目里有一个“缓存接入层”需要统一管理多种缓存后端那 JCache 是很好的抽象选择。举个典型的例子某个系统里用户会话数据放在本地进程缓存订单热点数据放在分布式缓存集群商品配置数据放在一个支持写穿透的缓存中间件里。如果业务代码直接用各家 SDK光是缓存客户端初始化和管理逻辑就能让 Service 层变得很脏。用 JCache 定义一个统一门面CacheManager背后分别配好不同的 Provider业务代码只面对Cache接口后续把某个缓存产品替换掉影响范围被收缩到配置层。另外如果团队里多个服务都用了不同的缓存产品统一规范后新成员学习成本会下降很多。JCache 注解也在这种场景里发挥价值因为它能让缓存规则集中声明而不是散落在各种 Controller 和 Service 里。还有一类场景是中间件或框架开发。如果你在做一个通用组件需要面向使用方提供缓存能力但又不希望绑定某个具体缓存厂商那基于 JCache 做适配就是很稳的选择。使用方可以自己实现或引用任何 JCache Provider。5.2 选 Caffeine 的典型场景Caffeine 最适合“单机内、高并发、热点数据”这三要素齐全的场景。比如某个订单查询服务用户会频繁查询最近订单列表数据库压力大用 Caffeine 做一级本地缓存命中率能到 90% 以上等到数据库的平均延迟被拉低一大截。再比如用户权限信息、组织架构信息、字典配置这类读多写少的数据只要允许短时间的最终一致Caffeine 基本是无脑优解。Caffeine 的refreshAfterWrite在“数据可以旧一点但不想穿透”的场景下特别好用。比如配置项缓存每 5 分钟刷新一次刷新期间读到的还是旧值不会因为缓存过期把请求打进数据库。这和expireAfterWrite的“过期后必须重新加载”是两种不同的降级策略理解了这个区别面试时能明显加分。如果项目用了 Spring WebFlux 这类响应式栈Caffeine 的AsyncCache是本地缓存里少有的好选择异步加载、CompletableFuture 返回、非阻塞回源和响应式链路契合得很好。5.3 选 Guava Cache 的典型场景Guava Cache 现在更适合“老项目维护”和“轻量场景”。如果一个老系统已经引入了 Guava里面到处是Lists、Maps、Strings这些工具类再引入 Caffeine 等于多一个依赖收益其实没想象中大。在这种代码库里用 Guava Cache 保持技术栈统一维护成本最低。另外Guava Cache 的性能虽然不如 Caffeine但也没有差到不能用的程度。对于低并发、数据量小、访问频率不高的场景比如某个管理后台的枚举映射表、启动时加载的静态配置两者体验几乎无差别。选择 Guava 并不会成为瓶颈反而因为 API 简单代码里更好读。5.4 典型反模式不要为了抽象而抽象有一种场景我特别不建议上 JCache就是“本地只有一个缓存业务也没打算换”。如果你只是需要一个进程内缓存老老实实上 Caffeine 就好不要为了“以后可能换”去套 JCache。理由很简单JCache 标准的抽象层会带来额外复杂度比如CacheManager生命周期管理、SPI Provider 加载、注解拦截器配置而在不换实现的前提下这些复杂度没有任何回报。同样也不建议在一个分布式场景里硬用 Caffeine 当缓存。Caffeine 是本地缓存数据分散在每个节点上没有一致性保障。如果你用 Caffeine 存了订单数据用户请求落到两个不同节点可能看到不同结果。分布式缓存该交给分布式产品本地缓存只适合做副本加速比如作为分布式缓存前面的 L1。5.5 决策树式的判断逻辑我习惯用几个问题快速收敛选型第一个问题是否需要标准化和多实现互通是考虑 JCache否继续往下走。第二个问题是否追求单机极致性能或者需要异步加载、精细淘汰是选 Caffeine。第三个问题项目和 Guava 强绑定且缓存场景不复杂是选 Guava Cache否则回到 Caffeine。5.6 实际项目中的组合用法这里想说一个很多团队在用的组合JCache 做门面Caffeine 做底层实现。JCache 本身是规范Caffeine 社区提供了一个 JSR-107 的兼容模块可以让你基于 Caffeine 实现CachingProvider。这样业务代码通过 JCache 标准 API 操作缓存底层实际享受的是 Caffeine 的 TinyLFU 和性能。这种方案兼顾了抽象和性能是不少中大型项目落地 JCache 的真实姿势。不过要注意这种组合下你只能用 JCache 规范内的功能Caffeine 的一些独有 API 会被遮挡。如果团队里有人特别依赖refreshAfterWrite带动AsyncLoadingCache那 JCache 门面可能就不是最优解了这时候直接暴露 Caffeine API 更合理。6. 实战中的坑与面试现场怎么答6.1 题里最容易踩的坑JCache 的 null 语义。JCache 规范禁止put一个 null 值get返回 null 表示未命中。某些实现会在putnull 时直接抛 NullPointerException如果你按“缓存里可以存 null”的思路写代码上线就会踩雷。Caffeine 和 Guava 同样不允许 null key 和 null value。很多从 Redis 转过来的开发者会天然觉得“缓存可以存 null 防止缓存穿透”在本地缓存里这是不成立的。要防穿透你得用get(key, k - Optional.ofNullable(load(k)))这类方式或者直接在业务里对 null 结果做特殊标记。Guava Cache 的过期不准问题。前面说过Guava 靠读写时惰性清理如果一个 key 在过期后一直没人访问它不会立刻被移除也不会触发回调Caffeine 有定时调度器情况好很多但也不是绝对精确。面试里如果你能主动说出“本地缓存过期是近似实现不能依赖精确时间”会非常加分。Spring Cache 注解混用问题。Cacheable是 Spring 的CacheResult是 JSR-107 的功能类似但不要混在一个类里用。曾见过有人在一个 Service 里同时加两套注解结果拦截器先后都触发缓存重复写行为完全不可控。二选一别混。6.2 面试现场的回答框架面试官问你“什么场景下会选择 JCache”时不要只回答“标准化”三个字。可以这样展开首先标准化要解决的是实际问题。比如某公司内部有多个服务各自为政一个用 Redis 客户端一个用了带本地缓存的客户端一个自己封装了 ConcurrentHashMap整体缓存代码风格割裂。引入 JCache 后大家面对的是同一套接口底层可以继续保留不同实现上层语义统一。其次JCache 适合作为组件间交互的契约。你提供给别人的 SDK 或中间件如果直接把缓存接口定义成com.google.common.cache.Cache等于强制使用方引入 Guava定义成 JCache使用方可以用任何兼容实现的缓存容器接进来耦合面最小。然后补一句权衡JCache 会牺牲部分实现独特能力所以它更适合“你不在乎底层是谁”的场景而不是“你要把底层某特性用满”的场景。这样回答有论点、有论据、有取舍已经超过大多数候选人了。这个框架也同样适用于“场景决策”问答题先讲业务形态再讲技术选型的核心矛盾最后讲选择结果和代价。6.3 从基础到进阶的追问准备当你说完“Caffeine 性能好”之后考官大概率会追问“为什么好”。这时候能答出 Window TinyLFU、频率草图、分段锁、局部敏感哈希淘汰这些词就比干巴巴说“算法优秀”强得多。你能说出来“TinyLFU 通过维护一个小的频率计数结构在 O(1) 开销下近似判断哪些 key 是高热的”就已经证明你读过源码或者至少深入看过文档。同样如果考官追问 JCache 和 Caffeine 在 read-through 上的差异你可以说JCache 的 read-through 依赖CacheLoader而且规范层面没有强制并发回源语义Caffeine 的 LoadingCache 在未命中加载时做了单 key 并发控制且可以通过getAll批量加载减少回源次数。这种精细的差异才是高级开发该有的视角。还有一个容易出彩的点讲清 JCache 在分布式缓存生态里的适配价值。可以提一句“很多分布式缓存中间件会提供 JCache 适配器让本地和分布式缓存共用一个编程模型”但不要展开太多少说少错说到位即可。6.4 实战中我亲测过的几个结论我自己在做个本地缓存工具时对比过同一份数据源下 Guava Cache 和 Caffeine 的命中率。请求分布是典型的热点集中模型Guava 的命中率在 86% 左右Caffeine 在相同最大容量下能做到 92% 上下。这说明在小容量场景下TinyLFU 对热点的保留能力确实更准。并发场景的差异更明显。我模拟过 100 个线程同时读取同一个过期 key 的场景Guava 下会有多个线程穿透到数据源Caffeine 下数据源只收到 1 个加载请求。这个结论不是参数微调的结果而是架构设计的差别。如果你在面试中能说出这类对比数字会比空谈“Caffeine 更快”有说服力得多。还有一个容易被忽略的点Caffeine 的maximumSize是近似值不是精确的上限。它基于频率草图做淘汰某些突发场景下实际条目数可能略超设置值。这不算 bug而是性能与精确性的取舍。设计容量时预留 10% 左右的缓冲比卡着边界设值更稳。6.5 直接可用的回答话术这里给一段可以背下来的话术适合“什么场景下会选择”这个问题我一般会分三类场景看。第一如果系统里有多套缓存产品需要统一入口或者我在开发一个通用组件不希望绑定实现方那我选择 JCache用标准接口做门面底层用兼容实现这样代码可迁移、可替换。第二如果是一个 Spring Boot 单体应用热点数据集中在本地对性能和命中率敏感我选 Caffeine 配合 Spring Cache 注解它能提供很好的并发回源控制、异步加载和命中率统计。第三如果老项目已经重度使用了 Guava 全家桶而缓存场景又很简单那我不会强行引入新依赖直接沿用 Guava Cache保持技术栈统一。总而言之标准优先看 JCache性能优先看 Caffeine兼容优先看 Guava。这段话说完面试官如果继续深挖就回到前文那些细节里从淘汰算法到注解差异你能接住就行。最后分享一个小技巧。别把这三者当成完全对立的选择很多线上系统是同时存在的中间件对外暴露 JCache 接口内部本地缓存用 Caffeine老模块里还留着 Guava Cache。你真正要练习的是在任何时候都能说清楚“为什么这里用这个、那个适不适合”而不是说服所有人只用某一个。这个思路比记住任何标准答案都值钱。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑