六、Cache
集成Spring Cache
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
若要使用Spring自带的内存缓存管理器,需要添加:
spring.cache.type = Simple
*Simple: 基于ConcurrentHashMap实现的缓存,适合单体应用或者开发环境使用
*none: 关闭缓存,比如开发阶段为为了确保功能正确,可以先禁用缓存
*redis: 使用Redis作为缓存,还需要在pom中增加Redis依赖//Redis实现一二级缓存
*Generic: 用户自定义缓存实现,用户需要实现一个org.springframework.cache.CacheManager的实现
其他还有JCache、EhCache 2.x、Hazelcast
6.1.注释驱动缓存
*需要使用注解@EnableCaching打开缓存功能
@Cacheable: 作用在方法上,触发缓存读取操作
@CacheEvict: 作用在方法上,触发缓存失效操作
@CachePut: 作用在方法上,触发缓存更新操作
@Cache: 作用在方法上,综合上面的各种操作,在有些场景下,调用业务会触发多种缓存操作
@CacheConfig: 在类上设置当前缓存的一些公共设置
6.1.1.@Cacheable
注解声明了方法的结果是可缓存的,如果缓存还在,则目标方法不会被调用,直接取缓存。可以为方法声明多个缓存,如果至少有一个缓存有缓存想项,则其缓存项将被返回:
@Cacheable({“Menu”,”menuExt”})
public Menu findMenu(Long menuId){…}
注意:对于不同的缓存实现,最好将缓存对象实现序列化Serializable接口,这样可以无缝切换到分布式缓存系统
6.1.2.Key生成器
(1).缓存的Key非常重要,Spring使用了KeyGenerator类来根据方法参数生成Key
*如果只有一个参数,那么这个参数就是Key
*如果没有参数,则返回SimpleKey.EMPTY
*如果有多个Key,则返回包含多个参数的SimpleKey
@Cacheable(“user_function”)
public boolean canAcessFunction(Long userId,Long orgId,String functionCode){
}
(2).使用ApEL表达式来指定Key比自定义KeyGenerator更简单
@Cacheable(cacheNames=”org”,key=”#orgId”)
public Org findOrg(Long orgId , boolean checkCrmSystem){…}
@Cacheable(cacheNames=”org” , key=”#user.orgId”)
public Org findOrg(User user) {…}
6.1.3.@CachePut
注解CachePut总是会执行方法主体,并且使用返回的结果更新缓存,其他同Cacheable
@CachePut(cacheNames=”org” , key=”#data.id”)
public Org updateOrg(Org data) { … }
6.1.4.@CacheEvict
注解CacheEvict用于删除缓存项或清空缓存,CacheEvict可以指定多个缓存名字来清空多个缓存。
*清除单个缓存
@CacheEvict(cacheNames=”user”,key=”#id”)
public void updateUser(Long id , int status) { … }
注意:CacheEvict只能清除不能加载,只有想用的Cacheable方法被调用后才会加载最新缓存项
*清除全部缓存(重新加载后,会清空config缓存)
@CacheEvict(cacheNames=”config”,allEntries=true)
public void loadConfig() { … }
6.1.5.@Caching
注解Caching可以混合以上各种注解,可以在Caching标签中混合@Cacheable、@CachePut、@CacheEvict
@Caching(evict = {@CacheEvict(cacheNames=”user”,key=”#user.id”),
@CacheEvict(cacheNames=”userExt”,key=”#ext.id”)})
public void updateUser(User user,UserExt ext) { … }
6.1.6.@CacheConfig
注解CacheConfig作用于类上,可为此类的方法的缓存注解提供默认值(cacheNames)
6.2使用Redis Cache
6.2.1.Redis缓存原理
@Configuration
@AutoConfigureAfter(RedisAutoConfiguration.class) //确保Redis配置成功
@ConditionalOnBean(RedisTemplate.class) //使用RedisTemplate来操作缓存
//生效的条件是系统没有实现CacheMansger
@ConditionalOnMissingBean(CacheManager.class)
//注解Conditional需要联合一个CacheCondition进一步判断是否执行当前匹配类
//CacheCondition实现了Condition接口,该接口返回boolean值,用来判断是否启用该配置, CacheCondition用于判断application.properties中是否配置了spring.cache.type,取出其对应的值与CacheCondition所在的配置类进行比较,若一致,返回match
@Conditional(CacheCondition.class)
class RedisCacheConfiguration {
……
}
Cache接口包含以下方法:
*ValueWrapper get(Object key): 根据Key获取缓存对象, ValueWrapper包含了get方法用于获取缓存对象
*void put(Object key,Object value): 设置缓存
*void evict(Object key): 根据Key值清除缓存
*void clear(): 清除所有缓存
6.3.实现Redis两级缓存
当缓存发生变化时,注解@CachePut和@CacheEvict会触发RedisCache的put(Object Key,Object Value)和evict(Object Key)操作,两级缓存需要同时更新ConcurrentHashMap和Redis缓存,而且需要通过Redis的Pub发出通知消息,其他Spring Boot应用通过Sub来接收消息,同步更新Spring Boot应用自身的一级缓存
6.3.1.实现TwoLevelCacheManager
class TwoLevelCacheManager extends RedisCacheManager {
RedisTemplate redisTemplate;
public TwoLevelCacheManager(RedisTemplate redisTemplate,RedisCacheWriter cacheWriter, RedisCacheConfiguration defaultCacheConfiguration) {
super(cacheWriter,defaultCacheConfiguration);
this.redisTemplate = redisTemplate;
}
//使用RedisAndLocalCache代替Spring Boot自带的RedisCache
@Override
protected Cache decorateCache(Cache cache) {
return new RedisAndLocalCache(this, (RedisCache) cache);
}
//通过其他分布式节点,缓存改变
public void publishMessage(String cacheName) {
this.redisTemplate.convertAndSend(topicName, cacheName);
}
// 接受一个消息清空本地缓存
public void receiver(String name) {
RedisAndLocalCache cache = ((RedisAndLocalCache) this.getCache(name));
if(cache!=null){
cache.clearLocal();
}
}
}
在Spring Cache中,在缓存管理器中创建好每个缓存后,都会调用decorateCache方法,这样缓存管理器子类就有机会实现自己的扩展
6.3.2.创建RedisAndLocalCanch
RedisAndLocalCanch时系统的核心,它实现了Cache接口、类
class RedisAndLocalCache implements Cache {
// 本地缓存提供
ConcurrentHashMap<Object, Object> local = new ConcurrentHashMap<Object, Object>();
RedisCache redisCache;
TwoLevelCacheManager cacheManager;
public RedisAndLocalCache(TwoLevelCacheManager cacheManager, RedisCache redisCache) {
this.redisCache = redisCache;
this.cacheManager = cacheManager;
}
@Override
public String getName() {
return redisCache.getName();
}
@Override
public Object getNativeCache() {
return redisCache.getNativeCache();
}
关键的get和put方法如下:
@Override
public ValueWrapper get(Object key) {
ValueWrapper wrapper = (ValueWrapper) local.get(key);
if (wrapper != null) {
return wrapper;
} else {
// 二级缓存取
wrapper = redisCache.get(key);
if (wrapper != null) {
local.put(key, wrapper);
}
return wrapper;
}
}
@Override
public <T> T get(Object key, Class<T> type) {
return redisCache.get(key, type);
}
@Override
public <T> T get(Object key, Callable<T> valueLoader) {
return redisCache.get(key, valueLoader);
}
@Override
public void put(Object key, Object value) {
System.out.println(value.getClass().getClassLoader());
redisCache.put(key, value);
clearOtherJVM();
}
@Override
public ValueWrapper putIfAbsent(Object key, Object value) {
ValueWrapper v = redisCache.putIfAbsent(key, value);
clearOtherJVM();
return v;
}
@Override
public void evict(Object key) {
redisCache.evict(key);
clearOtherJVM();
}
@Override
public void clear() {
redisCache.clear();
}
public void clearLocal() {
this.local.clear();
}
protected void clearOtherJVM() {
cacheManager.publishMessage(redisCache.getName());
}
}
}
变量local代表了一个简单地缓存实现,使用了ConcurrentHashMap
get方法有如下逻辑实现:
*通过Key从本地取出ValueWrapper
*如果ValueWrapper存在,则直接返回
*如果ValueWrapper不存在,则调用父类RedisCache取得缓存项
*如果缓存项为空,则说明暂时无此项,直接返回空,等待@Cacheable调用业务方法获取缓存项
Put方法的实现逻辑如下:
*先调用redisCache,更新二级缓存
*调用clearOtherJVM方法,通知其他节点缓存更新
*其他节点(包括本节点)的TwoLevelCacheManager收到消息后,会调用receiver方法从而实现一级缓存
6.3.3.缓存同步说明
实现Redis的Pub/Sub模式
// 定义一个redis 的频道,默认叫cache,用于pub/sub
@Value("${springext.cache.redis.topic:cache}")
String topicName;
// Redis message,参考Redis一章
@Bean
RedisMessageListenerContainer container(RedisConnectionFactory connectionFactory,
MessageListenerAdapter listenerAdapter) {
RedisMessageListenerContainer container = new RedisMessageListenerContainer();
container.setConnectionFactory(connectionFactory);
container.addMessageListener(listenerAdapter, new PatternTopic(topicName));
return container;
}
需要在配置文件中配置springext.cache.redis.topic 指定一个频道名字,如果没有配置,默认的频道名字是cache
配置监听器
@Bean
MessageListenerAdapter listenerAdapter(final TwoLevelCacheManager cacheManager) {
return new MessageListenerAdapter(new MessageListener() {
public void onMessage(Message message, byte[] pattern) {
byte[] bs = message.getChannel();
try {
// Sub 一个消息,通知缓存管理器
String type = new String(bs, "UTF-8");
String cacheName = new String(message.getBody(),"UTF-8");
cacheManager.receiver(cacheName);
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
// 不可能出错,忽略
}
}
});
}
6.3.4.将代码组合在一起
@Configuration
public class CacheConfig {
@Bean
public TwoLevelCacheManager cacheManager(StringRedisTemplate redisTemplate) {
RedisCacheWriter writer = RedisCacheWriter.lockingRedisCacheWriter(redisTemplate.getConnectionFactory());
SerializationPair pair = SerializationPair.fromSerializer(new JdkSerializationRedisSerializer(this.getClass().getClassLoader()));
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig().serializeValuesWith(pair);
TwoLevelCacheManager cacheManager = new TwoLevelCacheManager(redisTemplate,writer,config);
return cacheManager;
}