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

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

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,可用于此类目的。

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

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

  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 为提供者:

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 Caching Essentials

相关推荐
朝新_2 小时前
Spring事务和事务传播机制
数据库·后端·sql·spring·javaee
222you2 小时前
SpringBoot对SpringMVC的整合
java·spring boot·后端
刘一说2 小时前
深入理解 Spring Boot 高级特性:条件化 Bean 注册机制
java·spring boot·后端
用户69371750013842 小时前
Kotlin 函数详解:命名参数与默认参数值
android·后端·kotlin
启山智软2 小时前
使用 Spring Boot + Vue.js 组合开发多商户商城(B2B2C平台)是一种高效的全栈技术方案
vue.js·spring boot·后端
用户90555842148052 小时前
请求失败溯源Netty关闭连接源码流程
后端
踏浪无痕2 小时前
准备手写Simple Raft(一):想通Raft的核心问题
分布式·后端
00后程序员3 小时前
Charles抓包实战,开发者如何通过流量分析快速定位系统异常?
后端