Spring中的缓存指南
1.缓存抽象
在本教程中,我们将学习如何使用Spring中的缓存抽象,并普遍提高我们的系统的性能。
我们将为一些真实世界的方法实例启用简单的缓存,并讨论如何通过智能的缓存管理来切实提高这些调用的性能。
2.入门
Spring提供的核心缓存抽象位于spring-context模块中。因此,使用Maven时,我们的pom.xml应该包含以下依赖关系:
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.3.3</version>
</dependency>
有趣的是,还有一个名为spring-context-support的模块,它位于spring-context 模块之上,并提供了一些CacheManagers 由EhCache或Caffeine等支持。如果我们想使用这些作为我们的缓存存储,那么我们需要使用spring-context-support 模块来代替:
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context-support</artifactId>
<version>5.3.3</version>
</dependency>
由于 spring-context-support 模块可传递地依赖于 spring-context 模块,因此不需要为 spring-context 单独声明依赖关系。
2.1. Spring Boot
如果我们使用Spring Boot,那么我们可以利用spring-boot-starter-cache 启动包来轻松地添加缓存依赖项:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
<version>2.4.0</version>
</dependency>
在引擎盖下,启动器带来了spring-context-support模块。
3.启用缓存
为了实现缓存,Spring很好地利用了注解,就像在框架中启用任何其他配置级别的功能一样。
我们可以通过在任何一个配置类中添加@EnableCaching注解来启用缓存功能:
@Configuration
@EnableCaching
public class CachingConfig {
@Bean
public CacheManager cacheManager() {
return new ConcurrentMapCacheManager("addresses");
}
}
当然,我们也可以用XML配置来启用高速缓存管理:
<beans>
<cache:annotation-driven />
<bean id="cacheManager" class="org.springframework.cache.support.SimpleCacheManager">
<property name="caches">
<set>
<bean
class="org.springframework.cache.concurrent.ConcurrentMapCacheFactoryBean"
name="addresses"/>
</set>
</property>
</bean>
</beans>
注意:在我们启用缓存后,对于最小的设置,我们必须注册一个cacheManager。
3.1.使用Spring Boot
当使用Spring Boot时,仅仅在classpath上出现启动包和EnableCaching 注解就可以注册相同的ConcurrentMapCacheManager.,所以不需要单独的Bean声明。
此外,我们还可以使用一个或多个CacheManagerCustomizer<T> Bean来定制自动配置的 CacheManager :
@Component
public class SimpleCacheCustomizer
implements CacheManagerCustomizer<ConcurrentMapCacheManager> {
@Override
public void customize(ConcurrentMapCacheManager cacheManager) {
cacheManager.setCacheNames(asList("users", "transactions"));
}
}
CacheAutoConfiguration 自动配置选择这些自定义器,并在其完全初始化之前将其应用于当前的CacheManager 。
4.使用注解的缓存
一旦我们启用了缓存,下一步就是用声明式注解将缓存行为绑定到方法中。
4.1. @Cacheable
启用方法的缓存行为的最简单方法是用@Cacheable来划定它,并用存储结果的缓存名称对它进行参数化:
@Cacheable("addresses")
public String getAddress(Customer customer) {...}
getAddress()调用将首先检查缓存的addresses,然后再实际调用该方法,并将结果缓存起来。
虽然在大多数情况下,一个缓存就足够了,但Spring框架也支持将多个缓存作为参数来传递:
@Cacheable({"addresses", "directory"})
public String getAddress(Customer customer) {...}
在这种情况下,如果任何一个缓存包含了所需的结果,那么该结果将被返回,该方法将不会被调用。
4.2. @CacheEvict
现在,让所有的方法@Cacheable会有什么问题呢?
问题在于大小。我们不想用我们不经常需要的值来填充缓存。缓存可以变得相当大,相当快,而且我们可能会保留很多陈旧或未使用的数据。
我们可以使用@CacheEvict注解来表示删除一个或多个/所有的值,这样新的值就可以再次被加载到缓存中:
@CacheEvict(value="addresses", allEntries=true)
public String getAddress(Customer customer) {...}
这里我们使用额外的参数allEntries与要清空的缓存一起使用;这将清除缓存中的所有条目addresses,并为新的数据做准备。
4.3. @CachePut
虽然@CacheEvict通过删除陈旧和未使用的条目来减少在大型缓存中查找条目的开销,但我们希望避免从缓存中驱逐过多的数据。
相反,每当我们改变这些条目时,我们都会有选择地更新这些条目。
通过@CachePut注解,我们可以在不干扰方法执行的情况下更新缓存的内容。也就是说,该方法将一直被执行,并将结果缓存起来:
@CachePut(value="addresses")
public String getAddress(Customer customer) {...}
@Cacheable和@CachePut的区别在于,@Cacheable会跳过运行方法,而@CachePut会真正地运行方法,然后将其结果放入缓存中。
4.4. @Caching
如果我们想使用同一类型的多个注解来缓存一个方法怎么办?让我们看看一个不正确的例子:
@CacheEvict("addresses")
@CacheEvict(value="directory", key=customer.name)
public String getAddress(Customer customer) {...}
上面的代码将无法编译,因为Java不允许为一个给定的方法声明多个相同类型的注解。
上述问题的解决方法是:
@Caching(evict = {
@CacheEvict("addresses"),
@CacheEvict(value="directory", key="#customer.name") })
public String getAddress(Customer customer) {...}
如上面的代码片段所示,我们可以将多个缓存注解与Caching组合在一起,并使用它来实现我们自己的定制缓存逻辑。
4.5. @CacheConfig
通过@CacheConfig注解,我们可以将一些缓存配置精简到类级别的一个地方,这样我们就不用多次声明了:
@CacheConfig(cacheNames={"addresses"})
public class CustomerDataService {
@Cacheable
public String getAddress(Customer customer) {...}
5.条件性缓存
有时,缓存可能并不是在所有情况下对一个方法都能很好地工作。
重复我们在@CachePut注解中的例子,这将同时执行该方法,并在每次都缓存结果:
@CachePut(value="addresses")
public String getAddress(Customer customer) {...}
5.1.条件参数
如果我们想更多地控制注解何时被激活,我们可以用一个条件参数对@CachePut进行参数化,该参数接收一个SpEL表达式,并确保根据对该表达式的评估来缓存结果:
@CachePut(value="addresses", condition="#customer.name=='Tom'")
public String getAddress(Customer customer) {...}
5.2. Unless参数
我们还可以通过 unless 参数控制缓存基于方法的输出而不是输入:
@CachePut(value="addresses", unless="#result.length()<64")
public String getAddress(Customer customer) {...}
上述注解将缓存地址,除非它们短于64个字符。
重要的是要知道condition和unless参数可以和所有的缓存注解一起使用。
这种条件性缓存对于管理大型结果来说是相当有效的。它对于根据输入参数定制行为也很有用,而不是对所有操作强制执行一个通用行为。
6.基于XML的声明式缓存
如果我们不能访问我们的应用程序的源代码,或者想从外部注入缓存行为,我们也可以使用基于XML的声明式缓存。
这里是我们的XML配置:
<!-- the service that you wish to make cacheable -->
<bean id="customerDataService"
class="com.your.app.namespace.service.CustomerDataService"/>
<bean id="cacheManager"
class="org.springframework.cache.support.SimpleCacheManager">
<property name="caches">
<set>
<bean
class="org.springframework.cache.concurrent.ConcurrentMapCacheFactoryBean"
name="directory"/>
<bean
class="org.springframework.cache.concurrent.ConcurrentMapCacheFactoryBean"
name="addresses"/>
</set>
</property>
</bean>
<!-- define caching behavior -->
<cache:advice id="cachingBehavior" cache-manager="cacheManager">
<cache:caching cache="addresses">
<cache:cacheable method="getAddress" key="#customer.name"/>
</cache:caching>
</cache:advice>
<!-- apply the behavior to all the implementations of CustomerDataService interface->
<aop:config>
<aop:advisor advice-ref="cachingBehavior"
pointcut="execution(* com.your.app.namespace.service.CustomerDataService.*(..))"/>
</aop:config>
7.基于java的缓存
下面是等效的Java配置:
@Configuration
@EnableCaching
public class CachingConfig {
@Bean
public CacheManager cacheManager() {
SimpleCacheManager cacheManager = new SimpleCacheManager();
cacheManager.setCaches(Arrays.asList(
new ConcurrentMapCache("directory"),
new ConcurrentMapCache("addresses")));
return cacheManager;
}
}
这里是我们的CustomerDataService:
@Component
public class CustomerDataService {
@Cacheable(value = "addresses", key = "#customer.name")
public String getAddress(Customer customer) {
return customer.getAddress();
}
}
8.摘要
在这篇文章中,我们讨论了Spring中缓存的基础知识,以及如何通过注解很好地利用这一抽象概念。
这篇文章的完整实现可以在GitHub项目中找到。