SpringCache
日常开发中,对数据进行缓存是优化性能极为重要的一环。Java可以使用的缓存组件有很多,比如JDK自带并发工具包中的ConcurrentMap,或是基于JVM内存的EhCache,以及后来流行起来的缓存中间件Memcached、Redis等。我们直接使用这些组件实现数据缓存,要不胜其烦的编写很多代码,比如“判断缓存中是否存在数据,存在则取缓存,不存在则查数据库并写入缓存”这样的逻辑。
SpringCache则对缓存做了一个封装,并支持注解配置,使用非常方便,一些最简单的缓存逻辑我们可以使用该功能来极大的简化代码,我们这里以SpringBoot工程为例简单了解一下。
缓存相关注解
SpringCache提供了几个注解封装了不同的缓存组件,使用非常方便。
@Cacheable:先判断缓存是否存在,如果缓存存在直接返回缓存,如果缓存不存在则将方法执行后的结果添加到缓存。
@CachePut:执行方法后,将方法执行后的结果更新到缓存。
@CacheEvict:执行方法后移除指定key的缓存。
这些注解可以标注在方法上,它们可以单独使用也可以同时使用。以上方法常用的参数有value和key:
value:缓存命名空间,一个命名空间对应一组缓存,该参数是必传的。key:缓存键,可以使用固定字符串,也可以使用SpEL表达式指定,可以指定为操作的数据的主键。如果不指定,SpringCache会根据方法参数值生成缓存键。condition:缓存逻辑执行的条件,使用SpEL进行条件判断。
@Caching:用于组合以上操作。
引入依赖和框架配置
SpringCache提供了SpringBoot的起步依赖,我们将其引入。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
此外,还需要在application.properties配置文件中配置缓存类型。
spring.cache.type=redis
这里我们需要根据实际使用的缓存底层实现选择,后文会介绍集成EhCache和Redis的方法,但要注意不同的缓存底层实现可能在配置方法上有区别。
最后,我们需要在工程的启动类上添加@EnableCaching注解配置开启缓存。
package com.gacfox.democache;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cache.annotation.EnableCaching;
@SpringBootApplication
@EnableCaching
public class DemocacheApplication {
public static void main(String[] args) {
SpringApplication.run(DemocacheApplication.class, args);
}
}
集成EhCache
对于单机应用,使用内存作为缓存性能相比Redis更好,EhCache是一个比较常用的内存缓存框架。如果使用EhCache缓存,我们需要引入EhCache的依赖。在SpringBoot工程中,该依赖的版本由SpringBoot基础工程管理,我们可以不指定版本号。
<dependency>
<groupId>net.sf.ehcache</groupId>
<artifactId>ehcache</artifactId>
</dependency>
注意:EhCache2.x的groupId是net.sf.ehcache,3.x的groupId是org.ehcache,对于SpringBoot2.x我们需要引入2.x的EhCache,不要写错groupId搞错版本,新版本实测是未兼容的。
在application.properties配置中,我们需要配置缓存类型为ehcache,此外还需要指定ehcache.xml配置文件的路径。
spring.cache.type=ehcache
spring.cache.ehcache.config=classpath:ehcache.xml
ehcache.xml配置文件内容例子如下。
<?xml version="1.0" encoding='UTF-8'?>
<ehcache>
<diskStore path="java.io.tmpdir"/>
<cache
name="users"
maxElementsInMemory="1024"
eternal="false"
timeToIdleSeconds="120"
timeToLiveSeconds="120"
overflowToDisk="true"
diskPersistent="false"
/>
</ehcache>
ehcache.xml配置文件中,我们需要根据实际需要配置缓存的命名空间、大小、超时等参数。
此时SpringCache缓存就可以正常工作了。
集成Redis
SpringCache集成Redis非常简单,我们需要引入SpringDataRedis依赖。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
引入依赖后,配置SpringCache缓存类型为redis即可。此外,这里也可以对缓存的键前缀、超时时间等进行配置。
spring.cache.type=redis
spring.cache.redis.time-to-live=120s
spring.cache.redis.use-key-prefix=true
spring.cache.redis.key-prefix=CACHE_
缓存方法返回值
下面例子中,我们使用@Cacheable缓存了queryUserList()方法的查询结果。该方法第一次调用时会真实执行内部的代码,但随后的调用中,在缓存过期之前,都会返回缓存的数据。
@Slf4j
@Service("demoService")
public class DemoService {
@Cacheable("users")
public List<User> queryUserList() {
// ... 具体的业务逻辑
return users;
}
}
如果方法存在参数,缓存键会自动根据方法参数生成,缓存键可以简单理解为Redis的Key,它实际上就是一个用来判断缓存命中和取缓存值的字符串。下面例子中,缓存会根据参数id进行判断,相同参数的查询会生成相同的缓存键,因此会返回同一个缓存。
@Slf4j
@Service("demoService")
public class DemoService {
@Cacheable("users")
public User queryUserById(Long id) {
// ... 具体的业务逻辑
return user;
}
}
对于缓存键我们也可以手动指定,下面例子使用SpEL指定了key参数。
@Cacheable(value = "users", key = "#id")
SpEL是一种表达式,其中可以包含字符串拼接、计算等逻辑,下面是一个例子。
@Cacheable(value = "district", key = "#cityCode + '_' + #districtName")
public District getAreaByName(String cityCode, String districtName) { }
手动清除缓存
默认情况下,缓存会在超时后删除,但有时我们也需要手动让缓存失效,此时可以使用@CacheEvict。下面例子代码中,我们对queryUserList()方法进行了缓存,它使用ALL作为键。而执行updateUsers()方法会导致ALL键缓存的清除。
@Slf4j
@Service("demoService")
public class DemoService {
@Cacheable(value = "users", key = "'ALL'")
public List<User> queryUserList() {
// ... 具体的业务逻辑
return users;
}
@CacheEvict(value = "users", key = "'ALL'")
public void updateUsers() {
// ... 具体的业务逻辑
}
}
注意:这里的字符串key = "'ALL'"表示名字叫ALL的缓存键,key属性的值是SpEL表达式,因此字符串ALL外我们需要增加单引号。
默认情况下,@CacheEvict在方法执行后移除缓存,我们也可以指定参数beforeInvocation = true指定在方法执行前移除缓存。
编程式操作缓存
除了使用SpringCache提供的注解,我们也可以通过代码的方式读写缓存,这需要使用SpringCache的CacheManager接口。项目中配置SpringCache的起步依赖后,我们可以通过依赖注入的方式获取配置好的缓存实现,下面例子中,我们注入CacheManager实现并读取缓存。
package com.gacfox.demo.service;
import com.gacfox.demo.model.User;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cache.Cache;
import org.springframework.cache.CacheManager;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
@Slf4j
@Service("demoService")
public class DemoService {
@Resource
private CacheManager cacheManager;
public User queryUserById(Long id) {
Cache cache = cacheManager.getCache("user_cache");
if (cache != null) {
return cache.get(id, User.class);
} else {
return null;
}
}
}
代码中,我们先通过CacheManager实现获取了Cache实例,它需要一个缓存名,这和@Cacheable的value作用是相同的。获取Cache实例后,我们就可以通过get()、put()、evict()、clear()等方法操作缓存了。