JAVA缓存- JSR107 最终规范

Posted 顧棟

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了JAVA缓存- JSR107 最终规范相关的知识,希望对你有一定的参考价值。

文章目录

JSR107 java 缓存规范

原文地址:https://download.oracle.com/otndocs/jcp/jcache-1_0-fr-eval-spec/index.html

PDF下载:JSP107 最终规范

更详细的中文翻译:https://www.codercto.com/a/103936.html

本规范描述了 Java 缓存应用程序编程接口(“API”)的目标和功能。

Java Caching API 为 Java 程序从缓存中创建、访问、更新和删除条目提供了一种通用方法。

什么是缓存

缓存一词在计算中无处不在。 在应用程序设计的上下文中,它通常用于描述应用程序开发人员利用单独的内存或低延迟数据结构,缓存是临时存储或引用信息的副本,应用程序可能会在稍后的某个时间点重用,从而减轻重新访问或重新创建它的成本。

在 Java Caching API 的上下文中, Caching 描述了 Java 开发人员使用 Caching Provider 临时缓存 Java 对象的技术。 通常假设正在缓存来自数据库的信息。 然而,这不是缓存的必要条件。 从根本上说,任何生产或访问昂贵或耗时的信息都可以存储在缓存中。 一些常见的用例是:

  • client side caching of Web service calls Web 服务调用的客户端缓存
  • caching of expensive computations such as rendered images 缓存昂贵的计算,例如渲染图像
  • caching of data 数据缓存
  • servlet response caching servlet 响应缓存
  • caching of domain object graphs 域对象图缓存

目标

  • 为应用程序提供缓存功能,特别是缓存 Java 对象的能力;
  • 定义一套通用的缓存概念和设施;
  • 最大限度地减少 Java 开发人员需要学习采用缓存的概念数量;
  • 最大化在缓存实现之间使用缓存的应用程序的可移植性;
  • 支持进程内和分布式缓存实现;
  • 支持按值和可选的按引用缓存 Java 对象;
  • 根据 JSR-175 定义运行时缓存注释:Java 的元数据工具编程语言; 以便 Java 开发人员使用可选的注解处理器,来声明式地指定应用程序缓存要求;

Java 缓存 API 未解决

  • 资源和内存约束配置 - 虽然许多缓存实现支持限制缓存在运行时可能使用的资源量,但本规范并未定义如何配置或表示此功能。 然而,该规范确实为开发人员定义了一种标准机制,以指定信息应该可以缓存多长时间。

  • 缓存存储和拓扑——本规范没有指定缓存实现如何存储或表示被缓存的信息。

  • 管理 - 本规范未指定如何管理缓存。 它确实定义了通过 Java 管理扩展 (JMX) 以编程方式配置缓存和调查缓存统计信息的机制。

  • 安全性——本规范未指定如何保护缓存内容或如何控制对缓存的访问和操作。

  • 外部资源同步 - 本规范没有指定应用程序或缓存实现应如何保持缓存和外部资源内容同步。

虽然开发人员可以利用规范提供的通读和通写技术,但这些技术仅在缓存是唯一改变外部资源的应用程序时才有效。 在此场景之外,无法保证缓存同步。

提供的java chacing api的包是javax.cache.,maven获取方式

        <dependency>
            <groupId>javax.cache</groupId>
            <artifactId>cache-api</artifactId>
            <version>1.1.0</version>
        </dependency>

基础知识

核心概念

Java Caching API 定义了五个核心接口:CachingProviderCacheManagerCacheEntryExpiryPolicy

  • CachingProvider 定义了建立、配置、获取、管理和控制零个或多个 CacheManager 的机制。 应用程序可以在运行时访问和使用零个或多个 CachingProvider

  • CacheManager 定义了建立、配置、获取、管理和控制零或多个唯一命名的Cache都在CacheManager的上下文中的机制。一个CacheManager由单个 CachingProvider拥有。

  • Cache 是一种类似于 Map 的数据结构,它允许临时存储基于键的值,有些类似于java.util.Map数据结构。一个 Cache 由一个CacheManager拥有。

  • Entry是由缓存存储的单个键值对。

  • Cache存储的每个Entry都有一个定义的持续时间,称为到期持续时间(有效时长),在此期间它们可以被访问、更新和删除。一旦该持续时间过去,则该条目被称为已过期。一旦过期,条目就不再可供访问、更新或删除,就好像它们从未存在于缓存中一样。使用ExpiryPolicy设置到期。

    manage 0..* 1 0..* manage 0..* «interface» CachingProvider «interface» CacheManager «interface» Cache «interface» Entry «interface» ExpiryPolicy

按值存储和按引用存储

Cache 存储 Entry 的方式有两种。默认机制称为按值存储,在javax.cache.configuration.MutableConfiguration中由isStoreByValue控制,默认值true

它指示实现在将应用程序提供的键值对在缓存之前先制作它们的副本(复制),然后在从缓存中访问时返回条目的新副本。复制存储在缓存中的条目以及从缓存返回时再次复制条目的目的是允许应用程序继续改变键和值的状态,而不会对缓存所保存的条目产生其他副作用。

Java 序列化可以用来制作键和值的副本的简单方法实现。

为确保实现的应用程序可移植性,建议自定义键和值类在使用按值存储时实现并采用标准 Java 序列化。

实现用于制作条目的键和值的副本的机制可以是自定义的。然而,为了确保应用程序的可移植性,实现必须允许应用程序单独使用标准 Java 序列化。实现不得强制应用程序采用非标准 Java 序列化。

另一种可选的机制,称为按引用存储,指示Cache实现简单地存储和返回对应用程序提供的键和值的引用,而不是按值存储方法需要的副本。如果应用程序以后使用按引用存储的语义修改了提供给Cache的键或值,那么访问Cache条目的人就可以看到这些修改的副作用,而不需要应用程序更新Cache。

对于在 Java 堆上实现的缓存,按引用存储是更快的存储技术。

Map与Cache的异同点

相同点:

  • 缓存的value值通过关联的key进行存储和访问
  • 缓存中的每个key只能与单个value相关联。
  • 如果将可变的对象用作key,则必须非常小心。 如果key在与缓存一起使用时,key以影响相等比较的方式发生变化,则缓存的行为是不确定的。
  • 缓存依赖于相等的概念来确定键和值何时相同。 因此,自定义键和值类应该定义 Object.equals方法的合适实现。
  • 自定义键类应另外提供Object.hashCode方法的合适实现。

不同点:

  • 缓存的keys和values不能为null。

    任何对键或值使用 null 的尝试都将导致抛出 NullPointerException,无论使用如何。

  • 条目可能会过期。

    确保条目不再可用于应用程序,因为它们不再被视为有效,这个过程称为“到期”

  • 条目可能会被驱逐。

    • 缓存通常不配置为存储整个数据集。 相反,它们通常用于存储整个数据集的一个小的、经常使用的子集。

    • 为了确保缓存的大小不会无限制地消耗资源,缓存的实现可以定义一个策略来限制缓存在运行时可以使用的资源量,方法是在超出资源限制时删除某些条目。

    • 当缓存超过资源限制时从缓存中删除条目的过程称为“驱逐”。 当一个条目由于资源限制而从缓存中删除时,它被称为“被驱逐”。

    • 虽然规范没有定义缓存的容量,但一个明智的实现将定义表示所需容量限制的机制,以及一旦达到该容量就选择和驱逐条目的合适策略。 例如:LRU 驱逐策略试图驱逐最近最少使用的条目。

    • 规范中未定义容量的一些原因是:

      • 实现可以利用多层分层存储结构,从而定义每层的容量。 在这种情况下,不可能定义缓存的整体容量,这样做会很模糊。

      • 实现可以根据字节而不是每层的条目数来定义容量。

      • 条目在内存使用方面的相对成本与运行时条目实现的内部表示直接相关。

  • 为了支持比较和交换 (CAS) 操作,即那些以原子方式比较和交换值的操作,自定义值类应该提供Object.equals的合适实现。

    尽管推荐,但实现不一定需要调用自定义键类定义的Object.hashCodeObject.equals方法。 实现可以自由使用优化,从而避免调用这些方法。

    由于本规范没有定义对象等价的概念,因此应注意使用自定义键类并依赖实现特定优化来确定等价的应用程序可能不可移植。

  • 实现可能要求键和值以某种方式可序列化。

  • 缓存可以配置为控制条目的存储方式,使用按值存储或可选地使用按引用存储语义

  • 实现可以选择强制执行安全限制。 如果发生违规,必须抛出SecurityException

尽管推荐,但实现不一定需要调用自定义键类定义的Object.hashCodeObject.equals方法。 实现可以自由使用优化,从而避免调用这些方法。

由于本规范没有定义对象等价的概念,因此应注意使用自定义键类并依赖实现特定优化来确定等价的应用程序可能不可移植。

一致性

所有实现都必须支持如下所述的默认一致性模型。

默认一致性

当使用默认的一致性模式时,大多数缓存操作的执行就像缓存中的每个键都存在锁定机制一样。 当缓存操作在某个键上获得独占读写锁时,该键上的所有后续操作都将阻塞,直到该锁被释放。 结果是一个线程执行的操作发生在另一个线程执行的读取或变异操作之前,包括不同Java虚拟机中的线程。

可以理解为一个悲观锁,以加锁,修改,解锁来保证一致性。

对于某些缓存操作,缓存返回的值被认为是最后一个值。 最后一个值可能是旧值或新值,尤其是在同时更新条目的情况下。 返回哪个取决于实现。

这可以理解为一种没有锁的方法,没有保证一致性。其他操作遵循不同的约定,因为只有在条目的当前状态与所需状态匹配时才会发生突变。 在这样的操作中,多个线程可以自由竞争以应用这些更改,即好像它们共享一个锁一样。 这些是:

  boolean putIfAbsent(K key, V value);
  boolean remove(K key, V oldValue);
  boolean replace(K key, V oldValue, V newValue);
  boolean replace(K key, V value);
  V getAndReplace(K key, V value);

这可以理解为乐观锁的方式; 仅当状态与已知状态匹配时才应用更改,否则失败。 在同样以这种方式操作的 CPU 指令之后,这些类型的操作也称为比较和交换 (CAS) 操作。

由于这些方法必须与其他缓存操作进行交互,就好像它们具有排他锁一样,CAS 方法不能写入新值,除非它们也具有排他锁。

结果,在默认一致性中,虽然 CAS 方法可以允许更高级别的并发性,但它们将被非 CAS 方法阻止。

下表显示了适用于每种缓存方法的默认一致性。

MethodDefault Consistency
boolean containsKey(K key)last value
V get(K key)happen-before
Map<K,V> getAll(Collection<? extends K> keys)happen-before for each key individually but not for the Collection
V getAndPut(K key, V value)happen-before
V getAndRemove(K key)happen-before
V getAndReplace(K key, V value)happen-before plus compare and swap
CacheManager getCacheManager()N/A
CacheConfiguration getConfiguration()N/A
String getName()N/A
Iterator<Cache.Entry<K, V>> iterator()last value
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)happen-before for each key individually but not for the Collection.
boolean putIfAbsent(K key, V value)happen-before plus compare and swap
boolean remove(K key)happen-before
boolean remove(K key, V oldValue)happen-before plus compare and swap
void removeAll()last value
void removeAll(Set<? extends K> keys)happen-before for each key individually but not for the Collection.
T invoke(K key, EntryProcessor<K, V, T> entryProcessor, Object… arguments)entryProcessor);happen-before
Map<K, EntryProcessorResult> invokeAll(Set<? extends K> keys, EntryProcessor<K, V, T> entryProcessor, Object… arguments);happen-before for each key individually but not for the Collection.
boolean replace(K key, V value)happen-before plus compare and swap
boolean replace(K key, V oldValue, V newValue)happen-before plus compare and swap
T unwrap(Class cls)N/A

last value:最新值

happen-before:发生之前

happen-before for each key individually but not for the Collection:单独为每个键发生之前,但不适用于集合

happen-before plus compare and swap:发生前加比较和交换

N/A:不适用

更多一致性模型

除了所需的默认一致性模型之外,实现还可以为不同的一致性模型提供支持。

缓存拓扑

虽然规范没有强制要求特定的缓存拓扑结构,但可以认识到缓存条目可以很好地存储在本地和/或分布在多个进程中。 实现可以选择不支持、支持一种或者两种,或其他拓扑。

这个概念在规范中以多种方式表达:

  • 大多数修改方法提供具有void或低成本返回类型的签名。 例如,java.util.Map提供了方法V put(K key, V value)javax.cache.Cache提供了void put(K key, V value)。还提供了具有更昂贵返回类型的版本。 一个例子是CacheV getAndPut(K key, V value)方法。 它像 Map 一样返回旧值。

  • 通过具有不假定进程内实现的创建语义,Configuration是可序列化的,因此可以通过网络发送。 开发人员可以定义CacheEntryListenerExpiryPolicyCacheEntryFilterCacheWriterCacheLoader 的实现,并将它们与缓存相关联。 为了支持分布式拓扑,开发人员为他们的创建定义了一个Factory而不是实例。 Factory接口是可序列化的。

  • 在整个规范中使用Iterable来处理可能很大的返回类型和参数。 返回整个集合的方法(例如 Map 方法 keySet())可能会出现问题。

    缓存可能太大,以至于key集可能无法放入可用内存中,并且网络可能不好。CacheCacheEntryListener子接口上的监听器方法和 CacheLoader 上的批处理方法都使用Iterable

  • 没有提前规定 CacheEntryListenerExpiryPolicyCacheEntryFilterCacheWriterCacheLoader 的实现在哪里被实例化和执行。

    在分布式实现中,这些可能都位于数据附近,而不是与应用程序一起处理。

  • CachingProvider.getCacheManager(URI uri, ClassLoader classLoader)返回一个带有特定 ClassLoaderURICacheManager。 这使实现能够实例化多个实例。

执行上下文

EntryProcessors、CacheEntryListeners、CacheLoaders、CacheWriters 和 ExpiryPolicys(“customizations”)在配置它们的 CacheManager URI 和 ClassLoader 的上下文中进行实例化和操作。 这意味着在部署时,这些自定义的实例必须可供缓存的 ClassLoader 定义的应用程序类使用并访问。

实现可以安全地假设此类定制可用于使用 CacheManager 提供的 ClassLoader 了的 Cache。

如何实现类的可用性取决于实现和部署

例如:在 Java EE 环境中,应用程序定义的定制可以部署在企业应用程序 ear/war/jar 的范围内。

虽然定制可能在与应用程序相同的 ClassLoader 中可用,因此可以访问所有应用程序类,但为了确保可移植性,应用程序定制必须避免直接访问特定于部署的资源。 相反,定制应该只尝试访问和改变提供给他们的缓存信息和条目。

在支持它的实现和部署环境中,定制可以额外利用诸如资源注入(例如:CDI)之类的技术来允许直接访问应用程序和部署特定资源。 然而,没有要求实现支持这种能力。

可重入

虽然本规范不限制开发人员在使用自定义EntryProcessorsCacheEntryListenersCacheLoadersCacheWritersExpiryPolicys 时可能执行的操作,但缓存实现可能会限制来自这些接口的重入。 例如; 实现可能会限制EntryProcessor调用Cache上的方法或调用其他 EntryProcessor 的能力。 类似地,实现可能会限制 CacheLoader/CacheWriter访问 Cache的能力。 因此,强烈建议开发人员避免编写这些接口的可重入实现,因为这些实现可能不可移植。

简单示例

这个简单的示例创建了一个默认的 CacheManager,在其上配置了一个名为simpleCache的缓存,其键类型为 String,值类型为Integer,有效期为一小时,然后执行一些缓存操作。

    //解析出一个 a cache manager
    CachingProvider cachingProvider = Caching.getCachingProvider();
    CacheManager cacheManager = cachingProvider.getCacheManager();
    //配置缓存的存储类型,清理策略
    MutableConfiguration<String, Integer> config = new MutableConfiguration<>()
        .setTypes(String.class, Integer.class)
        .setExpiryPolicyFactory(AccessedExpiryPolicy.factoryOf(ONE_HOUR))
        .setStatisticsEnabled(true);
    //创建一个叫simpleCache的Cache
    Cache<String, Integer> cache = cacheManager.createCache("simpleCache", config);
    //对simpleCache的Cache进行塞值,剔除值,验证值
    String key = "key";
    Integer value1 = 1;
    cache.put("key", value1);
    Integer value2 = cache.get(key);
    assertEquals(value1, value2);
    cache.remove(key);
    assertNull(cache.get(key));

在使用默认的 CachingProvider 和默认的 CacheManager 的地方,有一个获取 Cache 的静态便捷方法,Caching.getCache

    //get the cache
    Cache<String, Integer> cache = Caching.getCache("simpleCache",
    String.class, Integer.class);

以上是关于JAVA缓存- JSR107 最终规范的主要内容,如果未能解决你的问题,请参考以下文章

28springboot——缓存之JSR107——基于注解的缓存使用②

面经之java缓存总结,从单机缓存到分布式缓存架构

SpringBootSpringBoot 缓存(十八)

SpringBoot与缓存

Java缓存

分布式缓存(SpringCache)