【本文内容】
- Spring Cache + Spring Boot集成,用默认的基于内存的Simple cache作为缓存实现。
- 基于Redis的缓存实现。
- 介绍5个重要的注解:
@Cacheable
,@CacheEvict
,@CachePut
,@Caching
,@CacheConfig
。 - 基于Hazelcast的缓存实现,如果同时依赖Hazelcast和Redis,自定义RedisCacheManager来指定使用Redis作为缓存实现。
【参考】
- https://howtodoinjava.com/spring-boot/spring-boot-cache-example/
- https://spring.io/guides/gs/caching/
【文档】
- Spring Cache: https://docs.spring.io/spring-framework/docs/5.2.8.RELEASE/spring-framework-reference/integration.html#cache
- 与Spring Boot集成:https://docs.spring.io/spring-boot/docs/2.1.6.RELEASE/reference/html/boot-features-caching.html
从Spring 3.1开始,Spring框架提供了关于缓存的抽象接口来统一不同的缓存技术(如Redis, EhCache, Hazelcast等)。Spring Cache 只负责维护抽象层,具体的实现可以自主选择。
1. 简单的例子
1.1 依赖
Spring Cache主要位于spring-context
和spring-context-support
包中,但Spring Boot整合成相关的starter。我用的是Spring Boot v2.7.0。除了必要的jpa等依赖,需要加入:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
1.2 启用Cache
使用注解@EnableCaching
来表示使用Spring Cache:
@EnableCaching
@SpringBootApplication
public class SpringCacheApplication {
public static void main(String[] args) {
SpringApplication.run(SpringCacheApplication.class, args);
}
}
如果需要关闭,可以在application.properties
中加(这个优先级高于上述的@EnableCaching):
spring.cache.type=none
1.3 Service
Course
以及CourseRepository
就是普通的JPA相关的类,这里略。
@Autowired
private CourseRepository courseRepository;
@Cacheable("courses")
public Course getById(int id) {
System.out.println("get from db for id = " + id);
Optional<Course> courseOptional = courseRepository.findById(id);
return courseOptional.isPresent() ? courseOptional.get() : null;
}
1.4 配置Cache的实现
由于Spring Cache提供的是抽象层,并没有具体的实现,所以需要我们配置相应的实现。
Spring Cache主要是通过CacheManager
来操作Cache的put/get等,所以需要配置一个这样的Bean,如果没有配置,在Spring Boot Starter中会找classpath
中有具体的实现,在Spring Boot AutoConfigure阶段会自动配置一个默认的。
具体支持的Cache实现有:
- Generic
- JCache (JSR-107) (EhCache 3, Hazelcast, Infinispan, and others)
- EhCache 2.x
- Hazelcast
- Infinispan
- Couchbase
- Redis
- Caffeine
- Simple
如果在classpath
中没有找到具体的Cache实现,那么就会用ConcurrentHashMap
来实现简单的Cache。
比如我们写个测试用例:
@SpringBootTest
public class CourseServiceTest {
@Autowired
private CourseService courseService;
@Autowired
private CacheManager cacheManager;
@Test
public void getByIdTest() {
cacheManager.getCacheNames().stream().forEach(System.out::println);
System.out.println(courseService.getById(1));
cacheManager.getCacheNames().stream().forEach(System.out::println);
System.out.println(courseService.getById(1));
}
能过debug可以看到由于没有配置classpath,会默认生成一个ConcurrentMapCacheManager的CacheManager的Bean:打印结果:可以看到标注了@Cacheable
的方法,在查询的时候,第一次会从数据库查,第二次直接从Cache里拿的:
【至此,基于内存的Simple Cache实现 + Spring Cache + Spring Boot的例子完毕。】
2. 改为Redis作为Cache的实现
使用Docker来启动Redis以及与Spring Boot的集成,参考我之前的文章:https://www.jianshu.com/p/7877ecc6a913
在classpath中加入了redis相关的依赖后:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
Debug cacheManager的bean,可以看到会从默认的ConcurrentMapCacheManager改为基于Redis的RedisCacheManager的bean:3. 一些重要的注解
-
@Cacheable
: 查找缓存:有就返回;没有就执行方法体,将结果缓存起来。 -
@CacheEvict
: 清除缓存。 -
@CachePut
: 执行方法体,将结果缓存起来。 -
@Caching
: 设置方法的复杂缓存规则。 -
@CacheConfig
: 抽取类中的所有@CachePut
,@Cacheable
,@CacheEvict
的公共配置。
3.1 @Cacheable
这个注解在上述#1.3中有配,标记在方法上,需要写上cacheNames
(可以是多个)。如:
@Cacheable("courses")
public Course getById(int id) {...}
getById(int)方法会先从缓存中查找是否有相应的结果,如果没有,才会真正的执行该方法,并且在结束后将结果放入缓存中。
如使用的Cache实现为Redis,可以看到上述@Cacheable
的运行后,在Redis中存的方式是配置的name + "::" + key(默认的key为传入的参数)
:
【其它】
还可以加个条件,比如id值大于等于2的时候才会存入cache:
@Cacheable(value = "courses", condition = "#id >= 2")
可以看到,同时查询id=1,2的时候,只有id=2的结果数据存入了Redis中:可以改变上述的默认用参数作为key的存储方式,比如:
@Cacheable(value = "courses", key = "{#root.methodName, #id}")
这样存储的key就会变为配置的name + "::" + 方法名 + "," + 参入的参数。
3.2 @CacheEvict
该注解的作用是按需清除缓存,比如数据更新了,数据删除了等。
@CacheEvict("courses")
public void deleteById(int id) {
courseRepository.deleteById(id);
}
测试用例:
@Test
public void deleteByIdTest() {
courseService.deleteById(2);
}
方法执行前缓存中有两个key,执行后会清除key为courses::2
的数据:
也可以使用allEntries=true来清理所有的cache。
@CacheEvict(value = "courses", allEntries = true)
public boolean clearAllCache() {
return true;
}
测试用例:
@Test
public void clearAllCacheTest() {
courseService.clearAll();
}
可以看到缓存中的以courses命令的cache被全部删除了:3.3 @CachePut
该注解有点像@Cacheable,只是不会先从缓存读取,而是会执行方法体,将结果缓存起来。
@CachePut("courses")
public Course update(Course course) {
Course courseInDb = courseRepository.findById(course.getId()).orElseThrow();
courseInDb.setName(course.getName());
courseInDb.setStatus(course.getStatus());
return courseRepository.save(courseInDb);
}
测试前:测试用例,更新两次:
@Test
public void updateCourse() {
Course course = new Course();
course.setId(2);
course.setName("test updated");
course.setStatus("0");
courseService.update(course);
Course course1 = new Course();
course1.setId(2);
course1.setName("test updated111");
course1.setStatus("0");
courseService.update(course1);
}
可以看到如同上述描述的,@CachePut
每次都会进入方法体,并且会把执行结果存到缓存中:
但我们的例子有个问题,即update更新的cache,key和getById不一样,所以我们可以改下:
@CachePut(value = "courses", key = "#course.id")
重新跑测试用例,可以看到key改成默认的id了:3.4 @Caching
在同一个方法上想要同时使用多个Cache相关的注解,可以使用@Caching
注解来将它们gourp起来。如:
@Caching(evict = {
@CacheEvict(cacheNames = "departments", allEntries = true),
@CacheEvict(cacheNames = "employees", key = "...")})
public boolean importEmployees(List<Employee> data) { ... }
3.5 @CacheConfig
@CacheConfig
允许我们将cache的配置放在class级别,这样就可以不同在每个方法中都重复声明,比如上述的CourseService
,我们同时对getById
, update
, deleteById
都声明了value=courses(这里的value等同于cacheNames),那么可以直接在CourseService
类上声明:
类级别注解,用于配置一些共同的选项(当方法注解声明的时候会被覆盖),例如CacheName。
@CacheConfig(cacheNames = "courses")
public class CourseServiceImpl implements CourseService {
@Cacheable
public Course getById(int id) {...}
@CachePut(key = "#course.id")
public Course update(Course course) {...}
}
如果方法级别上也有声明,那么优先级高于类级别上的声明。
4. 与Hazelcast集成
关于Spring与Hazelcast的集成,请参考我之前的文章:https://www.jianshu.com/p/913f35f41f31
这里假设已经配好Hazelcast的依赖以及hazelcast.xml
Debug cacheManager,可以看到CacheManager的实现为:HazelcastCacheManager
:
如果我把redis和hazelcast同时放在classpath
下,那么想要Spring Cache知道具体用哪个Cache实现,可以自定义CacheManager
,而不是依赖Spring Boot AutoConfigure来创建bean,比如自定义RedisCacheManager
:
@Configuration
public class CacheConfig {
@Bean
public RedisCacheManager redisCacheManager(RedisConnectionFactory redisConnectionFactory) {
RedisCacheWriter redisCacheWriter = RedisCacheWriter.nonLockingRedisCacheWriter(redisConnectionFactory);
RedisCacheConfiguration redisCacheConfiguration = RedisCacheConfiguration.defaultCacheConfig()
.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer()));
redisCacheConfiguration.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()));
return new RedisCacheManager(redisCacheWriter, redisCacheConfiguration);
}
}
这样即便classpath
下同时存在hazelcast和redis,也会始终使用redis作为Cache实现。