Java 缓存精要
实现更低延迟、降低成本并赋能智能体架构
作者:GRANVILLE BARNETT
架构师,HAZELCAST
缓存技术在系统中的作用日益重要,对于大规模解锁众多用例至关重要。几十年来,缓存已实现低成本、可扩展地访问会话状态和数据存储等信息。更现代的缓存用例正在实现低成本、可扩展的工具链,并在智能体架构中实现嵌入生成,这正在解锁下一代系统创新。
本参考资料卡介绍了使用 Java 的 JCache(Java 临时缓存 API)将缓存融入系统的方法。文中首先讨论了缓存的基础知识,然后通过代码示例简要介绍了 JCache API,最后总结了缓存部署架构。
缓存概述
缓存是先前计算结果的一个存储,以便可以省略后续计算。理解缓存最简单的方式是将其视为键值存储:对于给定的输入(键),输出(值)代表先前基于该输入计算出的结果。
缓存命中 表示特定数据存在于缓存中,这种情况下可以使用其值。否则,就会发生缓存未命中,此时需要执行相关计算并将其输出放入缓存。缓存未命中的代价可能除了昂贵的计算操作外,还涉及网络通信。
图 1: 简化的缓存命中/未命中流程

采用缓存是为了减少延迟并降低运营成本,几十年来对于实现众多类别的应用程序至关重要。缓存数据的例子包括 Web 应用程序的会话状态、数据库查询结果、网页渲染结果,以及来自通用网络和计算成本高昂的操作的结果。
缓存的一个更现代的用途是在 AI 领域。在这里,缓存的使用减少了昂贵的 API 调用(例如,嵌入生成),并最大限度地减少了智能体架构中智能体之间的对话断续(例如,由于工具调用和网络通信所致),从而解锁了新一波的解决方案和用户体验。
缓存可以驻留在进程内,作为客户端-服务器架构的一部分存在于服务中,或者是两者的结合。此外,缓存的部署通常可以组合。例如,应用程序可能与位于同一数据中心的缓存服务通信,而数据中心的本地缓存又是跨越多个数据中心的缓存的缓存。这种灵活性,加上缓存所支持的应用类别,使得缓存在过去几十年中成为一种主导的抽象概念。
本参考资料卡的剩余部分将讨论 JCache------Java 用于将缓存融入应用程序的抽象------首先简要概述您将经常使用的类,然后深入探讨 JCache 更广泛功能所提供的特性。最后,我们将总结缓存部署策略。
JCACHE 精要
JCache 在 Java 规范请求(JSR)107 中引入,并提供了一套关于缓存的抽象。JCache 有两个突出的特性:
-
JCache 是一个规范。 JSR 是由专家组设计和提交,并最终由 Java 社区过程执行委员会批准的规范。因为 JCache 是一个规范,所以它与那些 API 频繁变化的实现隔离开来。
-
JCache 是提供商独立的。 JCache 作为规范的一个副作用是,缓存解决方案提供商可以通过实现其暴露的服务提供程序接口(SPI)来与 JCache 集成。这为系统设计者提供了灵活性并避免了供应商锁定。
以下是一个简单的 JCache 示例,以便理解其使用方式。javax.cache 依赖项的获取方式可以在此处找到。
java
import java.util.Map;
import javax.cache.Cache;
import javax.cache.CacheManager;
import javax.cache.Caching;
import javax.cache.configuration.MutableConfiguration;
import javax.cache.spi.CachingProvider;
public class App {
public static void main(String[] args) {
CachingProvider cachingProvider = Caching.getCachingProvider(); // (1)
CacheManager cacheManager = cachingProvider.getCacheManager(); // (2)
MutableConfiguration<String, String> cacheConfig = new MutableConfiguration<String, String>(); // (3)
Cache<String, String> cache = cacheManager.createCache("dzone-cache", cacheConfig); // (4)
cache.put("England", "London"); // (5)
cache.putAll(Map.of("France", "Paris", "Ireland", "Dublin")); // (6)
assert cache.get("England").equals("London"); // (7)
assert cache.get("Italy") == null; // (8)
}
}
对上述示例的简要说明:
(1) 获取底层缓存提供程序的句柄
(2) 管理缓存的生命周期(例如,创建和销毁缓存)
(3) 允许启用/禁用缓存的特定功能(例如,统计信息、条目监听器)
(4) 创建由缓存提供程序支持的缓存
(5) 在缓存中放入单个键值条目
(6) 将键值条目放入缓存
(7) 断言缓存条目的存在
(8) 断言某个条目不在缓存中
本节的剩余部分将更详细地讨论上述示例中引入的抽象,以及您将经常遇到的相关类的其他方法。
javax.cache.spi.CachingProvider 构成了 JCache SPI,缓存提供者可以与之集成。您将使用的最常见功能是获取对 CacheManager 的引用。我们稍后将讨论 Caching。
getCacheManager 是 getCacheManager 变体中最简单的一个。这将根据提供者的默认设置获取一个 CacheManager。可以使用 javax.cache.CacheManager 创建和销毁缓存:
-
createCache创建一个具有给定名称和配置的缓存。 -
destroyCache销毁具有给定名称的缓存。
javax.cache.Cache 是对提供者缓存的抽象,并暴露了少量用于查询和变更缓存项的操作:
-
put和putAll将条目放入缓存。请注意,这些方法不返回与正在放入的键先前关联的任何值。 -
containsKey测试键是否存在于缓存中。 -
get和getAll返回与指定键关联的值。 -
remove和removeAll从缓存中移除项。
JCACHE 包
在本节中,我们将快速概述 javax.cache 更广泛包结构中的一些重要接口,并提供常用功能的示例。我们可以参考文档来浏览其内容的详尽列表。
图 2: javax.cache 的组成包

JAVAX.CACHE
通用管理(CacheManager)和与缓存交互(Cache)的设施位于 javax.cache 包内。除了初始配置之外,除非您想为缓存添加额外功能,否则仅使用此包中的类型就可以完成很多工作。例如,"JCache 精要"部分介绍中的示例用法主要使用了 javax.cache 中定义的接口。
JAVAX.CACHE.CONFIGURATION
在创建缓存期间,您可能希望添加功能,例如启用统计信息或通读缓存。此包提供了一个 Configuration 接口和一个实现 MutableConfiguration,可用于此类目的。
java
// ...
MutableConfiguration<String, String> cacheConfig = new MutableConfiguration<String, String>();
cacheConfig.setManagementEnabled(true).setStatisticsEnabled(true);
Cache<String, String> cache = cacheManager.createCache("dzone-cache", cacheConfig);
JAVAX.CACHE.EXPIRY
有时您希望驻留在缓存中的项过期。例如,我们可能有一个家庭保险报价的缓存,有效期为 24 小时。在这种情况下,我们可以使用过期策略如下:
java
// ...
MutableConfiguration<String, Double> cacheConfig = new MutableConfiguration<String, Double>();
cacheConfig.setExpiryPolicyFactory(CreatedExpiryPolicy.factoryOf(Duration.ONE_DAY));
Cache<String, Double> cache = cacheManager.createCache("insurance-home-quotes", cacheConfig);
cache.put(quote.getId(), quote.getValue());
javax.cache.expiry 包提供了额外的过期策略,可能对其他场景有用。例如,AccessedExpiryPolicy 允许基于缓存条目的最后访问时间附加过期设置。
JAVAX.CACHE.EVENT
JCache 的一个强大功能是能够订阅缓存事件。例如,我们可能希望在创建或删除缓存条目后运行某些领域逻辑。javax.cache.event 包提供了实现此功能的抽象,特别是订阅缓存创建、更新、过期和移除的能力。以下基本示例在缓存条目创建后运行某些领域逻辑:
java
// ...
CacheEntryCreatedListener<String, String> createdListener = new CacheEntryCreatedListener<String, String>() {
@Override
public void onCreated(Iterable<CacheEntryEvent<? extends String, ? extends String>> events) throws CacheEntryListenerException {
for (var c : events) {
performDomainLogic(c);
}
}
};
MutableConfiguration<String, String> cacheConfig = new MutableConfiguration<String, String>();
MutableCacheEntryListenerConfiguration<String, String> listenerConfig = new MutableCacheEntryListenerConfiguration<>(() -> createdListener, null, false, true); // 请参阅文档
cacheConfig.addCacheEntryListenerConfiguration(listenerConfig);
Cache<String, String> cache = cacheManager.createCache("events", cacheConfig);
cache.put("key", "value"); // 调用创建监听器
JAVAX.CACHE.PROCESSOR
JCache 的一个强大组件是能够使用 EntryProcessor 将计算移至数据所在处,然后以编程方式调用该计算。当使用在分布式系统(例如,Hazelcast)内托管其缓存的提供者时,这尤其强大,因为它以很少的投入为分布式计算提供了一个简单的入口点。以下是一个 EntryProcessor 的简单示例,它将 UUID 附加到缓存条目:
java
// ...
class AppendUuidEntryProcessor implements EntryProcessor<String, String, String> {
@Override
public String process(MutableEntry<String, String> entry, Object... arguments) throws EntryProcessorException {
if (entry.exists()) {
String newValue = entry.getValue() + "-" + UUID.randomUUID();
entry.setValue(newValue);
return newValue;
}
return null;
}
}
// ...
cache.invoke(key, new AppendUuidEntryProcessor())
JAVAX.CACHE.MANAGEMENT
JCache 提供的管理钩子非常强大且易于启用。例如,下面的小代码片段暴露了由 Java 管理扩展(JMX)规范定义的托管 Bean。这使得诸如 jconsole 和 JDK Mission Control 之类的 JMX 客户端能够查看缓存配置和统计信息(例如,命中和未命中百分比、平均获取和放置时间)。
java
// ...
MutableConfiguration<String, String> cacheConfig = new MutableConfiguration<String, String>();
cacheConfig.setManagementEnabled(true).setStatisticsEnabled(true);
Cache<String, String> cache = cacheManager.createCache("management", cacheConfig);
// ...
JAVAX.CACHE.SPI
"JCache 精要"部分提供的示例省略了我们如何注册缓存提供者,即使用 JCache API 与我们的应用程序交互的缓存宿主服务。这就是 JCache 的 SPI 组件发挥作用的地方。
实现这一点有两个组成部分:
-
将我们的缓存提供者添加到类路径中
-
告诉 JCache 使用该提供者
第一步很简单:只需添加对任何符合 JSR 107 标准的提供者的依赖。
第二步有几种通用的方法:
-
我们可以通过调用
Caching#getCachingProvider(...)的某个变体(以及其他方法)来告诉 JCache。 -
我们可以提供一个
META-INF/services/javax.cache.spi.CachingProvider文件,并让其指定提供者实现。指定的提供者是您的提供者的缓存提供者实现的完全限定名称。 -
我们可以使用
Caching#getCachingProvider();但是,最好明确限定要使用的提供者,因为您的类路径上可能有多个提供者,这会抛出javax.cache.CacheException。
例如,以下代码使用 CachingProvider Caching.getCachingProvider(String) 指定 Hazelcast 为提供者:
java
CachingProvider cachingProvider = Caching.getCachingProvider("com.hazelcast.cache.HazelcastCachingProvider");
CacheManager cacheManager = cachingProvider.getCacheManager();
MutableConfiguration<String, String> cacheConfig = new MutableConfiguration<String, String>();
Cache<String, String> cache = cacheManager.createCache("spi-example", cacheConfig);
cache.put("k", "v");
JAVAX.CACHE.ANNOTATION
JCache 定义了许多注解,用于集成到上下文和依赖注入环境中。Spring Framework 原生支持 JCache 注解。我们可以参考 JCache 文档以获取更多信息。
JAVAX.CACHE.INTEGRATION
javax.cache.integration 包提供了 CacheLoader(需要通读)和 CacheWriter(需要通写)。CacheLoader 在将数据读入缓存时使用------例如 Cache#loadAll(...)。CacheWriter 可以作为一个集成点,将缓存变更(例如,写入、删除)传播到外部存储服务。
缓存部署
JCache 没有缓存部署策略的概念;它仅仅是缓存提供者之上的一个 API。然而,不同的提供者支持不同类型的缓存部署。请考虑哪种缓存部署对您的应用程序有意义,并由此反向确定合适的缓存提供者。
图 3: 缓存部署示例

请注意,一些缓存提供者可能支持所有这三种缓存部署,而其他提供者可能不支持。
本节的剩余部分讨论图 2 中所示的常见缓存部署:
-
嵌入式 -- 缓存与应用程序位于同一进程中。
-
客户端-服务器 -- 缓存托管在独立的服务中,客户端与该服务通信以确定缓存驻留。
-
嵌入式/客户端-服务器 -- 这是一种混合模式,整个缓存托管在不同的服务上,但客户端在同一进程中拥有一个较小的本地缓存。
重要的是要注意,上述缓存部署并非互斥的;它们可以通过多种方式组合以满足应用程序需求。
最简单的缓存部署是让缓存与应用程序驻留在同一进程中,这样做的好处是提供低延迟的缓存访问。嵌入式缓存不能在应用程序之间共享,并且在应用程序重启或故障时,其托管(它们所需的资源)和重建成本可能很高。
客户端-服务器缓存部署将缓存托管在与客户端不同的服务中。缓存服务允许通过跨服务复制来满足容错需求,提供更大的容量、更多的可扩展性选项,以及跨应用程序共享缓存的能力。客户端-服务器模型的主要缺点在于客户端缓存查询期间网络通信的成本。
混合嵌入式/客户端-服务器部署是指我们拥有一个嵌入式缓存,它包含来自服务缓存条目的一个子集,作为应用程序缓存请求的副作用被填充。在这里,客户端可以对频繁访问的数据(或表现出特定访问模式的数据)实现低延迟的缓存命中,并省去与缓存服务通信所带来的网络通信开销。如果嵌入式缓存过期,一些提供者会负责使用服务托管的缓存来更新它们。
结论
本参考资料卡介绍了缓存以及如何将其与 Java 的 JCache API 一起使用。JCache API 直观、强大,并且由于其是一个规范而避免了供应商锁定,为架构师和系统设计者提供了他们所需的灵活性。这种灵活性在我们进入基于智能体架构的新一代创新时尤为重要,其中缓存对于工具链和嵌入生成至关重要。
作者:GRANVILLE BARNETT,
架构师,HAZELCAST
Granville Barnett 拥有计算机科学博士学位,是拥有超过 15 年经验的分布式系统专家。他目前是 Hazelcast 的架构师,此前曾在 HP Labs 和 Microsoft 任职。Granville 拥有多项美国专利,并发表了关于程序验证主题的研究。
附加资源:
-
《Java 应用程序容器化与部署》,作者 Mark Heckler,DZone 参考资料卡
【注】本文译自:Java Caching Essentials