目录
3. 实验(读取Redis缓存配置属性&执行Redis缓存的clean() 指令)
0. 引言
Java Spring Boot框架能够帮助我们便捷地为应用容器整合各式服务,但也将很多底层细节“藏匿”了起来,笔者在研究Redis服务器作为缓存时的配置问题时搜到一篇有趣的文章:
https://www.shenyanchao.cn/blog/2018/07/23/spring-cache-redis-annotation/
其对配置选项“setUsePrefix”做出的解读如下所示:
- 坑1:不使用
prefix
,需要额外的zset来保存已知key集合,风险点是zset有可能很大,占用空间,如果被置换出去,功能则不一致- 坑2:使用
prefix
, 没有额外的zset。但是失效或者清理所有key的时候,使用keys *
可能导致redis被拖死,清理时间内无响应。
那么在(较)新版本的spring-boot-starter-data-redis中还是否有这个说法呢?笔者决定通过源代码和实际测试两种方法进行尝试:
1. 组件版本
jedis:3.6.0
spring-boot-starter-data-redis:2.5.1
Redis:4.0.25
2. 源码分析
我们需要分析的是:“org.springframework.data.redis.cache.RedisCache
”类中的clear()方法:
public void clear() {
byte[] pattern = (byte[])this.conversionService.convert(this.createCacheKey("*"), byte[].class);
this.cacheWriter.clean(this.name, pattern);
}
其中,this.name为配置文件中对缓存的命名。RedisCacheWriter分为wait和nonWait两类,与这个this.name有关,有待进一步解释。
clear()函数中并没有判断配置选项usePrefix是否为真,并调用相应的函数。
createCacheKey方法传入的值为字符串"*",不难联想到这一步的目的是为了获取所有和缓存相关的Key的名称(和上文引用的文献的第二种方法对应)。为了进一步确认,继续看createCacheKey方法:
protected String createCacheKey(Object key) {
String convertedKey = this.convertKey(key);
return !this.cacheConfig.usePrefix() ? convertedKey : this.prefixCacheKey(convertedKey);
}
该方法首先使用convertKey对输入参数key进行处理,然后对usePrefix的值进行了判断,当usePrefix为真时,需要调用prefixCacheKey方法进一步做处理。而convertKey方法如下所示:
protected String convertKey(Object key) {
if (key instanceof String) {
return (String)key;
} else {
......
}
}
因为"*"是字符串类型,因此余下部分的逻辑在此不必理会。
而prefixCacheKey方法如下所示:
private String prefixCacheKey(String key) {
return this.cacheConfig.getKeyPrefixFor(this.name) + key;
}
连续追踪方法getKeyPrefixFor函数,发现如下所示接口中的compute方法被调用:
@FunctionalInterface
public interface CacheKeyPrefix {
String SEPARATOR = "::";
String compute(String var1);
static CacheKeyPrefix simple() {
return (name) -> {
return name + "::";
};
}
static CacheKeyPrefix prefixed(String prefix) {
Assert.notNull(prefix, "Prefix must not be null!");
return (name) -> {
return prefix + name + "::";
};
}
}
该接口与成员变量org.springframework.data.redis.cache.RedisCacheConfiguration#keyPrefix对应,
可在RedisCache的默认配置确认其值:
public static RedisCacheConfiguration defaultCacheConfig(@Nullable ClassLoader classLoader) {
DefaultFormattingConversionService conversionService = new DefaultFormattingConversionService();
registerDefaultConverters(conversionService);
return new RedisCacheConfiguration(Duration.ZERO, true, true, CacheKeyPrefix.simple(), SerializationPair.fromSerializer(RedisSerializer.string()), SerializationPair.fromSerializer(RedisSerializer.java(classLoader)), conversionService);
}
因此,我们只需关注方法CacheKeyPrefix.simple()即可。
即配置属性usePrefix为True,那么this.createCacheKey("*") 返回的值为cacheName::*。
回到第二部分开头,继续看org.springframework.data.redis.cache.RedisCacheWriter#clean这个属于CacheWriter类的方法做了什么:
byte[][] keys = (byte[][])((Set)Optional.ofNullable(connection.keys(pattern)).orElse(Collections.emptySet())).toArray(new byte[0][]);
if (keys.length > 0) {
this.statistics.incDeletesBy(name, keys.length);
connection.del(keys);
}
Pattern作为参数被传入connection.keys方法并执行。
而这个属于org.springframework.data.redis.connection.RedisKeyCommand类的方法keys。虽然笔者没有进一步跟踪下去,但是不难想到该指令对应Redis服务器中的同名指令。
其能够根据输入的Pattern获取相应的键值集合。再然后被del指令移除,Clean方法的逻辑到此分析完成。
3. 实验(读取Redis缓存配置属性&执行Redis缓存的clean 指令)
目的①.确认RedisCacheConfig中的usePrefix属性的默认值。
目的②.测试RedisCache.clean()命令的效果。
编写如下代码/配置文件:
pom.xml
spring:
resources:
static-locations: classpath:/META-INF/resources/,classpath:/resources/,classpath:/static/,classpath:/public/,file:${web.upload-path}
cache:
cache-names: c1
redis:
time-to-live: 1800s
redis:
database: 0
host: 192.168.59.128
port: 6379
password: .....
jedis:
pool:
max-active: 8
max-idle: 8
max-wait-millis: -1
min-idle: 0
测试类
@SpringBootTest
class DemoApplicationTests {
// 测试缓存写入功能的类
@Autowired
BookDao bookDao;
@Autowired
RedisCacheManager redisCacheManager;
@Test
void contextLoads() {
//调用相应的方法测试缓存写入....
RedisCache redisCache = (RedisCache) redisCacheManager.getCache("c1");
RedisCacheConfiguration redisConfiguration =redisCache.getCacheConfiguration();
System.out.println(redisConfiguration.usePrefix());
redisCache.clear();
}
}
测试结果表明:(1)RedisCache默认配置usePrefix为True,即使在配置文件中没有为Cashe配置前缀名;(2) redisCache.clear()命令被执行后,该缓存存入的键值对均被删除,而其他的键值对不受影响。
同时对源代码的分析表明:无论usePrefix是否为True,当执行clean方法时,均采用keys *方法来获取需要删除的键值对,而不会建立Zset来维护需要删除的键值对。
4. 回顾(注解@FunctionalInterface及函数式接口)
RedisCacheConfiguration类的默认配置中,方法CacheKeyPrefix.simple()的返回值被赋值给接口CacheKeyPrefix。
CacheKeyPrefix.simple()方法所声明的返回类型确实是接口CacheKeyPrefix,但是从源代码来看该方法更像是直接返回了一个lambda表达式。
为什么可以这么做呢?笔者认为这可能与函数式接口有关。函数式接口具备以下相关性质:
1.@FunctionalInterface只能标记“有且仅有一个抽象方法”的接口,但从上文的示例来看,该接口可以包含多个静态方法。
2. 具有一个抽象方法的接口便是一个功能接口,即使我们没有为其添加 @FunctionalInterface
注释。
3. 函数式接口可以被隐式转换为 lambda 表达式,也可以使用Lambda表达式来表示该接口的一个实现。
第三条足以解释清为什么上面的写法是正确的。需要实例化一个函数式接口时,同样可以构建一个lambda表达式来对其进行赋值。