Java Spring Boot+Redis缓存: usePrefix配置分析

目录

0. 引言

1. 组件版本

2. 源码分析

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表达式来对其进行赋值。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值