SpringBoot配置多CacheManager

SpringCache配置多CacheManager

背景

​ Spring为了减少数据的执行次数(重点在数据库查询方面), 在其内部使用aspectJ技术,为执行操作的结果集做了一层缓存的抽象。这极大的提升了应用程序的性能。由于其切面注入的特性,所以不会对我们的程序造成任何的影响。对于一些实时性要求不那么高的业务数据,我们可以在Service上进行一些缓存的操作。这样就可以减少访问数据库的频率。

默认缓存的使用顺序

​ 在Spring内部,缓存的实现,依赖org.springframework.cache.Cacheorg.springframework.cache.CacheManager共同协作,它们只是定义了一种规范接口,实际的存储规则,需要用户自己定义,当没有提供用户自定义Bean对象,SpringBoot会自动执行以下的检测顺序:

  1. Generic

  2. JCache (JSR-107) (EhCache 3, Hazelcast, Infinispan, and others)

  3. EhCache 2.x

  4. Hazelcast

  5. Infinispan

  6. Couchbase

  7. Redis

  8. Caffeine

  9. Simple

    具体的用法请参考:Spring Caching

Cache

cache接口的作用是Spring提供的一种抽象操作规范,里面包含的crud操作

CacheManager

cacheManager接口的作用是用来获取Cache,类似一种对象工厂,所有的Cache,必须依赖与CacheManager来获取。

在Spring内部提供了三个默认的实现:

  • SimpleCacheManager 简单的集合实现
  • ConcurrentMapCacheManager 内部使用ConcurrentHashMap作为缓存容器
  • NoOpCacheManager 一个空的实现,不会保存任何记录。

当然其他的依赖需要我们引入三方的Jar,以及一些自定义的配置。

下面看下,执行切面的具体实现:

public class CacheInterceptor extends CacheAspectSupport implements MethodInterceptor, Serializable {

   @Override
   @Nullable
   public Object invoke(final MethodInvocation invocation) throws Throwable {
      Method method = invocation.getMethod();

      CacheOperationInvoker aopAllianceInvoker = () -> {
         try {
            return invocation.proceed();
         }
         catch (Throwable ex) {
            throw new CacheOperationInvoker.ThrowableWrapper(ex);
         }
      };

      try {
         return execute(aopAllianceInvoker, invocation.getThis(), method, invocation.getArguments());
      }
      catch (CacheOperationInvoker.ThrowableWrapper th) {
         throw th.getOriginal();
      }
   }

}

使用AOP的方式,将拦截添加缓存注解的方法,然后会调用父类的方法去执行:

execute方法中,会去寻找缓存的操作源CacheOperationSource,操作源中包含一个CacheOperation的集合。CacheOperation代表着@Cachable上注解信息的的实体类。然后我们可以用过cacheManager属性去寻找对应的cacheManager实现,获取Cache来完成缓存操作。

实现多CacheManager

​ 前面分析了,缓存的大致实现过程,我们就可以使用cacheManager去有选择性的选择我们需要使用的缓存实现。

​ 但是这里有个注意的点,我们需要给CacheManager一个默认的实现,这是由于Spring容器初始化机制造成的。CacheAspectSupport抽象实现了SmartInitializingSingleton接口,这个接口在Spring容器中,是单例对象的回调接口,当所有的单例对象实例化完毕,就会调用此方法。

​ 我们观察下CacheAspectSupport的逻辑:

public void afterSingletonsInstantiated() {
   if (getCacheResolver() == null) {
      // Lazily initialize cache resolver via default cache manager...
      Assert.state(this.beanFactory != null, "CacheResolver or BeanFactory must be set on cache aspect");
      try {
         setCacheManager(this.beanFactory.getBean(CacheManager.class));
      }
      catch (NoUniqueBeanDefinitionException ex) {
         throw new IllegalStateException("No CacheResolver specified, and no unique bean of type " +
               "CacheManager found. Mark one as primary or declare a specific CacheManager to use.");
      }
      catch (NoSuchBeanDefinitionException ex) {
         throw new IllegalStateException("No CacheResolver specified, and no bean of type CacheManager found. " +
               "Register a CacheManager bean or remove the @EnableCaching annotation from your configuration.");
      }
   }
   this.initialized = true;
}

setCacheManager(this.beanFactory.getBean(CacheManager.class));,默认是从容器中拿到CacheManager对象,这就会出现一个问题,当配置多个CacheManager实例bean,并暴露给容器后,会出现Bean装载的错误,因为getBean,默认只会返回一个对象,当出现了两个Bean实例就会报错,找不到Bean对象。

​ 所以,当对于自定义的多个Bean,我们需要指定一个打上@Primary注解,这样就可以解决冲突。

具体实现

​ 接下来,贴出具体示例:

我们选择ehcache与redis作为多种缓存源。

以下示例全部基于2.1.4.RELEASE版本,不同版本的代码差异较大

  1. pom文件

    <dependency>
                <groupId>net.sf.ehcache</groupId>
                <artifactId>ehcache</artifactId>
            </dependency>
    
            <dependency>
                <groupId>com.google.guava</groupId>
                <artifactId>guava</artifactId>
                <version>27.1-jre</version>
            </dependency>
      <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-data-redis</artifactId>
            </dependency>
    <dependency>
                <groupId>org.apache.commons</groupId>
                <artifactId>commons-pool2</artifactId>
            </dependency>
      <dependency>
                <groupId>com.h2database</groupId>
                <artifactId>h2</artifactId>
                <scope>runtime</scope>
            </dependency>
         <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-data-jpa</artifactId>
            </dependency>
    

    我选择使用内置的H2+JPA作为数据持久层。因为这样比较方便。。不用连接MYSQL。

  2. application.yml配置

    spring:
      application:
        name: authority-management
      datasource:
        driver-class-name: org.h2.Driver
        url: jdbc:h2:~/test
        password:
        username: sa
      h2:
        console:
          enabled: true
      jpa:
        generate-ddl: true
        hibernate:
          ddl-auto: create-drop
        show-sql: true
      redis:
        host: XXXX
        port: 6379
        lettuce:
          pool:
            max-active: 8
            max-wait: 200
            max-idle: 8
            min-idle: 2
        timeout: 2000
      cache:
        ehcache:
          config: classpath:ehcache.xml
        type: EHCACHE
    
  3. 编写配置类

@Configuration
@EnableCaching
@EnableConfigurationProperties(CacheProperties.class)
public class CacheManagerConfiguration {
    private final CacheProperties cacheProperties;
    public CacheManagerConfiguration(CacheProperties cacheProperties)    {
        this.cacheProperties = cacheProperties;
    }
    public interface CacheManagerNames {
        String REDIS_CACHE_MANAGER = "redisCacheManager";
        String EHCACHE_CACHE_MANAGER = "ehCacheManager";
    }

    @Bean(name = CacheManagerNames.REDIS_CACHE_MANAGER)
    public RedisCacheManager redisCacheManager(RedisConnectionFactory factory) {
        Map<String, RedisCacheConfiguration> expires = ImmutableMap.<String, RedisCacheConfiguration>builder()
                .put("15", RedisCacheConfiguration.defaultCacheConfig().entryTtl(
                        Duration.ofMillis(15)
                ))
                .put("30", RedisCacheConfiguration.defaultCacheConfig().entryTtl(
                        Duration.ofMillis(30)
                ))
                .put("60", RedisCacheConfiguration.defaultCacheConfig().entryTtl(
                        Duration.ofMillis(60)
                ))
                .put("120", RedisCacheConfiguration.defaultCacheConfig().entryTtl(
                        Duration.ofMillis(120)
                ))
                .build();

        RedisCacheManager redisCacheManager = RedisCacheManager.RedisCacheManagerBuilder.fromConnectionFactory(factory)
                .withInitialCacheConfigurations(expires)
                .build();
        return redisCacheManager;
    }

    @Bean(name = CacheManagerNames.EHCACHE_CACHE_MANAGER)
    @Primary
    public EhCacheCacheManager ehCacheManager() {
        Resource resource = this.cacheProperties.getEhcache().getConfig();
        resource = this.cacheProperties.resolveConfigLocation(resource);
        EhCacheCacheManager ehCacheManager = new EhCacheCacheManager(
                EhCacheManagerUtils.buildCacheManager(resource)
        );
        ehCacheManager.afterPropertiesSet();
        return ehCacheManager;
    }
}

以上就是具体配置信息,需要主要的是别忘记@Primary

Service实现

   @Override
    @Cacheable(key = "#userId", cacheNames = CacheManagerConfiguration.CacheNames.CACHE_15MINS,
            cacheManager = CacheManagerConfiguration.CacheManagerNames.EHCACHE_CACHE_MANAGER)
    public User findUserAccordingToId(Long userId) {
        return userRepository.findById(userId).orElse(User.builder().build());
    }

    @Override
    @Cacheable(key = "#username", cacheNames = CacheManagerConfiguration.CacheNames.CACHE_15MINS,
            cacheManager = CacheManagerConfiguration.CacheManagerNames.REDIS_CACHE_MANAGER)
    public User findUserAccordingToUserName(String username) {
        return userRepository.findUserByUsername(username);
    }

现在,就可以使用cacheManager属性来选择缓存源,用户可以灵活配置。

除了在方法上,使用注解外,我们还可以直接指定到类上,下面的示例,表示该类的全部方法都使用Encache作为缓存源。

@CacheConfig(cacheManager = CacheManagerConfiguration.CacheManagerNames.EHCACHE_CACHE_MANAGER)
  1. 测试类+日志信息

     userService.findUserAccordingToId(save.getId());
    userService.findUserAccordingToId(save.getId());
    userService.findUserAccordingToUserName(save.getUsername());
    userService.findUserAccordingToUserName(save.getUsername());
    
    Hibernate: select user0_.id as id1_11_0_, user0_.email as email2_11_0_, user0_.image_url as image_ur3_11_0_, user0_.introduction as introduc4_11_0_, user0_.last_login_at as last_log5_11_0_, user0_.level as level6_11_0_, user0_.login_ip as login_ip7_11_0_, user0_.nickname as nickname8_11_0_, user0_.password as password9_11_0_, user0_.retry_login_count as retry_l10_11_0_, user0_.sex as sex11_11_0_, user0_.status as status12_11_0_, user0_.telephone as telepho13_11_0_, user0_.username as usernam14_11_0_ from user user0_ where user0_.id=?
    2019-05-12 20:43:06.486  INFO 12020 --- [           main] io.lettuce.core.EpollProvider            : Starting without optional epoll library
    2019-05-12 20:43:06.488  INFO 12020 --- [           main] io.lettuce.core.KqueueProvider           : Starting without optional kqueue library
    2019-05-12 20:43:06.807  INFO 12020 --- [           main] o.h.h.i.QueryTranslatorFactoryInitiator  : HHH000397: Using ASTQueryTranslatorFactory
    Hibernate: select user0_.id as id1_11_, user0_.email as email2_11_, user0_.image_url as image_ur3_11_, user0_.introduction as introduc4_11_, user0_.last_login_at as last_log5_11_, user0_.level as level6_11_, user0_.login_ip as login_ip7_11_, user0_.nickname as nickname8_11_, user0_.password as password9_11_, user0_.retry_login_count as retry_l10_11_, user0_.sex as sex11_11_, user0_.status as status12_11_, user0_.telephone as telepho13_11_, user0_.username as usernam14_11_ from user user0_ where user0_.username=?
    
    Process finished with exit code -1
    

    ​ 可以看到,两次查询,都只是用了一次select语句,还出现了一次redis连接操作,证明上述的配置是生效的。

注意:
  1. 我在此使用的lettuce作为redis客户端,使用连接池时,它依赖commons-pool2
  2. 测试时,当redis作为缓存时,发现有时候还是会去数据库查询两次,怀疑是配置的超时时间问题。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值