SpringCache
日常开发中,对数据进行缓存是优化性能极为重要的一环。Java可以使用的缓存组件有很多,比如JDK自带并发工具包中的ConcurrentMap,或是基于JVM内存的EhCache,以及后来流行起来的缓存中间件Memcached、Redis等。我们直接使用这些组件实现数据缓存,要不胜其烦的编写很多代码,比如“判断缓存中是否存在数据,存在则取缓存,不存在则查数据库并写入缓存”这样的逻辑。SpringCache则对缓存做了一个封装,并支持注解配置,使用非常方便,一些最简单的缓存逻辑我们可以使用该功能来极大的简化代码。
这篇笔记我们以SpringBoot3.x工程为例,介绍SpringCache的用法。
缓存相关注解
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=jcache
这里我们需要根据实际使用的缓存底层实现选择,后文会介绍集成EhCache(通过JCache规范)和Redis的方法,但要注意不同的缓存底层实现可能在配置方法上有区别。
最后,我们需要在工程的启动类上添加@EnableCaching注解配置开启缓存。
package com.gacfox.demo;
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);
}
}
不过此时还SpringCache还不能使用,我们还得集成缓存组件,下面我们介绍集成EhCache和Redis两种组件。
集成EhCache
对于单机应用,使用内存作为缓存性能相比Redis更好,EhCache是一个比较常用的内存缓存框架。如果使用EhCache缓存,我们需要引入JSR-107(JCache)和EhCache依赖。在SpringBoot工程中,这两个依赖的版本都由SpringBoot基础工程管理,我们可以不指定版本号。
<dependency>
<groupId>org.ehcache</groupId>
<artifactId>ehcache</artifactId>
<classifier>jakarta</classifier>
</dependency>
<dependency>
<groupId>javax.cache</groupId>
<artifactId>cache-api</artifactId>
</dependency>
注意:SpringBoot2.x和3.x这里发生了破坏性变更。对于旧版本SpringBoot2.x,需要引入的是net.sf.ehcache下的ehcache包,而升级到Spring3.x后,首先是EhCache的groupId和version都发生了变化,其次是集成缓存的API从直接调用EhCache变更为了JSR-107(JCache)。
在application.properties配置中,我们需要配置缓存类型为jcache,此外还需要指定ehcache.xml配置文件的路径。
spring.cache.type=jcache
spring.cache.jcache.config=classpath:ehcache.xml
ehcache.xml配置文件内容例子如下。
<?xml version="1.0" encoding="UTF-8"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://www.ehcache.org/v3"
xmlns:jsr107="http://www.ehcache.org/v3/jsr107"
xsi:schemaLocation="http://www.ehcache.org/v3
http://www.ehcache.org/schema/ehcache-core-3.10.xsd
http://www.ehcache.org/v3/jsr107
http://www.ehcache.org/schema/ehcache-107-ext-3.10.xsd">
<!-- 磁盘持久化目录 -->
<persistence directory="java.io.tmpdir"/>
<cache alias="users">
<!-- 键类型 -->
<key-type>java.lang.String</key-type>
<!-- 值类型 -->
<value-type>java.util.List</value-type>
<!-- 过期策略 -->
<expiry>
<ttl unit="seconds">120</ttl>
</expiry>
<!-- 存储层配置 -->
<resources>
<!-- 堆内最多1024条 -->
<heap unit="entries">1024</heap>
<!-- 开启磁盘溢出,重启后不持久化 -->
<disk unit="MB" persistent="false">100</disk>
</resources>
</cache>
</config>
ehcache.xml配置文件中,我们需要根据实际需要配置缓存的类型、过期策略、持久化策略等参数。此时SpringCache缓存就可以正常工作了。
集成Redis
SpringCache集成Redis非常简单,我们需要引入Spring Data Redis依赖。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
引入依赖后,首先配置Spring Data Redis的连接信息,然后配置SpringCache缓存类型为redis即可。此外,这里也可以对缓存的键前缀、超时时间等进行配置。
spring.data.redis.host=127.0.0.1
spring.data.redis.port=6379
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()方法的查询结果。该方法第一次调用时会真实执行内部的代码,但随后的调用中,在缓存过期之前,都会返回缓存的数据。
package com.gacfox.demo.service;
import com.gacfox.demo.model.User;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;
import java.util.List;
@Slf4j
@Service("demoService")
public class DemoService {
@Cacheable(cacheNames = "users", key = "'ALL'")
public List<User> queryUserList() {
// 模拟查询用户列表
log.info("查询用户列表");
User u1 = User.builder().name("Tom").build();
User u2 = User.builder().name("Jerry").build();
return List.of(u1, u2);
}
}
代码中,cacheNames是缓存名,如果你使用的是EhCache,这个名字需要和配置文件中的配置项对应;如果你用的是Redis,它会作为Redis Key的一部分自动创建。key属性是缓存的键,这个属性非常重要,我们可以看到它是一个SpEL表达式,通常是和方法的参数完全对应的,我们这里被缓存的方法没有参数,因此使用了固定的键名ALL。此外由于key属性的值是SpEL表达式,因此字符串ALL外我们需要增加单引号。
如果被缓存的方法有参数,我们可以取参数作为缓存键。
@Cacheable(cacheNames = "users", key = "#id")
实际上,SpEL是一种表达式,其中可以包含字符串拼接、计算等逻辑,因此如果有多个逻辑也问题不大,我们用SpEL拼接即可。
@Cacheable(cacheNames = "district", key = "#cityCode + '_' + #districtName")
public District getAreaByName(String cityCode, String districtName) { }
手动清除缓存
默认情况下,缓存会在超时后删除,但有时我们也需要手动让缓存失效,此时可以使用@CacheEvict。下面例子代码中,我们对queryUserList()方法进行了缓存,它使用ALL作为键。而执行updateUsers()方法会导致ALL键缓存的清除。
@Slf4j
@Service("demoService")
public class DemoService {
@Cacheable(cacheNames = "users", key = "'ALL'")
public List<User> queryUserList() {
// ... 具体的业务逻辑
return users;
}
@CacheEvict(cacheNames = "users", key = "'ALL'")
public void updateUsers() {
// ... 具体的业务逻辑
}
}
默认情况下,@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.beans.factory.annotation.Autowired;
import org.springframework.cache.Cache;
import org.springframework.cache.CacheManager;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
@Slf4j
@Service("demoService")
public class DemoService {
private final CacheManager cacheManager;
@Autowired
public DemoService(CacheManager cacheManager) {
this.cacheManager = cacheManager;
}
public User queryUserById(String id) {
if (!StringUtils.hasText(id)) {
return null;
}
Cache cache = cacheManager.getCache("users");
if (cache == null) {
throw new RuntimeException("找不到缓存配置");
}
User user = cache.get(id, User.class);
if (user != null) {
return user;
}
// 模拟从数据库查询
log.info("查询用户列表");
User dbUser = User.builder().name("Tom").build();
if (dbUser != null) {
cache.put(id, dbUser);
}
return dbUser;
}
}
代码中,我们先通过CacheManager实现获取了Cache实例,它需要一个缓存名,这和@Cacheable的cacheNames作用是相同的。获取Cache实例后,我们就可以通过get()、put()、evict()、clear()等方法操作缓存了。