写点什么

Java 缓存精要

作者:码界行者
  • 2025-11-18
    上海
  • 本文字数:5810 字

    阅读完需:约 19 分钟

Java 缓存精要

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 依赖项的获取方式可以在此处找到。


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


getCacheManagergetCacheManager 变体中最简单的一个。这将根据提供者的默认设置获取一个 CacheManager。可以使用 javax.cache.CacheManager 创建和销毁缓存:


  • createCache 创建一个具有给定名称和配置的缓存。

  • destroyCache 销毁具有给定名称的缓存。


javax.cache.Cache 是对提供者缓存的抽象,并暴露了少量用于查询和变更缓存项的操作:


  • putputAll 将条目放入缓存。请注意,这些方法不返回与正在放入的键先前关联的任何值。

  • containsKey 测试键是否存在于缓存中。

  • getgetAll 返回与指定键关联的值。

  • removeremoveAll 从缓存中移除项。

JCACHE 包

在本节中,我们将快速概述 javax.cache 更广泛包结构中的一些重要接口,并提供常用功能的示例。我们可以参考文档来浏览其内容的详尽列表。


图 2:javax.cache 的组成包


JAVAX.CACHE

通用管理(CacheManager)和与缓存交互(Cache)的设施位于 javax.cache 包内。除了初始配置之外,除非您想为缓存添加额外功能,否则仅使用此包中的类型就可以完成很多工作。例如,"JCache 精要"部分介绍中的示例用法主要使用了 javax.cache 中定义的接口。

JAVAX.CACHE.CONFIGURATION

在创建缓存期间,您可能希望添加功能,例如启用统计信息或通读缓存。此包提供了一个 Configuration 接口和一个实现 MutableConfiguration,可用于此类目的。


// ...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 小时。在这种情况下,我们可以使用过期策略如下:


// ...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 包提供了实现此功能的抽象,特别是订阅缓存创建、更新、过期和移除的能力。以下基本示例在缓存条目创建后运行某些领域逻辑:


// ...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 附加到缓存条目:


// ...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 客户端能够查看缓存配置和统计信息(例如,命中和未命中百分比、平均获取和放置时间)。

// ...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 组件发挥作用的地方。


实现这一点有两个组成部分:


  1. 将我们的缓存提供者添加到类路径中

  2. 告诉 JCache 使用该提供者


第一步很简单:只需添加对任何符合 JSR 107 标准的提供者的依赖。


第二步有几种通用的方法:


  • 我们可以通过调用 Caching#getCachingProvider(...) 的某个变体(以及其他方法)来告诉 JCache。

  • 我们可以提供一个 META-INF/services/javax.cache.spi.CachingProvider 文件,并让其指定提供者实现。指定的提供者是您的提供者的缓存提供者实现的完全限定名称。

  • 我们可以使用 Caching#getCachingProvider();但是,最好明确限定要使用的提供者,因为您的类路径上可能有多个提供者,这会抛出 javax.cache.CacheException


例如,以下代码使用 CachingProvider Caching.getCachingProvider(String) 指定 Hazelcast 为提供者:

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,架构师,HAZELCASTGranville Barnett 拥有计算机科学博士学位,是拥有超过 15 年经验的分布式系统专家。他目前是 Hazelcast 的架构师,此前曾在 HP Labs 和 Microsoft 任职。Granville 拥有多项美国专利,并发表了关于程序验证主题的研究。

附加资源:




【注】本文译自:Java Caching Essentials

发布于: 40 分钟前阅读数: 7
用户头像

码界行者

关注

分享程序人生。 2019-07-04 加入

“码”界老兵,分享程序人生。

评论

发布
暂无评论
Java 缓存精要_Java缓存_码界行者_InfoQ写作社区