在SpringBoot中配置多个cache,实现多个cacheManager灵活切换

SpringBoot配置多个cache,实现多个cacheManager灵活切换

注:本文所用的springBoot版本号为1.5.6.6

目的&效果

在springBoot中同时配置了RedisCache和ehCache,当使用@Cacheable注解时,默认为redisCache,通过指定注解中"cacheManager"的属性值,达到任意切换cache的效果。

第一次尝试

由于@Cacheable注解是可以通过参数指定CacheManager或CacheResolver的,因此推断springCache本身支持配置多个CacheManager。

首先尝试直接配置RedisCacheManager和EhCacheCacheManager,结果启动报错:
在这里插入图片描述

定位到该行报错代码,原来是在CacheAspectSupport.afterSingletonsInstantiated方法,通过beanFactory获取CacheManager.class的bean时,由于配置了多个bean,抛出了一个NoUniqueBeanDefinitionException异常。
在SpringBoot中配置多个cache,实现多个cacheManager灵活切换_第1张图片

分析

理论上只要在其中一个CacheManager的bean上加上@primary注解就可以解决这个报错了。 但是这样解决看起来是不优雅的。

从代码中看,在CacheAspectSupport.afterSingletonsInstantiated方法中,会先通过getCacheResolver方法尝试获取类中的成员变量cacheResolver,如果没有获取到,才会通过getBean方法尝试获取默认的cacheManager并通过cacheManager创建一个cacheResolver。 也就是说,在此之前是有地方设置默认cacheManager的。

一路查看源代码,最终发现, springCache会在配置类ProxyCachingConfiguration通过@Bean实例化一个CacheInterceptor(CacheAspectSupport的子类),并给interceptor的cacheResolver、cacheManager等成员变量赋值:

	@Bean
	@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
	public CacheInterceptor cacheInterceptor() {
		CacheInterceptor interceptor = new CacheInterceptor();
		interceptor.setCacheOperationSources(cacheOperationSource());
		if (this.cacheResolver != null) {
			interceptor.setCacheResolver(this.cacheResolver);
		}
		else if (this.cacheManager != null) {
			interceptor.setCacheManager(this.cacheManager);
		}
		if (this.keyGenerator != null) {
			interceptor.setKeyGenerator(this.keyGenerator);
		}
		if (this.errorHandler != null) {
			interceptor.setErrorHandler(this.errorHandler);
		}
		return interceptor;
	}

而用来赋值的cacheManager等,是在ProxyCachingConfiguration的父类,抽象配置类AbstractCachingConfiguration里面获取到默认值的:

	@Autowired(required = false)
	void setConfigurers(Collection<CachingConfigurer> configurers) {
		if (CollectionUtils.isEmpty(configurers)) {
			return;
		}
		if (configurers.size() > 1) {
			throw new IllegalStateException(configurers.size() + " implementations of " +
					"CachingConfigurer were found when only 1 was expected. " +
					"Refactor the configuration such that CachingConfigurer is " +
					"implemented only once or not at all.");
		}
		CachingConfigurer configurer = configurers.iterator().next();
		useCachingConfigurer(configurer);
	}

	/**
	 * Extract the configuration from the nominated {@link CachingConfigurer}.
	 */
	protected void useCachingConfigurer(CachingConfigurer config) {
		this.cacheManager = config.cacheManager();
		this.cacheResolver = config.cacheResolver();
		this.keyGenerator = config.keyGenerator();
		this.errorHandler = config.errorHandler();
	}

因此,只需实现CachingConfigurer配置接口编写配置类,并重写其cacheManager方法提供默认的cacheManager就可以指定默认的cacheManager了。

最终方案

step0 首先确保maven中已经引入了相关jar包依赖,主要包括redis和ehcache的相关依赖。
        
        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-data-redisartifactId>
        dependency>
        <dependency>
            <groupId>net.sf.ehcachegroupId>
            <artifactId>ehcacheartifactId>
            <version>2.10.4version>
        dependency>
step1 在application.yml中添加redis的连接配置和ehcache的配置。
spring:
    redis:
      # Connection URL, will override host, port and password (user will be ignored), e.g. redis://user:[email protected]:6379
      url: redis://localhost:6379
      pool:
            maxActive: 8  # 连接池最大连接数(使用负值表示没有限制)
            maxWait: 800 # 连接池最大阻塞等待时间(使用负值表示没有限制)
            maxIdle: 8 # 连接池中的最大空闲连接
            minIdle: 2 # 连接池中的最小空闲连接
      timeout: 2000 # 连接或读取超时时长(毫秒)

    cache:
        type: ehcache
        ehcache:
          config: ehcache.xml
step2 在项目的resources目录下,添加ehcache.xml配置文件。

<ehcache>
    
    

    
    <defaultCache
            maxElementsInMemory="10000"
            eternal="false"
            overflowToDisk="true"
            timeToIdleSeconds="10"
            timeToLiveSeconds="20"
            diskPersistent="false"
            diskExpiryThreadIntervalSeconds="120"/>

    <cache name="cp_salary:cache:10m"
           maxElementsInMemory="100000"
           eternal="false"
           overflowToDisk="false"
           timeToIdleSeconds="10"
           timeToLiveSeconds="600"/>

    <cache name="cp_salary:cache:20m"
           maxElementsInMemory="100000"
           eternal="false"
           overflowToDisk="false"
           timeToIdleSeconds="10"
           timeToLiveSeconds="1200"/>

    <cache name="cp_salary:cache:30m"
           maxElementsInMemory="100000"
           eternal="false"
           overflowToDisk="false"
           timeToIdleSeconds="10"
           timeToLiveSeconds="1800"/>

ehcache>
step3 编写CacheManagerConfig类,在类里通过@Bean注解生成RedisCacheManager和EhCacheCacheManager的bean。参考代码如下。
package com.lianjia.sh.salary.payroll.config;

import com.alibaba.fastjson.support.spring.GenericFastJsonRedisSerializer;
import com.google.common.collect.ImmutableMap;
import org.springframework.boot.autoconfigure.cache.CacheManagerCustomizer;
import org.springframework.boot.autoconfigure.cache.CacheProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.cache.ehcache.EhCacheCacheManager;
import org.springframework.cache.ehcache.EhCacheManagerUtils;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.Resource;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.connection.jedis.JedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.StringRedisSerializer;

import java.util.Map;
import java.util.concurrent.TimeUnit;

/**
 * @author Stephen.Shi
 * @summary CacheManager配置
 * @since 2018/9/9
 */
@Configuration
@EnableCaching
@EnableConfigurationProperties(CacheProperties.class)
public class CacheManagerConfig {
    private final CacheProperties cacheProperties;

    CacheManagerConfig(CacheProperties cacheProperties) {
        this.cacheProperties = cacheProperties;
    }

    /**
     * cacheManager名字
     */
    public interface CacheManagerNames {
        /**
         * redis
         */
        String REDIS_CACHE_MANAGER = "redisCacheManager";

        /**
         * ehCache
         */
        String EHCACHE_CACHE_MAANGER = "ehCacheCacheManager";
    }

    /**
     * 缓存名,名称暗示了缓存时长 注意: 如果添加了新的缓存名,需要同时在下面的RedisCacheCustomizer#RedisCacheCustomizer里配置名称对应的缓存时长
     * ,时长为0代表永不过期;缓存名最好公司内部唯一,因为可能多个项目共用一个redis。
     *
     * @see RedisCacheCustomizer#customize(RedisCacheManager)
     */
    public interface CacheNames {
        /** 15分钟缓存组 */
        String CACHE_15MINS = "cp_salary:cache:15m";
        /** 30分钟缓存组 */
        String CACHE_30MINS = "cp_salary:cache:30m";
        /** 60分钟缓存组 */
        String CACHE_60MINS = "cp_salary:cache:60m";
        /** 180分钟缓存组 */
        String CACHE_180MINS = "cp_salary:cache:180m";
    }

    /**
     * ehcache缓存名
     */
    public interface EhCacheNames {
        String CACHE_10MINS = "cp_salary:cache:10m";

        String CACHE_20MINS = "cp_salary:cache:20m";

        String CACHE_30MINS = "cp_salary:cache:30m";
    }

    @Bean
    public RedisTemplate redisTemplate(JedisConnectionFactory jedisConnectionFactory) {
        RedisTemplate redisTemplate = new RedisTemplate();
        redisTemplate.setConnectionFactory(jedisConnectionFactory);
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        redisTemplate.setValueSerializer(new GenericFastJsonRedisSerializer());
        redisTemplate.afterPropertiesSet();
        return redisTemplate;
    }

    /**
     * 默认的redisCacheManager
     * @param redisTemplate 通过参数注入,这里没有手动给它做配置。在引入了redis的jar包,并且往
     * application.yml里添加了spring.redis的配置项,springboot的autoconfig会自动生成一个
     * redisTemplate的bean
     * @return
     */
    @Bean
    public RedisCacheManager redisCacheManager(RedisTemplate<Object, Object> redisTemplate) {
        RedisCacheManager cacheManager = new RedisCacheManager(redisTemplate);
        cacheManager.setUsePrefix(true);
        this.redisCacheManagerCustomizer().customize(cacheManager);
        return cacheManager;
    }

    /** cache的一些自定义配置 */
    @Bean
    public RedisCacheCustomizer redisCacheManagerCustomizer() {
        return new RedisCacheCustomizer();
    }

    private static class RedisCacheCustomizer
            implements CacheManagerCustomizer<RedisCacheManager> {
        /** CacheManager缓存自定义初始化比较早,尽量不要@autowired 其他spring 组件 */
        @Override
        public void customize(RedisCacheManager cacheManager) {
            // 自定义缓存名对应的过期时间
            Map<String, Long> expires = ImmutableMap.<String, Long>builder()
                    .put(CacheNames.CACHE_15MINS, TimeUnit.MINUTES.toSeconds(15))
                    .put(CacheNames.CACHE_30MINS, TimeUnit.MINUTES.toSeconds(30))
                    .put(CacheNames.CACHE_60MINS, TimeUnit.MINUTES.toSeconds(60))
                    .put(CacheNames.CACHE_180MINS, TimeUnit.MINUTES.toSeconds(180)).build();
            // spring cache是根据cache name查找缓存过期时长的,如果找不到,则使用默认值
            cacheManager.setDefaultExpiration(TimeUnit.MINUTES.toSeconds(30));
            cacheManager.setExpires(expires);
        }
    }

	/**
	* 创建ehCacheCacheManager
	*/
    @Bean
    public EhCacheCacheManager ehCacheCacheManager() {
        Resource location = this.cacheProperties
                .resolveConfigLocation(this.cacheProperties.getEhcache().getConfig());
        return new EhCacheCacheManager(EhCacheManagerUtils.buildCacheManager(location));
    }
}
step4 编写CacheConfig.java,配置默认的cacheManager
package com.lianjia.sh.salary.payroll.config;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.cache.Cache;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.CachingConfigurerSupport;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.cache.interceptor.CacheErrorHandler;
import org.springframework.cache.interceptor.SimpleCacheErrorHandler;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheManager;

/**
 * Spring cache的一些配置,建议组件相关配置都放在相应的configuration类中
 *
 * @author
 */
@Configuration
@ConditionalOnBean(RedisCacheManager.class)
public class CacheConfig extends CachingConfigurerSupport {
  @Autowired
  private RedisCacheManager redisCacheManager;

  /**
   * 重写这个方法,目的是用以提供默认的cacheManager
   * @author Stephen.Shi
   * @return
   */
  @Override
  public CacheManager cacheManager() {
    return redisCacheManager;
  }

  /** 如果cache出错, 我们会记录在日志里,方便排查,比如反序列化异常 */
  @Override
  public CacheErrorHandler errorHandler() {
    return new LoggingCacheErrorHandler();
  }


  /* non-public */ static class LoggingCacheErrorHandler extends SimpleCacheErrorHandler {
    private final Logger logger = LoggerFactory.getLogger(this.getClass());

    @Override
    public void handleCacheGetError(RuntimeException exception, Cache cache, Object key) {
      logger.error(String.format("cacheName:%s,cacheKey:%s",
          cache == null ? "unknown" : cache.getName(), key), exception);
      super.handleCacheGetError(exception, cache, key);
    }

    @Override
    public void handleCachePutError(RuntimeException exception, Cache cache, Object key,
        Object value) {
      logger.error(String.format("cacheName:%s,cacheKey:%s",
          cache == null ? "unknown" : cache.getName(), key), exception);
      super.handleCachePutError(exception, cache, key, value);
    }

    @Override
    public void handleCacheEvictError(RuntimeException exception, Cache cache, Object key) {
      logger.error(String.format("cacheName:%s,cacheKey:%s",
          cache == null ? "unknown" : cache.getName(), key), exception);
      super.handleCacheEvictError(exception, cache, key);
    }

    @Override
    public void handleCacheClearError(RuntimeException exception, Cache cache) {
      logger.error(String.format("cacheName:%s", cache == null ? "unknown" : cache.getName()),
          exception);
      super.handleCacheClearError(exception, cache);
    }
  }
}

step5 编写demo,查看效果
package com.lianjia.sh.salary.payroll.service;

import com.lianjia.sh.salary.payroll.config.CacheManagerConfig;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;

/**
 * @author shitiecheng
 * @version v1
 * @since 2018/7/31
 */
@Service
public class CacheDemoService {

    @Cacheable(key = "key", cacheManager = CacheManagerConfig.CacheManagerNames.EHCACHE_CACHE_MAANGER, cacheNames = CacheManagerConfig.EhCacheNames.CACHE_10MINS)
    public String demo(String key) {
        return "abc" + key;
    }

    @Cacheable(key = "key", cacheNames = CacheManagerConfig.CacheNames.CACHE_15MINS)
    public String demo2(String key) {
        return "abc" + key;
    }
}

结果:

你可能感兴趣的:(springboot)