SpringBoot配置多CacheManager

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

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

Generic

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

EhCache 2.x

Hazelcast

Infinispan

Couchbase

Redis

Caffeine

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();
}
}

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
使用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;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
​ setCacheManager(this.beanFactory.getBean(CacheManager.class));,默认是从容器中拿到CacheManager对象,这就会出现一个问题,当配置多个CacheManager实例bean,并暴露给容器后,会出现Bean装载的错误,因为getBean,默认只会返回一个对象,当出现了两个Bean实例就会报错,找不到Bean对象。

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

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

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

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

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>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
我选择使用内置的H2+JPA作为数据持久层。因为这样比较方便。。不用连接MYSQL。

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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
编写配置类

@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;
}
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
以上就是具体配置信息,需要主要的是别忘记@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);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
现在,就可以使用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());
1
2
3
4
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
1
2
3
4
5
6
7
​ 可以看到,两次查询,都只是用了一次select语句,还出现了一次redis连接操作,证明上述的配置是生效的。

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

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。

原文链接:https://blog.csdn.net/qq_26440803/article/details/90145543

posted @ 2024-06-20 17:30  枫树湾河桥  阅读(219)  评论(0编辑  收藏  举报
Live2D