spring cache动态获取redis指定namespace下的key

spring cache是一种可以通过注解或XML配置方式来实现缓存的组件。 在开发过程中很方便,如@Cacheable、@CacheEvict等。一般都是事先在方法上去加入注解,并且写死了cacheName,比如如下例子:

    @Cacheable(value = "oxx.applications", key = "#packageName")
    public Application fetchByPackageName(String packageName) {
        Application application = new Application();
        application.setPackageName(packageName);
        Application application1 = applicationMapper.selectOne(application);
        return application1;
    }

这样只要调个方法,都会去namespace为oxx.applications下去找key为oxx.applications:+packageName的redis键值(PS: 这里使用的是RedisCacheManager)redis存储结构如下:

spring cache动态获取redis指定namespace下的key_第1张图片
但是有一天生产环境出问题了,怀疑是缓存问题,于是想看下redis某个key的值,这时候只能通过找DBA了,一两次没所谓,但经常去找就得买可乐了。 于是想搞个后门(做一个接口,前端输入key去拿缓存)。
说干就干,以为很简单,不就是写个Controller吗,简单。。。 可是那么多的key,每一种key的namespace(cacheName)都不同,难道要写N个接口?

    @Cacheable(value = "oxx.applications1", key = "#packageName")
    public Application fetchByPackageName(String packageName) {
        return null;
    }
    
    @Cacheable(value = "oxx.applications2", key = "#key2")
    public Application fetchByKey2(String key2) {
        return null;
    }
    
    @Cacheable(value = "oxx.applications3", key = "#key3")
    public Application fetchByKey3(String key3) {
        return null;
    }
    ..........
    ..........
    ..........
    ..........
    ..........
    ..........

例如这样? 这肯定不行。

点开@Cacheable注解,里面有一个cacheResolver的东东,看字面意义是缓存解析的意思,想到能不能通过这个来做下文章,于是查看了Api文档发现可行。
NamedCacheResolver类实现了CacheResolver接口,里面包含了cacheNames集合,并且关联了CacheManager对象,所以是支持通过调用方法方式修改cahceName的。

思路:可以在调用获取缓存接口的时候传入cahceName和key,每次都去动态修改CacheManager的cacheNames属性,最后再去调用加了@Cacheable注解的方法。

代码如下:

package com.xxxxx.xxxx.xxx.xx.xx.cache;

import com.boot.service.cache.manager.CustomizerRedisCacheManager;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.cache.interceptor.NamedCacheResolver;
import org.springframework.context.annotation.Bean;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;

/**
 * @author 
 */
@ConditionalOnProperty(name = "spring.cache.type", havingValue = "redis")
@Service
public class DynamicRedisCache {

    @Cacheable(cacheResolver = "namedCacheResolver", key = "#key")
    public Object get(String key){
        // 只查不覆盖,所以return null,redis默认不会存为null的key
        return null;
    }

    @CacheEvict(cacheResolver = "namedCacheResolver", key = "#key")
    public void del(String key) {
    }

    @Bean
    public CustomizerRedisCacheManager redisCacheManager(RedisTemplate redisTemplate) {

        CustomizerRedisCacheManager redisCacheManager = new CustomizerRedisCacheManager(redisTemplate);
        redisCacheManager.setUsePrefix(true);

        return redisCacheManager;

    }

    @Bean
    public NamedCacheResolver namedCacheResolver(RedisCacheManager redisCacheManager){
        NamedCacheResolver namedCacheResolver = new NamedCacheResolver(redisCacheManager);
        namedCacheResolver.setCacheNames(redisCacheManager.getCacheNames());
        return namedCacheResolver;
    }
}

RedisCacheManager


import org.springframework.cache.Cache;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.core.RedisOperations;
import org.springframework.util.CollectionUtils;

import java.util.*;

/**
 * @Description RedisCacheManager 覆盖父类的loadCaches方法,父类每次会保留上一次的cacheName
 * @Author 
 * @Date 2019-07-04 09:34
 **/
public class CustomizerRedisCacheManager extends RedisCacheManager {
    private Set configuredCacheNames;

    public CustomizerRedisCacheManager(RedisOperations redisOperations) {
        super(redisOperations);
    }

    @Override
    public void setCacheNames(Collection cacheNames) {
        Set newCacheNames = CollectionUtils.isEmpty(cacheNames) ? Collections.emptySet() : new HashSet(cacheNames);
        this.configuredCacheNames = newCacheNames;
    }

    @Override
    protected Collection loadCaches() {
        Set caches = new LinkedHashSet(new ArrayList());
        Set cachesToLoad = new LinkedHashSet(this.configuredCacheNames);
        if (!CollectionUtils.isEmpty(cachesToLoad)) {
            Iterator var3 = cachesToLoad.iterator();

            while(var3.hasNext()) {
                String cacheName = (String)var3.next();
                caches.add(this.createCache(cacheName));
            }
        }

        return caches;
    }
}

RedisTemplate

 @Bean
 public RedisTemplate redisTemplate(RedisConnectionFactory redisConnectionFactory) {
        RedisTemplate template = new RedisTemplate<>();
        template.setConnectionFactory(redisConnectionFactory);
        template.setKeySerializer(new StringRedisSerializer());
        template.setHashKeySerializer(new StringRedisSerializer());
        template.afterPropertiesSet();
        return template;
    }

注意:@ConditionalOnProperty增加此注解是因为在单元测试中使用的spring.cache.type = simple,如果不加此注解单元测试会跑不过-_- 如果没有单元测试或者也是使用的redis则去掉即可。
redisCacheManager.setUsePrefix(true); – 意思是key包含前缀,一般都是包含前缀的,所以必须要。
CustomizerRedisCacheManager 是我自定义的类,继承了RedisCacheManager,因为每次请求都缓存了上一次的cacheName所以重写了父类的loadCaches,保证每次都是当前的cacheName。

有了NamedCacheResolver 和 CustomizerRedisCacheManager 两个实例,那么我们主要在调用缓存接口的时候动态去修改cacheName就能实现动态获取缓存了

import com.google.common.collect.Lists;
import com.service.cache.manager.CustomizerRedisCacheManager;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.interceptor.NamedCacheResolver;
import org.springframework.stereotype.Service;

/**
 * @author 
 */
@Service
public class CacheService {

    @Autowired(required = false)
    private DynamicRedisCache dynamicRedisCache;

    @Autowired(required = false)
    private CustomizerRedisCacheManager redisCacheManager;

    @Autowired(required = false)
    private NamedCacheResolver namedCacheResolver;

    /**
     * 动态获取spring cache cacheName 对应key的redis值
     * @param cacheName
     * @param key
     * @return
     */
    public Object getFromRedis(String cacheName, String key) {
        setCacheResolver(cacheName);
        return dynamicRedisCache.get(key);
    }

    /**
     * 删除redis指定namespace的键值
     * @param cacheName
     * @param key
     */
    public boolean delRedisCache(String cacheName, String key){
        setCacheResolver(cacheName);
        dynamicRedisCache.del(key);
        return true;
    }

    private void setCacheResolver(String cacheName){
        redisCacheManager.setCacheNames(Lists.newArrayList(cacheName));
        redisCacheManager.afterPropertiesSet();
        namedCacheResolver.setCacheNames(redisCacheManager.getCacheNames());
        namedCacheResolver.setCacheManager(redisCacheManager);
    }
}

Controller

    @Autowired
    private CacheService cacheService;

    @GetMapping("/getFromRedis")
    public CommonResponse getFromRedis(@Param("cacheName") String cacheName, @Param("cacheKey") String cacheKey) {
        return Response.ok("获取成功", cacheService.getFromRedis(cacheName, cacheKey));
    }
    
   @GetMapping("/del")
    public CommonResponse delRedisCache(@Param("cacheName") String cacheName, @Param("cacheKey") String cacheKey) {
        return Response.ok("删除指定缓存成功", cacheService.delRedisCache(cacheName, cacheKey));
    }

大功告成。
spring cache动态获取redis指定namespace下的key_第2张图片

spring cache动态获取redis指定namespace下的key_第3张图片

你可能感兴趣的:(总结,开发笔记)