为了让Java中的缓存接口更加统一化、规范化,Java Community Process(JCP)组织提出并通过了JSR-107提案。提案中从缓存的基础概念到缓存的操作、拓扑等细节进行了非常详细的解释,并且提供了一套开发者可用的API。本文是我阅读完提案后对其进行的总结。如果需要完整的提案PDF可以在JCP官网下载:https://jcp.org/aboutJava/communityprocess/final/jsr107/index.html
什么是缓存?
在应用程序设计领域中,它通常代表开发者使用内存或低延迟的数据结构来临时存储缓存数据的副本或引用,便于复用来减少重新访问或重新创建的成本。在 Java缓存API领域中,术语“缓存”代表的是Java开发者使用缓存组件临时缓存Java对象的技术。通常缓存的是数据库的数据,或者任何创建或访问代价较大或者耗时的数据都可以进行缓存,比如:
- Web服务调用的客户端缓存
- 昂贵的计算,例如渲染的图像
- 数据缓存
- Servlet响应缓存
- 领域对象图(caching of domain object graphs)
JSR-107提案
JSR是Java Specification Requests的缩写,意思是 Java 规范提案。2012年10月26日 JSR 规范委员会发布了 JSR 107(JCache API的首个早期草案)。JCache 规范定义了一种对Java对象临时在内存中进行缓存的方法,包括对象的创建、共享访问、假脱机(spooling)、失效、各JVM的一致性等,可被用于缓存JSP内最经常读取的数据。
JSR-107提案有许多种实现,例如:
- JCache API
- Spring Cache
- EhCache
- Caffeine
······
JSR-107 基础概念
JSR-107 目标
- 为应用程序提供缓存功能,尤其是缓存Java对象的能力
- 定义一套通用的缓存概念和设施
- 减少Java开发人员采用缓存的学习成本
- 最大化应用程序切换缓存实现的能力
- 支持进程内和分布式缓存实现
- 支持按值和按引用缓存Java对象
在Maven中引入JCache
<dependency>
<groupId>javax.cache</groupId>
<artifactId>cache-api</artifactId>
<version>1.1.0</version>
</dependency>
值存储和引用存储
按值存储(默认的存储方式):存储实际的数据,对象被序列化并完整地存储在缓存中。当从缓存中检索数据时,会返回原始对象的完整副本。
优点:适用于大多数情况,提供了对完整数据的直接访问,没有引用的间接性。这对于简化缓存操作和提高性能非常有用。
缺点:可能会导致内存使用增加,尤其是在缓存大量数据时。此外,它可能需要更多的时间来序列化和反序列化数据。
引用存储
按引用存储的是对象的引用(可选):缓存存储了指向实际数据的引用,而不是数据本身。当从缓存中检索数据时,会返回原始对象的引用,然后通过引用来访问数据。
优点:无需存储数据副本,能够节省内存开销,适用于大数据或超大对象的情况
缺点:会引入更多复杂性,增加垃圾回收的管理成本
Cache和Map的异同
相同点
- 通过 key 进行存储和访问
- 每个 key 仅与一个值进行关联
不同点
- 缓存键和值不能为null
- 缓存条目可能会过期
- 缓存条目可能会被策略淘汰
- 缓存的具体实现可能要求键和值以某种方式可序列化
- 缓存支持按值存储或者按引用存储
一致性问题
默认一致性模型
参考官方给出的一致性模型表,适用于Cache中的方法:
| 方法 | 默认一致性 |
|---|---|
boolean containsKey(K key) | 最新值 |
V get(K key) | happen-before |
Map<K,V> getAll(Collection<? extends K> keys) | 对里面的每个key,而不是整个集合 |
V getAndPut(K key, V value) | happen-before |
V getAndRemove(K key) | happen-before |
V getAndReplace(K key, V value) | happen-before + CAS |
CacheManager getCacheManager() | N/A |
CacheConfiguration getConfiguration() | N/A |
String getName() | N/A |
Iterator<Cache.Entry<K, V>> iterator() | 最新值 |
void loadAll(Set<? extends K> keys, boolean replaceExistingValues, CompletionListener listener) | N/A |
void put(K key, V value) | happen-before |
void putAll(Map<? extends K,? extends V> map) | 对里面的每个key,而不是整个集合 |
boolean putIfAbsent(K key, V value) | happen-before + CAS |
boolean remove(K key) | happen-before |
boolean remove(K key, V oldValue) | happen-before + CAS |
void removeAll() | 最新值 |
void removeAll(Set<? extends K> keys) | happen-before + CAS |
<T> T invoke(K key, EntryProcessor<K, V, T> entryProcessor, Object... arguments)entryProcessor); | happen-before |
<T> Map<K, EntryProcessorResult<T>> invokeAll(Set<? extends K> keys,EntryProcessor<K, V, T> entryProcessor, Object... arguments); | 对里面的每个key,而不是整个集合 |
boolean replace(K key, V value) | happen-before + CAS |
boolean replace(K key, V oldValue, V newValue) | happen-before + CAS |
<T> T unwrap(Class<T> cls) | N/A |
高级一致性模型
这个需要开发者自行基于默认一致性模型进行拓展
缓存拓扑
本地缓存
本地缓存直接存储在本地内存中,不支持跨进程跨应用共享。
本地缓存中,创建多个Provider和Manager缓存就会被隔离开来,所以如果要保持多个进程中的缓存一致,需要考虑分布式缓存。
分布式缓存
分布式缓存能够很方便地在多个进程或者应用程序之间共享数据,常见的分布式缓存数据库如 Redis。
开发者可以通过拓展 CachingProvider 接口的 getCacheManager() 中的特定 URI 和 ClassLoader 来实现分布式缓存。原理是让多个 CachingProvider 获取到“同一个” CacheManager 实例
JCache核心模块
核心接口
- Cache接口:Cache接口提供了一系列方法来操作Cache,它与Map十分相似。具体异同点上方已给出
- CacheProvider接口:缓存提供器,提供用于获取CacheManager实例、URI和默认ClassLoader实例的方法
- CacheManager接口:缓存管理器,可供管理缓存(Cache),提供一系列对缓存的操作方法,比如:创建缓存、销毁缓存、获取缓存、关闭缓存等。
- ExpiryPolicy接口:缓存的过期策略接口,具体缓存过期策略实现此接口
- AccessedExpiryPolicy:访问时更新过期时间
- CreatedExpiryPolicy:创建时更新过期时间
- EternalExpiryPolicy:永不过期
- ModifiedExpiryPolicy:修改时更新过期时间
- TouchedExpiryPolicy:创建、更新、访问时更新过期时间
Caching类
Caching类可以理解为一个缓存的工具类,提供了非常方便的获取CachingProvider的方法。具体如下:
- 获取默认的CachingProvider
- 根据ClassLoader获取CachingProvider
- 根据全类名创建/获取开发者实现的实例
事件
监听器
-
CacheEntryListener:缓存条目监听器,用于监听缓存中条目变化的接口,是其他事件监听接口的基类
public interface CacheEntryListener<K, V> extends EventListener { }CacheEntryEvent实现如下:
package javax.cache.event; import javax.cache.Cache; import javax.cache.configuration.CacheEntryListenerConfiguration; import java.util.EventObject; public abstract class CacheEntryEvent<K, V> extends EventObject implements Cache.Entry<K, V> { private EventType eventType; public CacheEntryEvent(Cache source, EventType eventType) { super(source); this.eventType = eventType; } @Override public final Cache getSource() { return (Cache) super.getSource(); } @Override public abstract V getValue(); public abstract V getOldValue(); public abstract boolean isOldValueAvailable(); public final EventType getEventType() { return eventType; } } -
CacheEntryCreatedListener:缓存条目创建监听器,顾名思义,在缓存创建时该监听器触发。
-
CacheEntryUpdatedListener:缓存条目更新监听器。
-
CacheEntryRemovedListener:缓存条目删除监听器。
-
CacheEntryExpiredListener:缓存条目过期监听器。
事件类型(EventType枚举类)
- CREATED:缓存创建
- UPDATED:缓存更新
- REMOVED:缓存删除
- EXPIRED:缓存过期
配置
Configuration接口
Configuration接口是用于在CacheManager中获取Cache的最基本配置,提供了例如getKeyType()、getValueType()等方法。如果要严格按照JSR-107规范的话,需要实现CompleteConfiguration接口。
Configuration.java
package javax.cache.configuration;
import javax.cache.Cache;
import javax.cache.CacheManager;
import java.io.Serializable;
/**
* A basic read-only representation of a {@link Cache} configuration.
* <p>
* The properties provided by instances of this interface are used by
* {@link CacheManager}s to configure {@link Cache}s.
* <p>
* Implementations of this interface must override {@link Object#hashCode()} and
* {@link Object#equals(Object)} as {@link Configuration}s are often compared at
* runtime.
*
* @param <K> the type of keys maintained the cache
* @param <V> the type of cached values
*/
public interface Configuration<K, V> extends Serializable {
Class<K> getKeyType();
Class<V> getValueType();
boolean isStoreByValue();
}
CompleteConfiguration接口
CompleteConfiguration接口是JSR-107规范定义的完整的配置接口。
package javax.cache.configuration;
import javax.cache.expiry.ExpiryPolicy;
import javax.cache.integration.CacheLoader;
import javax.cache.integration.CacheWriter;
import java.io.Serializable;
/**
* A read-only representation of the complete JCache {@link javax.cache.Cache}
* configuration.
* <p>
* The properties provided by instances of this interface are used by
* {@link javax.cache.CacheManager}s to configure {@link javax.cache.Cache}s.
* <p>
* Implementations of this interface must override {@link Object#hashCode()} and
* {@link Object#equals(Object)} as
* {@link javax.cache.configuration.CompleteConfiguration}s are often compared at
* runtime.
*
* @param <K> the type of keys maintained the cache
* @param <V> the type of cached values
*/
public interface CompleteConfiguration<K, V> extends Configuration<K, V>,
Serializable {
boolean isReadThrough();
boolean isWriteThrough();
boolean isStatisticsEnabled();
boolean isManagementEnabled();
Iterable<CacheEntryListenerConfiguration<K,
V>> getCacheEntryListenerConfigurations();
Factory<CacheLoader<K, V>> getCacheLoaderFactory();
Factory<CacheWriter<? super K, ? super V>> getCacheWriterFactory();
Factory<ExpiryPolicy> getExpiryPolicyFactory();
}
MutableConfiguration
MutableConfiguration类已经实现了CompleteConfiguration的配置文件,开发者可以直接使用。
下面是一个MutableConfiguration的使用例子:
CacheManager cacheManager = Caching.getCachingProvider().getCacheManager();
MutableConfiguration<String, Integer> config = new MutableConfiguration<>();
config.setStoreByValue(true); // 指定缓存值存储方式为拷贝存储
config.setTypes(String.class, Integer.class); // 指定键和值的类型
config.setExpiryPolicyFactory(AccessedExpiryPolicy.factoryOf(Duration.ONE_HOUR)); // 设置缓存过期策略
Cache<String, Integer> cache = cacheManager.createCache("myCache", config);
JMX监控
Java Management Extensions(JMX)技术是 Java SE 平台的标准功能,提供了一种简单的、标准的监控和管理资源的方式,对于如何定义一个资源给出了明确的结构和设计模式,主要用于监控和管理 Java 应用程序运行状态、设备和资源信息、Java 虚拟机运行情况等信息。JMX 是可以动态的,所以也可以在资源创建、安装、实现时进行动态监控和管理,JDK 自带的 jconsole 就是使用 JMX 技术实现的监控工具。
在 JMX 中, 使用 MBean 或 MXBean 来表示一个资源(下面简称 MBean),访问和管理资源也都是通过 MBean,所以 MBean 往往包含着资源的属性和操作方法。
JCache实现中提供了如下的MXBean接口:
- CacheMXBean接口:缓存的配置信息接口,每个缓存必须有唯一的对象名,并且拥有类型和属性对
- CacheStatisticsMXBean接口:分析缓存信息的接口,例如获取缓存命中次数、命中率等信息
Caching Annotations 缓存注解
JCache缓存注解提供了一系列方便Java开发者使用的注解,常用注解如下:
@CacheResult:作用在方法上,表示该方法需要被缓存。参数:cacheName="xxx"@CachePut:作用在方法上,通常用于更新缓存数据的操作@CacheRemove:作用在方法上,表示方法中与参数匹配的值在方法执行后将被从缓存中删除@CacheRemoveAll:作用在方法上,方法执行后,与指定cacheName相关的所有缓存都会被删除@CacheDefaults:作用在类上,用于为整个类指定缓存的默认配置@CacheKey:作用在参数上,标记的键会被缓存@CacheValue:作用在参数上,标记的参数的返回值会被缓存
总结
JSR-107提案很好地总结了Java领域中的缓存技术,但随着技术栈的迭代,其中的一些部分已经与现在复杂的分布式系统脱节。并且有些约束太过严格,反而有可能影响系统性能且不利于系统的拓展。所以JSR-107规范固然重要,但也不必刻板地完全遵守。现在主流的缓存框架例如Caffeine的设计也不完全遵循JSR-107,但它也提供了对JSR-107(JCache API)的支持。