术语“缓冲区”和“缓存”倾向于互换使用。然而,请注意,它们代表不同的事物。传统上,缓冲区用作快速实体和慢速实体之间数据的中间临时存储区。由于一方必须等待另一方(这会影响性能),缓冲区通过允许整个数据块一次移动而不是在小块中移动来缓解这种情况。数据只从缓冲区写入和读取一次。此外,至少有一方知道缓冲区是可见的。
另一方面,根据定义,缓存是隐藏的,任何一方都不知道发生了缓存。它还可以提高性能,但可以让相同的数据以快速的方式多次读取。
您可以在此处找到缓冲区和缓存之间差异的进一步解释。
缓存抽象的核心是将缓存应用于Java方法,从而根据缓存中可用的信息减少执行次数。也就是说,每次调用一个目标方法时,抽象都会应用一个缓存行为来检查是否已经为给定的参数调用了该方法。如果已调用,则返回缓存的结果,而不必调用实际的方法。如果尚未调用该方法,则会调用该方法,并将结果缓存并返回给用户,以便下次调用该方法时返回缓存的结果。这样,对于给定的一组参数,昂贵的方法(无论是CPU绑定的还是IO绑定的)只能调用一次,并且结果可以重用,而无需再次实际调用该方法。缓存逻辑的应用是透明的,不会对调用程序造成任何干扰。
这种方法只适用于保证对给定输入(或参数)返回相同输出(结果)的方法,无论调用多少次。
缓存抽象提供了其他与缓存相关的操作,例如更新缓存内容或删除一个或所有条目的能力。如果缓存处理的数据在应用程序过程中可能会发生更改,则这些功能非常有用。
与Spring框架中的其他服务一样,缓存服务是一种抽象(不是缓存实现),需要使用实际存储来存储缓存数据 — 也就是说,抽象使您无需编写缓存逻辑,但不提供实际的数据存储。这种抽象由 org.springframework.cache.Cache org.springframework.cache.CacheManager 接口具体化。
Spring提供了这种抽象的一些实现:java.util.concurrent.ConcurrentMap ,Ehcache 2.x、 Gemfire 缓存、Caffeine, 和符合JSR-107的缓存(如Ehcache 3.x)。有关插入其他缓存存储和提供程序的更多信息,请参阅 Plugging-in Different Back-end Caches
缓存抽象对于多线程和多进程环境没有特殊处理,因为这些特性由缓存实现处理。
如果您有一个多进程环境(即部署在多个节点上的应用程序),则需要相应地配置缓存提供程序。根据您的用例,在多个节点上复制相同的数据就足够了。但是,如果在应用程序过程中更改数据,则可能需要启用其他传播机制。
缓存特定项与典型的get直接等效,如果找不到,则继续并最终放置通过编程缓存交互找到的代码块。没有应用锁,多个线程可能会尝试同时加载同一项。这同样适用于废弃。如果多个线程同时尝试更新或废弃数据,则可能会使用过时的数据。某些缓存提供程序在该领域提供高级功能。有关更多详细信息,请参阅缓存提供程序的文档。
要使用缓存抽象,需要注意两个方面:
8.2. 基于声明性注释的缓存
对于缓存声明,Spring的缓存抽象提供了一组Java注释:
@Cacheable:触发缓存。
@CacheEvict:触发缓存删除。
@CachePut:在不干扰方法执行的情况下更新缓存。
@Caching:重新组合要应用于方法的多个缓存操作。
@CacheConfig:在类级别共享一些常见的缓存相关设置。
顾名思义,您可以使用 @Cacheable 来划分可缓存的方法 —— 即,将其结果存储在缓存中的方法,以便在后续调用时(使用相同的参数),返回缓存中的值,而不必实际调用该方法。在最简单的形式中,注释声明需要与注释方法关联的缓存的名称,如下例所示:
@Cacheable("books")
public Book findBook(ISBN isbn) {...}
在前面的代码片段中,findBook方法与名为books的缓存相关联。每次调用该方法时,都会检查缓存,以查看调用是否已运行且无需重复。虽然在大多数情况下,只声明一个缓存,但注释允许指定多个名称,以便使用多个缓存。在这种情况下,在调用方法之前会检查每个缓存 — 如果至少命中一个缓存,则返回关联的值。
所有其他不包含该值的缓存也会更新,即使缓存的方法实际上没有被调用。
以下示例在findBook方法上使用@Cacheable和多个缓存:
@Cacheable({"books", "isbns"})
public Book findBook(ISBN isbn) {...}
由于缓存本质上是键值存储,因此需要将缓存方法的每次调用转换为适合缓存访问的键。缓存抽象使用基于以下算法的简单密钥生成器:
只要参数具有自然键并实现有效的hashCode()和equals()方法,这种方法就可以很好地用于大多数用例。如果不是这样,你需要改变策略。
要提供不同的默认密钥生成器,您需要实现 org.springframework.cache.interceptor.KeyGenerator 接口。
默认密钥生成策略随着Spring 4.0的发布而改变。Spring的早期版本使用了密钥生成策略,对于多个密钥参数,只考虑参数的hashCode(),而不考虑equals()。这可能会导致意外的密钥冲突(有关背景信息,请参阅SPR-10237)。新的SimpleKeyGenerator对此类场景使用复合键。
如果要继续使用之前的关键策略,可以配置不推荐使用的 org.springframework.cache.interceptor.DefaultKeyGenerator 类或创建自定义的基于哈希的KeyGenerator实现。
由于缓存是通用的,因此目标方法很可能具有各种签名,这些签名无法轻易映射到缓存结构的顶部。当目标方法有多个参数,其中只有一些参数适合缓存时,这一点就会变得很明显(而其余的参数仅由方法逻辑使用)。考虑以下示例:
@Cacheable("books")
public Book findBook(ISBN isbn, boolean checkWarehouse, boolean includeUsed)
乍一看,虽然这两个布尔参数会影响书籍的查找方式,但它们对缓存没有任何用处。此外,如果两者中只有一个重要,而另一个不重要,那该怎么办?
对于这种情况,@Cacheable注释允许您指定如何通过键属性生成键。您可以使用SpEL来选择感兴趣的参数(或其嵌套属性),执行操作,甚至调用任意方法,而无需编写任何代码或实现任何接口。与默认生成器相比,这是推荐的方法,因为随着代码库的增长,签名中的方法往往会有很大的不同。虽然默认策略可能适用于某些方法,但很少适用于所有方法。
以下示例使用各种SpEL声明(如果您不熟悉SpEL,请帮自己一个忙,阅读Spring表达式语言):
@Cacheable(cacheNames="books", key="#isbn")
public Book findBook(ISBN isbn, boolean checkWarehouse, boolean includeUsed)
@Cacheable(cacheNames="books", key="#isbn.rawNumber")
public Book findBook(ISBN isbn, boolean checkWarehouse, boolean includeUsed)
@Cacheable(cacheNames="books", key="T(someType).hash(#isbn)")
public Book findBook(ISBN isbn, boolean checkWarehouse, boolean includeUsed)
前面的代码片段显示了选择某个参数、其属性之一甚至任意(静态)方法是多么容易。
如果负责生成密钥的算法过于具体或需要共享,则可以在操作上定义自定义密钥生成器。为此,请指定要使用的KeyGeneratorbean实现的名称,如下例所示:
@Cacheable(cacheNames="books", keyGenerator="myKeyGenerator")
public Book findBook(ISBN isbn, boolean checkWarehouse, boolean includeUsed)
key和keyGenerator参数是互斥的,指定这两个参数的操作会导致异常。
缓存抽象使用一个简单的CacheResolver,它使用配置的CacheManager检索在操作级别定义的缓存。
要提供不同的默认缓存解析器,您需要实现 org.springframework.cache.interceptor.CacheResolver 接口。
默认缓存解析非常适合使用单个CacheManager且没有复杂缓存解析要求的应用程序。
对于使用多个缓存管理器的应用程序,可以将cacheManager设置为用于每个操作,如下例所示:
@Cacheable(cacheNames="books", cacheManager="anotherCacheManager")
public Book findBook(ISBN isbn) {...}
您还可以以类似于替换密钥生成的方式完全替换CacheResolver。每个缓存操作都会请求解析,让实现根据运行时参数实际解析要使用的缓存。以下示例显示如何指定CacheResolver:
@Cacheable(cacheResolver="runtimeCacheResolver")
public Book findBook(ISBN isbn) {...}
自Spring 4.1以来,缓存注释的value属性不再是必需的,因为无论注释的内容如何,CacheResolver都可以提供此特定信息。
与key和keyGenerator类似,cacheManager和CacheSolver参数是互斥的,指定这两个参数的操作会导致异常,因为CacheSolver实现会忽略自定义cacheManager。这可能不是你所期望的。
在多线程环境中,某些操作可能会针对同一个参数同时调用(通常在启动时)。默认情况下,缓存抽象不会锁定任何内容,并且可能会多次计算相同的值,从而破坏缓存的目的。
对于这些特定情况,可以使用sync属性指示基础缓存提供程序在计算值时锁定缓存项。因此,只有一个线程忙于计算该值,而其他线程则被阻止,直到缓存中的条目更新为止。以下示例显示了如何使用sync属性:
@Cacheable(cacheNames="foos", sync=true)
public Foo executeExpensiveOperation(String id) {...}
这是一个可选功能,您最喜欢的缓存库可能不支持它。核心框架提供的所有CacheManager实现都支持它。有关更多详细信息,请参阅缓存提供程序的文档。
有时,一个方法可能不适合一直缓存(例如,它可能取决于给定的参数)。缓存注释通过条件参数支持此类用例,条件参数接受一个SpEL表达式,该表达式的计算值为true或false。如果为true,则缓存该方法。如果没有,它的行为就好像没有缓存该方法一样(也就是说,无论缓存中有什么值或使用了什么参数,每次都会调用该方法)。例如,仅当参数名称的长度小于32时,才会缓存以下方法:
@Cacheable(cacheNames="book", condition="#name.length() < 32")
public Book findBook(String name)
缓存抽象支持 java.util.Optional 返回类型。如果存在可选值,它将存储在关联的缓存中。如果不存在可选值,则null将存储在关联的缓存中#结果总是引用业务实体,而不是受支持的包装,因此前面的示例可以重写如下:
@Cacheable(cacheNames="book", condition="#name.length() < 32", unless="#result?.hardback")
public Optional<Book> findBook(String name)
请注意,#result仍然引用Book,而不是 Optional。因为它可能为null,所以我们使用SpEL的安全导航操作符。
每个SpEL表达式根据专用上下文进行计算。除了内置参数外,框架还提供专用的缓存相关元数据,如参数名称。下表描述了上下文可用的项,以便您可以将其用于键和条件计算:
Name | Location | Description | Example |
---|---|---|---|
methodName | Root object | The name of the method being invoked | #root.methodName |
method | Root object | The method being invoked | #root.method.name |
target | Root object | The target object being invoked | #root.target |
targetClass | Root object | The class of the target being invoked | #root.targetClass |
args | Root object | The arguments (as array) used for invoking the target | #root.args[0] |
caches | Root object | Collection of caches against which the current method is run | #root.caches[0].name |
Argument name | Evaluation context | Name of any of the method arguments. If the names are not available (perhaps due to having no debug information), the argument names are also available under the #a<#arg> where #arg stands for the argument index (starting from 0). | #iban or #a0 (you can also use #p0 or #p<#arg> notation as an alias). |
result | Evaluation context | The result of the method call (the value to be cached). Only available in unless expressions, cache put expressions (to compute the key), or cache evict expressions (when beforeInvocation is false). For supported wrappers (such as Optional), #result refers to the actual object, not the wrapper. | #result |
当需要在不干扰方法执行的情况下更新缓存时,可以使用@CachePut注释。也就是说,始终调用该方法,并将其结果放入缓存(根据@CachePut选项)。它支持与@Cacheable相同的选项,应该用于缓存填充,而不是方法流优化。以下示例使用@CachePut注释:
@CachePut(cacheNames="book", key="#isbn")
public Book updateBook(ISBN isbn, BookDescriptor descriptor)
强烈反对在同一方法上使用@CachePut和@Cacheable注释,因为它们有不同的行为。后者导致使用缓存跳过方法调用,前者强制调用以运行缓存更新。这会导致意外的行为,除了特定的转角情况(例如注释具有相互排斥的条件)之外,应该避免此类声明。还请注意,此类条件不应依赖于结果对象(即#result变量),因为这些条件是预先验证以确认排除的。
缓存抽象不仅允许填充缓存存储,还允许删除。此过程对于从缓存中删除过时或未使用的数据非常有用。与@Cacheable相反,@CacheEvict 定义了执行缓存逐出的方法(即充当从缓存中删除数据的触发器的方法)。与它的同级缓存类似,@CacheEvict 需要指定一个或多个受操作影响的缓存,允许指定自定义缓存和密钥解析或条件,并具有一个额外的参数(allEntries),该参数指示是否需要执行缓存范围的逐出,而不仅仅是项逐出(基于密钥)。以下示例从图书缓存中逐出所有条目:
@CacheEvict(cacheNames="books", allEntries=true)
public void loadBooks(InputStream batch)
使用allEntries属性从缓存中逐出所有条目。
当需要清除整个缓存区域时,此选项非常方便。而不是逐出每个条目(这需要很长时间,因为它效率很低),而是在一次操作中删除所有条目,如前一个示例所示。请注意,框架会忽略此场景中指定的任何键,因为它不适用(整个缓存被逐出,而不仅仅是一个条目)。
您还可以通过使用 beforeInvocation 属性指示逐出是在调用方法之后(默认)还是在调用方法之前发生。前者提供了与其余注释相同的语义:一旦方法成功完成,就会在缓存上运行一个操作(在本例中为逐出)。如果该方法未运行(因为它可能被缓存)或引发异常,则不会发生逐出。后者(beforeInvocation=true)导致逐出总是在调用方法之前发生。在驱逐不需要与方法结果挂钩的情况下,这很有用。
请注意,void方法可以与@CacheEvict 一起使用-当这些方法充当触发器时,将忽略返回值(因为它们不与缓存交互)。@Cacheable的情况并非如此,它将数据添加到缓存或更新缓存中的数据,因此需要一个结果。
有时,需要指定相同类型的多个注释(例如@cacheexecute或@CachePut) — 例如,因为不同缓存之间的条件或键表达式不同@缓存允许在同一方法上使用多个嵌套的@Cacheable、@CachePut和@cacheexecute注释。以下示例使用两个@cacheexecute注释:
@Caching(evict = { @CacheEvict("primary"), @CacheEvict(cacheNames="secondary", key="#p0") })
public Book importBooks(String deposit, Date date)
到目前为止,我们已经看到缓存操作提供了许多自定义选项,您可以为每个操作设置这些选项。但是,如果某些自定义选项应用于类的所有操作,则配置这些选项可能会很繁琐。例如,指定用于类的每个缓存操作的缓存名称可以替换为单个类级别定义。这就是@CacheConfig发挥作用的地方。以下示例使用@CacheConfig设置缓存的名称:
@CacheConfig("books")
public class BookRepositoryImpl implements BookRepository {
@Cacheable
public Book findBook(ISBN isbn) {...}
}
@CacheConfig是一个类级注释,允许共享缓存名称、自定义键生成器、自定义CacheManager和自定义CacheResolver。将此注释放置在类上不会启用任何缓存操作。
操作级自定义始终覆盖在@CacheConfig上设置的自定义。因此,这为每个缓存操作提供了三个级别的自定义:
重要的是要注意,尽管声明缓存注释不会自动触发它们的操作,就像Spring中的许多事情一样,该功能必须以声明方式启用(这意味着如果怀疑缓存是罪魁祸首,可以通过只删除一个配置行而不是代码中的所有注释来禁用它)。
要启用缓存注释,请将注释@EnableCaching添加到其中一个@Configuration类中:
@Configuration
@EnableCaching
public class AppConfig {
}
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:cache="http://www.springframework.org/schema/cache"
xsi:schemaLocation="
http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/cache https://www.springframework.org/schema/cache/spring-cache.xsd">
<cache:annotation-driven/>
</beans>
缓存抽象提供了几种存储集成选项。要使用它们,您需要声明一个适当的CacheManager(一个控制和管理缓存实例并可用于检索这些实例以供存储的实体)。
基于JDK的缓存实现位于 org.springframework.cache.concurrent 。它允许您使用ConcurrentHashMap作为后备缓存存储。以下示例显示了如何配置两个缓存:
<!-- simple cache manager -->
<bean id="cacheManager" class="org.springframework.cache.support.SimpleCacheManager">
<property name="caches">
<set>
<bean class="org.springframework.cache.concurrent.ConcurrentMapCacheFactoryBean" p:name="default"/>
<bean class="org.springframework.cache.concurrent.ConcurrentMapCacheFactoryBean" p:name="books"/>
</set>
</property>
</bean>
前面的代码段使用SimpleCacheManager为两个名为default和books的嵌套ConcurrentMapCache实例创建CacheManager。请注意,直接为每个缓存配置名称。
由于缓存是由应用程序创建的,因此它绑定到其生命周期,因此适合于基本用例、测试或简单应用程序。缓存扩展良好,速度非常快,但它不提供任何管理、持久性功能或逐出契约。
Ehcache 3.x完全符合JSR-107,不需要专门的支持。
Ehcache 2.x实施位于 org.springframework.cache.ehcache 包。同样,要使用它,需要声明适当的CacheManager。以下示例显示了如何执行此操作:
<bean id="cacheManager"
class="org.springframework.cache.ehcache.EhCacheCacheManager" p:cache-manager-ref="ehcache"/>
<!-- EhCache library setup -->
<bean id="ehcache"
class="org.springframework.cache.ehcache.EhCacheManagerFactoryBean" p:config-location="ehcache.xml"/>
此设置引导Spring IoC内的ehcache库(通过ehcache bean),然后将其连接到专用的CacheManager实现中。请注意,整个特定于Ehcache的配置都是从Ehcache.xml 读取。
Caffeine 是对Guava缓存的Java 8重写,其实现位于 org.springframework.cache.caffeine 并提供 Caffeine 的几种特性。
以下示例配置了一个CacheManager,用于按需创建缓存:
<bean id="cacheManager"
class="org.springframework.cache.caffeine.CaffeineCacheManager"/>
<bean id="cacheManager" class="org.springframework.cache.caffeine.CaffeineCacheManager">
<property name="cacheNames">
<set>
<value>default</value>
<value>books</value>
</set>
</property>
</bean>
GemFire是一个面向内存、支持磁盘、弹性可扩展、连续可用、主动(具有内置的基于模式的订阅通知)、全局复制的数据库,并提供功能齐全的边缘缓存。有关如何将GemFire用作CacheManager(以及更多)的更多信息,请参阅Spring数据GemFire参考文档。
Spring的缓存抽象也可以使用符合JSR-107的缓存。JCache实现位于组织中。springframework。隐藏物jcache包。
同样,要使用它,需要声明适当的CacheManager。以下示例显示了如何执行此操作:
<bean id="cacheManager"
class="org.springframework.cache.jcache.JCacheCacheManager"
p:cache-manager-ref="jCacheManager"/>
<!-- JSR-107 cache manager setup -->
<bean id="jCacheManager" .../>
有时,在切换环境或进行测试时,可能会有缓存声明,而没有配置实际的后备缓存。由于这是无效的配置,因此在运行时会引发异常,因为缓存基础结构无法找到合适的存储。在这种情况下,您可以连接一个不执行缓存的简单虚拟缓存,而不是删除缓存声明(这可能会很乏味) — 也就是说,它强制每次调用缓存的方法。以下示例显示了如何执行此操作:
<bean id="cacheManager" class="org.springframework.cache.support.CompositeCacheManager">
<property name="cacheManagers">
<list>
<ref bean="jdkCache"/>
<ref bean="gemfireCache"/>
</list>
</property>
<property name="fallbackToNoOpCache" value="true"/>
</bean>
显然,有很多缓存产品可以用作后备存储。对于那些不支持JSR-107的系统,您需要提供一个CacheManager和一个Cache实现。这听起来可能比实际情况更困难,因为在实践中,类往往是简单的适配器,将缓存抽象框架映射到存储API之上,就像ehcache类那样。大多数CacheManager类都可以使 org.springframework.cache.support 中的类。(例如AbstractCacheManager,它负责处理样板代码,只剩下要完成的实际映射)。