知识 - Spring Cache
# 前言
本内容介绍 Spring Cache 的自定义实现缓存功能,如果只是使用 Spring Cache 的内置缓存功能或者了解 Spring Cache API,请看 Spring - Cache
Spring Cache 内置了很多缓存方案,但是有场景需要既有本地缓存,也有 Redis 缓存。将热点数据缓存到本地,这样获取可以快速返回,然后其他数据缓存到 Redis,需要时去 Redis 获取。
下面介绍如何实现 Caffeine + Redis 缓存方案,Caffeine 作为一级缓存,Redis 作为二级缓存。
实现步骤:
自定义类实现 Cache 接口。该接口是 Spring Cache 提供的自定义缓存类,比如 Spring Cache 内置了 RedisCache、EhCache、JCache、CaffeineCache 等缓存类,这些缓存的实现也是实现了 Cache 接口,重写里面的方法。
Cache 接口需要实现的方法都是对缓存数据的增删改查,也就是说,缓存的增删改查,都是通过 Cache 接口来实现的,因此我们自定义缓存类,就必须实现 Cache,这样 Spring 就会调用实现了这些增删改查方法的自定义类。
自定义类具体看 CaffeineRedisCache 类。实现增删改查的方法里,调用了 Caffeine 和 RedisCache 的增删改查方法。就形成了 Spring Cache 调用 Cache 的增删改查方法 -> 调用 Caffeine 和 RedisCache 的增删改查方法。实现一级和二级缓存
自定义类实现 CacheManager 接口。该接口是 Spring Cache 提供的自定义缓存管理类,比如 Spring Cache 内置了 Redis、EhCache、JCache、Caffeine 等缓存管理类,这些缓存管理类的实现也是实现了 CacheManager 接口,重写里面的方法。
CacheManager 接口需要实现的方法就是获取 Cache 缓存类,比如 RRedisCache、EhCache、JCache、CaffeineCache 等。
自定义类具体看 CaffeineRedisCacheManager 类。在该类里,获取了 RedisCacheManager,获取该 Manager 的目的是获取 RedisCache,因为 RedisCache 内置了对 Redis 的缓存操作。然后将 RedisCache 传入自定义缓存类 CaffeineRedisCache 里,实现缓存增删改查
当我们使用 Cacheable、CachePut、CacheEvict 等注解后,最终会会调用 CacheManager 的 getCache 方法,从而获取 Cache 类,最后调用 Cache 类的增删改查缓存操作。
# 实现
依赖:
<dependencies>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.32</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
</dependency>
</dependencies>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 缓存实现
Caffeine + Redis 实现双重缓存
@RequiredArgsConstructor
public class CaffeineRedisCache implements Cache {
// 从 CacheConfiguration 获取缓存实例
private static final com.github.benmanes.caffeine.cache.Cache<Object, Object> CAFFEINE = SpringHelper.getBean("caffeine");
private final Cache cache;
public String getUniqueKey(Object key) {
return cache.getName() + ":" + key;
}
@Override
public String getName() {
return cache.getName();
}
@Override
public Object getNativeCache() {
return cache.getNativeCache();
}
@Override
public ValueWrapper get(Object key) {
// 从 Caffeine 获取缓存,如果缓存不存在,则从 cache 获取,并缓存到 Caffeine
Object o = CAFFEINE.get(getUniqueKey(key), k -> cache.get(key));
return (ValueWrapper) o;
}
@Override
public <T> T get(Object key, Class<T> type) {
// 从 Caffeine 获取缓存,如果缓存不存在,则从 cache 获取,并缓存到 Caffeine
Object o = CAFFEINE.get(getUniqueKey(key), k -> cache.get(key, type));
return (T) o;
}
@Override
public <T> T get(Object key, Callable<T> valueLoader) {
Object o = CAFFEINE.get(getUniqueKey(key), k -> cache.get(key, valueLoader));
return (T) o;
}
@Override
public void put(Object key, Object value) {
// 在 Caffeine 删除缓存,不能存储 value,否则取出来类型转换报错
CAFFEINE.invalidate(getUniqueKey(key));
cache.put(key, value);
}
@Override
public void evict(Object key) {
// 在 cache 删除缓存
boolean b = cache.evictIfPresent(key);
if (b) {
// 在 Caffeine 删除缓存
CAFFEINE.invalidate(getUniqueKey(key));
}
}
@Override
public void clear() {
cache.clear();
CAFFEINE.invalidateAll();
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
# 缓存管理
Caffeine + Redis 双重缓存管理
@RequiredArgsConstructor
public class CaffeineRedisCacheManager implements CacheManager {
Map<String, Cache> cacheMap = new ConcurrentHashMap<>();
private final RedisConnectionFactory redisConnectionFactory;
private final CacheProperties cacheProperties;
@Override
public Cache getCache(String cacheName) {
// 重写 cacheName 支持多参数。如:#cacheName#ttl
String[] array = StringUtils.delimitedListToStringArray(cacheName, "#");
cacheName = array[0];
Cache cache = cacheMap.get(cacheName);
if (Objects.nonNull(cache)) {
return cache;
}
// Redis 缓存配置项
RedisCacheConfiguration configuration = createConfiguration(cacheProperties);
// cacheName 后面的参数默认是 ttl
if (array.length > 1) {
long seconds = DurationStyle.detectAndParse(array[1]).toSeconds();
configuration.entryTtl(Duration.ofSeconds(seconds));
}
// 创建 RedisCacheManager,从里面获取 Redis 的缓存类
RedisCacheManager redisCacheManager = new RedisCacheManager(RedisCacheWriter.nonLockingRedisCacheWriter(redisConnectionFactory), configuration);
// 获取 RedisCache,里面只想 Redis 封装了 Redis 的命令操作
Cache redisCache = redisCacheManager.getCache(cacheName);
CaffeineRedisCache newCache = new CaffeineRedisCache(redisCache);
cacheMap.put(cacheName, newCache);
return newCache;
}
/**
* 参考 Spring 实现 Redis Cache 的配置项:{@link org.springframework.boot.autoconfigure.cache.RedisCacheConfiguration#createConfiguration}
*
* @param cacheProperties Spring Cache 缓存配置项
*/
private RedisCacheConfiguration createConfiguration(CacheProperties cacheProperties) {
CacheProperties.Redis redisProperties = cacheProperties.getRedis();
RedisCacheConfiguration config = RedisCacheConfiguration
.defaultCacheConfig()
.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()))
.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(getJacksonSerializer()));
if (Objects.nonNull(redisProperties.getTimeToLive())) {
config = config.entryTtl(redisProperties.getTimeToLive());
}else {
// 默认 2 小时
config = config.entryTtl(Duration.ofHours(2));
}
if (Objects.nonNull(redisProperties.getKeyPrefix())) {
config = config.prefixCacheNameWith(redisProperties.getKeyPrefix());
}
if (!redisProperties.isCacheNullValues()) {
config = config.disableCachingNullValues();
}
if (!redisProperties.isUseKeyPrefix()) {
config = config.disableKeyPrefix();
}
return config;
}
/**
* 获取 Jackson 序列化器,序列化到 Redis 的值,方法参考 {@link cn.youngkbt.redis.config.RedisTemplateConfig#initRedisTemplate(RedisTemplate)}
* @return Jackson 序列化器
*/
private Jackson2JsonRedisSerializer<Object> getJacksonSerializer() {
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
objectMapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL);
// 解决 Redis 无法存入 LocalDateTime 等 JDK8 的时间类
JavaTimeModule javaTimeModule = new JavaTimeModule();
/*
* 新增 LocalDateTime 序列化、反序列化规则
*/
javaTimeModule
.addSerializer(LocalDateTime.class, new LocalDateTimeSerializer(DatePatternPlus.NORM_DATETIME_FORMATTER)) // yyyy-MM-dd HH:mm:ss
.addSerializer(LocalTime.class, new LocalTimeSerializer(DateTimeFormatter.ISO_LOCAL_TIME)) // HH:mm:ss
.addSerializer(Instant.class, InstantSerializer.INSTANCE) // Instant 类型序列化
.addDeserializer(LocalDateTime.class, new LocalDateTimeDeserializer(DatePatternPlus.NORM_DATETIME_FORMATTER)) // yyyy-MM-dd HH:mm:ss
.addDeserializer(LocalDate.class, new LocalDateDeserializer(DateTimeFormatter.ISO_LOCAL_DATE)) // yyyy-MM-dd
.addDeserializer(LocalTime.class, new LocalTimeDeserializer(DateTimeFormatter.ISO_LOCAL_TIME)) // HH:mm:ss
.addDeserializer(Instant.class, InstantDeserializer.INSTANT);// Instant 反序列化
objectMapper.registerModules(javaTimeModule);
// 使用 Jackson2JsonRedisSerialize 替换默认序列化
return new Jackson2JsonRedisSerializer<>(objectMapper, Object.class);
}
@Override
public Collection<String> getCacheNames() {
return Collections.unmodifiableSet(cacheMap.keySet());
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
# 配置项
Spring Boot Application 配置项
@Data
@ConfigurationProperties("spring.cache.caffeine")
public class CaffeineCacheProperties {
/**
* 缓存过期时间,单位秒
*/
private Duration expire = Duration.ofSeconds(30);
/**
* 缓存初始化容量
*/
private int capacity = 100;
/**
* 缓存最大容量
*/
private Long maximumSize = 1000L;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 容器装配
@EnableCaching
@AutoConfiguration(after = RedisConnectionFactory.class)
@EnableConfigurationProperties({CaffeineCacheProperties.class, CacheProperties.class})
public class CacheConfiguration {
/**
* caffeine 本地缓存处理器
*/
@Bean
public Cache<Object, Object> caffeine(CaffeineCacheProperties properties) {
return Caffeine.newBuilder()
// 过期时间
.expireAfterWrite(properties.getExpire())
// 初始的缓存空间大小
.initialCapacity(properties.getCapacity())
// 缓存的最大条数
.maximumSize(properties.getMaximumSize())
.build();
}
/**
* 自定义缓存管理器 整合spring-cache
*/
@Bean
public CacheManager cacheManager(RedisConnectionFactory redisConnectionFactory, CacheProperties properties) {
return new CaffeineRedisCacheManager(redisConnectionFactory, properties);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
Spring Boot 3.x 需要在 resource 下建立 META-INF/spring
路径,然后创建 org.springframework.boot.autoconfigure.AutoConfiguration.imports
文件,内容为
cn.youngkbt.cache.config.CacheConfiguration
这样 Spring 会自动扫描该文件的容器装配类,将里面涉及的类注入到 Spring 容器。
# 示例
首先在 application 里配置 Spring Cahce 时,不要使用 type,然后正常使用 Spring Cache 提供的注解即可,缓存的逻辑将使用上面实现的缓存逻辑。