Springboot实现缓存很简单,最多两处配置(Maven工程的pom.xml引入依赖包和系统配置文件application.yml定义参数)即可引入缓存,然后再使用注解,就可以很方便是开始使用缓存了。
这样是比较简单,书写代码也很高效。可是执行的代码高效吗,当然的不高效:
1、单机的redis,并发访问量有限吧?
2、集群redis,并发访问量还是有限吧?
3、redis的命中率、网络传输,都需要一定的损耗时间吧?
站在前人的肩膀上,才能更接近成功。参考了一些前人的资料,我目前整理出下面的两级缓存机制。
一、两级缓存数据流原理
1、Cacheable(读取数据流程)
读取数据,先从一级缓存中读取,一级缓存中有值则直接读取,没有则再读取二级缓存;二级缓存中有值则更新一级缓存返回值,没有则从数据库中获取数据,依次更新一二级缓存。
2、CachePut(数据变更流程)
3、CacheEvict(缓存删除流程)
4、利用redis的消息发布订阅模式,实现其它站点缓存同步
二、一级缓存引入
Spring Boot的本地缓存有好几种,我采用Caffeine。
Caffeine是一个基于Google开源的Guava设计理念的一个高性能内存缓存,使用java8开发,spring boot引入Caffeine后已经逐步废弃Guava的整合了。Caffeine源码及介绍地址:caffeine
caffeine提供了多种缓存填充策略、值回收策略,同时也包含了缓存命中次数等统计数据,对缓存的优化能够提供很大帮助
caffeine的介绍可以参考://www.jb51.net/article/134242.htm
这里简单说下caffeine基于时间的回收策略有以下几种:
- expireAfterAccess:访问后到期,从上次读或写发生后的过期时间
- expireAfterWrite:写入后到期,从上次写入发生之后的过期时间
- 自定义策略:到期时间由实现Expiry接口后单独计算
在工程的pom.xml中增加一下依赖
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
</dependency>
三、代码
1、增加属性文件CacheRedisCaffeineProperties.java
package com.demo.example.security.core.cache;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import org.springframework.boot.context.properties.ConfigurationProperties;
@ConfigurationProperties(prefix = "spring.cache.multi")
public class CacheRedisCaffeineProperties {
/**
* 本机的ip地址
*/
private String localIp="127.0.0.1";
private Set<String> cacheNames = new HashSet<>();
/** 是否存储空值,默认true,防止缓存穿透 */
private boolean cacheNullValues = true;
/** 是否动态根据cacheName创建Cache的实现,默认true */
private boolean dynamic = true;
/** 缓存key的前缀 */
private String cachePrefix;
private Redis redis = new Redis();
private Caffeine caffeine = new Caffeine();
public class Redis {
/** 全局过期时间,单位毫秒,默认不过期 */
private long defaultExpiration = 0;
/** 每个cacheName的过期时间,单位毫秒,优先级比defaultExpiration高 */
private Map<String, Long> expires = new HashMap<>();
/** 缓存更新时通知其他节点的topic名称 */
private String topic = "cache:redis:caffeine:topic";
public long getDefaultExpiration() {
return defaultExpiration;
}
public void setDefaultExpiration(long defaultExpiration) {
this.defaultExpiration = defaultExpiration;
}
public Map<String, Long> getExpires() {
return expires;
}
public void setExpires(Map<String, Long> expires) {
this.expires = expires;
}
public String getTopic() {
return topic;
}
public void setTopic(String topic) {
this.topic = topic;
}
}
public class Caffeine {
/** 访问后过期时间,单位毫秒 */
private long expireAfterAccess;
/** 写入后过期时间,单位毫秒 */
private long expireAfterWrite;
/** 写入后刷新时间,单位毫秒 */
private long refreshAfterWrite;
/** 初始化大小 */
private int initialCapacity;
/** 最大缓存对象个数,超过此数量时之前放入的缓存将失效 */
private long maximumSize;
/** 由于权重需要缓存对象来提供,对于使用spring cache这种场景不是很适合,所以暂不支持配置 */
// private long maximumWeight;
public long getExpireAfterAccess() {
return expireAfterAccess;
}
public void setExpireAfterAccess(long expireAfterAccess) {
this.expireAfterAccess = expireAfterAccess;
}
public long getExpireAfterWrite() {
return expireAfterWrite;
}
public void setExpireAfterWrite(long expireAfterWrite) {
this.expireAfterWrite = expireAfterWrite;
}
public long getRefreshAfterWrite() {
return refreshAfterWrite;
}
public void setRefreshAfterWrite(long refreshAfterWrite) {
this.refreshAfterWrite = refreshAfterWrite;
}
public int getInitialCapacity() {
return initialCapacity;
}
public void setInitialCapacity(int initialCapacity) {
this.initialCapacity = initialCapacity;
}
public long getMaximumSize() {
return maximumSize;
}
public void setMaximumSize(long maximumSize) {
this.maximumSize = maximumSize;
}
}
public String getLocalIp() {
return localIp;
}
public void setLocalIp(String localIp) {
this.localIp = localIp;
}
public Set<String> getCacheNames() {
return cacheNames;
}
public void setCacheNames(Set<String> cacheNames) {
this.cacheNames = cacheNames;
}
public boolean isCacheNullValues() {
return this.cacheNullValues;
}
public void setCacheNullValues(boolean cacheNullValues) {
this.cacheNullValues = cacheNullValues;
}
public boolean isDynamic() {
return dynamic;
}
public void setDynamic(boolean dynamic) {
this.dynamic = dynamic;
}
public String getCachePrefix() {
return cachePrefix;
}
public void setCachePrefix(String cachePrefix) {
this.cachePrefix = cachePrefix;
}
public Redis getRedis() {
return redis;
}
public void setRedis(Redis redis) {
this.redis = redis;
}
public Caffeine getCaffeine() {
return caffeine;
}
public void setCaffeine(Caffeine caffeine) {
this.caffeine = caffeine;
}
}
localIp表示本机的ip地址,默认值为127.0.0.1。系统启动,当其值为空或者为127.0.0.1时,系统通过InetAddress获取本机的ip地址.
redis参数的expires,可以设置不同的缓存项不同的过期时间。
2、redis监控的消息体CacheMessage.java
package com.demo.example.security.core.cache;
import java.io.Serializable;
public class CacheMessage implements Serializable {
/**序列化id.*/
private static final long serialVersionUID = 5987219310442078193L;
/**
* 发送消息的ip
*/
private String ip;
/**
* cache组名
*/
private String cacheName;
/**
* cache键值
*/
private Object key;
public CacheMessage(String ip, String cacheName, Object key) {
super();
this.ip = ip;
this.cacheName = cacheName;
this.key = key;
}
public String getIp() {
return ip;
}
public void setIp(String ip) {
this.ip = ip;
}
public String getCacheName() {
return cacheName;
}
public void setCacheName(String cacheName) {
this.cacheName = cacheName;
}
public Object getKey() {
return key;
}
public void setKey(Object key) {
this.key = key;
}
@Override
public String toString() {
return "CacheMessage [ip=" + ip + ", cacheName=" + cacheName + ", key=" + key + "]";
}
}
发送消息的ip是为了识别消息是谁发送的。
3、消息监听者。当接受消息时,判断是否本机发送的,非本机发送的调用缓存管理器的清除本地缓存方法
/**
*
*/
package com.demo.example.security.core.cache;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.data.redis.connection.Message;
import org.springframework.data.redis.connection.MessageListener;
import org.springframework.data.redis.core.RedisTemplate;
public class CacheMessageListener implements MessageListener {
private final Logger logger = LoggerFactory.getLogger(CacheMessageListener.class);
/**
* 缓存配置参数.
*/
private CacheRedisCaffeineProperties cacheRedisCaffeineProperties;
/**
* redis连接模板.
*/
private RedisTemplate<Object, Object> redisTemplate;
/**
* 缓存管理器.
*/
private RedisCaffeineCacheManager redisCaffeineCacheManager;
/**
* 构造函数,注入
* @param redisTemplate redis连接模板
* @param redisCaffeineCacheManager 缓存管理器
* @param cacheRedisCaffeineProperties 缓存配置参数
*/
public CacheMessageListener(RedisTemplate<Object, Object> redisTemplate,
RedisCaffeineCacheManager redisCaffeineCacheManager,
CacheRedisCaffeineProperties cacheRedisCaffeineProperties) {
super();
this.redisTemplate = redisTemplate;
this.redisCaffeineCacheManager = redisCaffeineCacheManager;
this.cacheRedisCaffeineProperties=cacheRedisCaffeineProperties;
}
/**
* 监控消息,接受到消息后的处理
*/
@Override
public void onMessage(Message message, byte[] pattern) {
CacheMessage cacheMessage = (CacheMessage) redisTemplate.getValueSerializer().deserialize(message.getBody());
logger.debug("recevice a redis topic message, clear local cache, {}",cacheMessage);
if (cacheMessage.getIp().equals(cacheRedisCaffeineProperties.getLocalIp())) {
logger.error("自己发送的,不清理,{}",cacheMessage);
} else {
//调用缓存管理器的清除本地缓存
redisCaffeineCacheManager.clearLocal(cacheMessage);
}
}
}
4、本地缓存对象RedisCaffeineCache.java
/**
*
*/
package com.demo.example.security.core.cache;
import java.lang.reflect.Constructor;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.Callable;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.cache.support.AbstractValueAdaptingCache;
import org.springframework.data.redis.core.RedisTemplate;
import com.github.benmanes.caffeine.cache.Cache;
public class RedisCaffeineCache extends AbstractValueAdaptingCache {
private final Logger logger = LoggerFactory.getLogger(RedisCaffeineCache.class);
private CacheRedisCaffeineProperties cacheRedisCaffeineProperties;
private String name;
private RedisTemplate<Object, Object> redisTemplate;
private Cache<Object, Object> caffeineCache;
private String cachePrefix;
private long defaultExpiration = 0;
private Map<String, Long> expires;
private String topic = "cache:redis:caffeine:topic";
protected RedisCaffeineCache(boolean allowNullValues) {
super(allowNullValues);
}
public RedisCaffeineCache(String name, RedisTemplate<Object, Object> redisTemplate,
Cache<Object, Object> caffeineCache, CacheRedisCaffeineProperties cacheRedisCaffeineProperties) {
super(cacheRedisCaffeineProperties.isCacheNullValues());
this.cacheRedisCaffeineProperties=cacheRedisCaffeineProperties;
this.name = name;
this.redisTemplate = redisTemplate;
this.caffeineCache = caffeineCache;
this.cachePrefix = cacheRedisCaffeineProperties.getCachePrefix();
this.defaultExpiration = cacheRedisCaffeineProperties.getRedis().getDefaultExpiration();
this.expires = cacheRedisCaffeineProperties.getRedis().getExpires();
this.topic = cacheRedisCaffeineProperties.getRedis().getTopic();
logger.info("topic={}",topic);
logger.info("defaultExpiration={}",defaultExpiration);
}
@Override
public String getName() {
logger.debug("String getName() [name={}]",this.name);
return this.name;
}
@Override
public Object getNativeCache() {
logger.debug("Object getNativeCache() [name= {}'",this.name);
return this;
}
@SuppressWarnings("unchecked")
@Override
public <T> T get(Object key, Callable<T> valueLoader) {
logger.debug("<T> T get(Object key, Callable<T> valueLoader), the cache'name is {},the key is : {}", this.name,key);
Object value = lookup(key);
if (value != null) {
return (T) value;
}
ReentrantLock lock = new ReentrantLock();
try {
lock.lock();
value = lookup(key);
if (value != null) {
return (T) value;
}
value = valueLoader.call();
Object storeValue = toStoreValue(valueLoader.call());
put(key, storeValue);
return (T) value;
} catch (Exception e) {
try {
Class<?> c = Class.forName("org.springframework.cache.Cache$ValueRetrievalException");
Constructor<?> constructor = c.getConstructor(Object.class, Callable.class, Throwable.class);
RuntimeException exception = (RuntimeException) constructor.newInstance(key, valueLoader, e.getCause());
throw exception;
} catch (Exception e1) {
throw new IllegalStateException(e1);
}
} finally {
lock.unlock();
}
}
@Override
public void put(Object key, Object value) {
logger.debug("put(Object key, Object value), the cache'name is {},the key is : {}", this.name,key);
if (!super.isAllowNullValues() && value == null) {
this.evict(key);
return;
}
long expire = getExpire();
if (expire > 0) {
redisTemplate.opsForValue().set(getKey(key), toStoreValue(value), expire, TimeUnit.MILLISECONDS);
} else {
redisTemplate.opsForValue().set(getKey(key), toStoreValue(value));
}
pushMessage(key);
caffeineCache.put(key, value);
}
@Override
public ValueWrapper putIfAbsent(Object key, Object value) {
logger.debug("ValueWrapper putIfAbsent(Object key, Object value), the cache'name is {},the key is : {}", this.name,key);
Object cacheKey = getKey(key);
Object prevValue = null;
// 考虑使用分布式锁,或者将redis的setIfAbsent改为原子性操作
synchronized (key) {
prevValue = redisTemplate.opsForValue().get(cacheKey);
if (prevValue == null) {
long expire = getExpire();
if (expire > 0) {
redisTemplate.opsForValue().set(getKey(key), toStoreValue(value), expire, TimeUnit.MILLISECONDS);
} else {
redisTemplate.opsForValue().set(getKey(key), toStoreValue(value));
}
pushMessage(key);
caffeineCache.put(key, toStoreValue(value));
}
}
return toValueWrapper(prevValue);
}
@Override
public void evict(Object key) {
// 先清除redis中缓存数据,然后清除caffeine中的缓存,避免短时间内如果先清除caffeine缓存后其他请求会再从redis里加载到caffeine中
logger.debug("evict(Object key), the cache'name is {},the key is : {}",this.name, key);
redisTemplate.delete(getKey(key));
pushMessage(key);
caffeineCache.invalidate(key);
}
@Override
public void clear() {
logger.debug("clear() the cache'name is {}",this.name);
// 先清除redis中缓存数据,然后清除caffeine中的缓存,避免短时间内如果先清除caffeine缓存后其他请求会再从redis里加载到caffeine中
Set<Object> keys = redisTemplate.keys(this.name.concat(":"));
for (Object key : keys) {
redisTemplate.delete(key);
}
pushMessage(null);
caffeineCache.invalidateAll();
}
@Override
protected Object lookup(Object key) {
logger.debug("Object lookup(Object key),the cache'name is {}, the key is : {}", this.name,key);
Object cacheKey = getKey(key);
Object value = caffeineCache.getIfPresent(key);
if (value != null) {
logger.debug("get cache from caffeine, the cache'name is {},the key is : {}",this.name, cacheKey);
return value;
}
value = redisTemplate.opsForValue().get(cacheKey);
if (value != null) {
logger.debug("get cache from redis and put in caffeine, the cache'name is {},the key is : {}", this.name,cacheKey);
caffeineCache.put(key, value);
}
return value;
}
private Object getKey(Object key) {
return this.name.concat(":").concat(
StringUtils.isEmpty(cachePrefix) ? key.toString() : cachePrefix.concat(":").concat(key.toString()));
}
private long getExpire() {
long expire = defaultExpiration;
Long cacheNameExpire = expires.get(this.name);
return cacheNameExpire == null ? expire : cacheNameExpire.longValue();
}
/**
* 缓存变更时通知其他节点清理本地缓存
* @param key 待清理的键
*/
private void pushMessage(Object key) {
logger.debug("push(CacheMessage message) 缓存变更时通知其他节点清理本地缓存,the cache'name is {}, the key is : {}",this.name, key);
redisTemplate.convertAndSend(topic, new CacheMessage(cacheRedisCaffeineProperties.getLocalIp(),this.name,key));
}
/**
* 清理本地缓存
* @param key
*/
public void clearLocal(Object key) {
logger.debug("clear local cache, the cache'name is {}, the key is : {}",this.name, key);
if (key == null) {
caffeineCache.invalidateAll();
} else {
caffeineCache.invalidate(key);
}
}
}
RedisCaffeineCache.java继承于AbstractValueAdaptingCache,实现spring的cache。可以说,我们实现了一套全新的,实现spring规范的cache。当然,在遵守spring规范下,我们同时实现了《两级缓存数据流原理》。
5、缓存管理器RedisCaffeineCacheManager.java
package com.demo.example.security.core.cache;
import java.util.Collection;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.TimeUnit;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.cache.Cache;
import org.springframework.cache.CacheManager;
import org.springframework.data.redis.core.RedisTemplate;
import com.demo.example.security.core.cache.CacheRedisCaffeineProperties;
import com.github.benmanes.caffeine.cache.Caffeine;
public class RedisCaffeineCacheManager implements CacheManager {
/**
* 日志.
*/
private final Logger logger = LoggerFactory.getLogger(RedisCaffeineCacheManager.class);
/**
* 管理器中已经建立的缓存组.
*/
private ConcurrentMap<String, Cache> cacheMap = new ConcurrentHashMap<String, Cache>();
/**
* 缓存配置参数.
*/
private CacheRedisCaffeineProperties cacheRedisCaffeineProperties;
/**
* redis连接模板.
*/
private RedisTemplate<Object, Object> redisTemplate;
/**
* 是否动态创建缓存组,每组独立一个缓存对象
*/
private boolean dynamic = true;
/**
* 管理器中已经建立的缓存组的名称.
*/
private Set<String> cacheNames;
/**
* 构造函数.
*
* @param cacheRedisCaffeineProperties
* 缓存配置参数
* @param redisTemplate
* redis连接模板
*/
public RedisCaffeineCacheManager(CacheRedisCaffeineProperties cacheRedisCaffeineProperties,
RedisTemplate<Object, Object> redisTemplate) {
super();
this.cacheRedisCaffeineProperties = cacheRedisCaffeineProperties;
this.redisTemplate = redisTemplate;
this.dynamic = cacheRedisCaffeineProperties.isDynamic();
this.cacheNames = cacheRedisCaffeineProperties.getCacheNames();
if (this.dynamic == false) {
logger.debug("create cache instance, the cache name is : {}", "localonly");
Cache cache = new RedisCaffeineCache("localonly", redisTemplate, caffeineCacheBuild(),
cacheRedisCaffeineProperties);
cacheMap.putIfAbsent("localonly", cache);
}
}
/**
* 获取缓存实列.
*/
@Override
public Cache getCache(String name) {
Cache cache = cacheMap.get(name);
if (cache == null) {
if (this.dynamic) {
logger.debug("create cache instance, the cache name is : {}", name);
cache = new RedisCaffeineCache(name, redisTemplate, caffeineCacheBuild(), cacheRedisCaffeineProperties);
Cache oldCache = cacheMap.putIfAbsent(name, cache);
if (oldCache != null) {
cache = oldCache;
}
} else {
if (cacheNames.contains(name) || cacheNames.size()==0) {
cache = cacheMap.get("localonly");
} else {
//系统报错,缓存没有纳入管理
logger.error("[缓存组={}]未纳入缓存管理,请无配置缓存注解",name);
}
}
}
return cache;
}
/**
* 构建Caffeine缓存实列
*
* @return Caffeine缓存实列
*/
private com.github.benmanes.caffeine.cache.Cache<Object, Object> caffeineCacheBuild() {
Caffeine<Object, Object> cacheBuilder = Caffeine.newBuilder();
if (cacheRedisCaffeineProperties.getCaffeine().getExpireAfterAccess() > 0) {
cacheBuilder.expireAfterAccess(cacheRedisCaffeineProperties.getCaffeine().getExpireAfterAccess(),
TimeUnit.MILLISECONDS);
}
if (cacheRedisCaffeineProperties.getCaffeine().getExpireAfterWrite() > 0) {
cacheBuilder.expireAfterWrite(cacheRedisCaffeineProperties.getCaffeine().getExpireAfterWrite(),
TimeUnit.MILLISECONDS);
}
if (cacheRedisCaffeineProperties.getCaffeine().getInitialCapacity() > 0) {
cacheBuilder.initialCapacity(cacheRedisCaffeineProperties.getCaffeine().getInitialCapacity());
}
if (cacheRedisCaffeineProperties.getCaffeine().getMaximumSize() > 0) {
cacheBuilder.maximumSize(cacheRedisCaffeineProperties.getCaffeine().getMaximumSize());
}
if (cacheRedisCaffeineProperties.getCaffeine().getRefreshAfterWrite() > 0) {
cacheBuilder.refreshAfterWrite(cacheRedisCaffeineProperties.getCaffeine().getRefreshAfterWrite(),
TimeUnit.MILLISECONDS);
}
return cacheBuilder.build();
}
/**
* 获取缓存组的名称.
*/
@Override
public Collection<String> getCacheNames() {
if (this.dynamic) {
return this.cacheMap.keySet();
} else {
return this.cacheNames;
}
}
/**
* 清除本地缓存.
*
* @param cacheMessage
*/
public void clearLocal(CacheMessage cacheMessage) {
Cache cache = cacheMap.get(cacheMessage.getCacheName());
if (cache == null) {
return;
}
RedisCaffeineCache redisCaffeineCache = (RedisCaffeineCache) cache;
redisCaffeineCache.clearLocal(cacheMessage.getKey());
}
}
这个缓管理器主要由三个方面组成。
A、构造函数,接受参数和初始化。
B、spring规范中CacheManager接口必须要实现的两个接口:缓存实例和管理的实例的名称集合。
C、某缓存实例的缓存清除方法,以供消息监听器调用.
6、配置文件CacheRedisCaffeineConfiguration.java
package com.demo.example.security.core.cache;
import java.net.InetAddress;
import java.net.UnknownHostException;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
import org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.listener.ChannelTopic;
import org.springframework.data.redis.listener.RedisMessageListenerContainer;
@Configuration
@AutoConfigureAfter(RedisAutoConfiguration.class)
@EnableConfigurationProperties(CacheRedisCaffeineProperties.class)
public class CacheRedisCaffeineConfiguration {
private final Logger logger = LoggerFactory.getLogger(CacheRedisCaffeineConfiguration.class);
@Autowired
private CacheRedisCaffeineProperties cacheRedisCaffeineProperties;
@Bean
public RedisCaffeineCacheManager cacheManager(RedisTemplate<Object, Object> redisTemplate) {
if (StringUtils.isBlank(cacheRedisCaffeineProperties.getLocalIp()) || "127.0.0.1".equals(cacheRedisCaffeineProperties.getLocalIp())) {
//获取本机的ip地址
try {
InetAddress address = InetAddress.getLocalHost();
String host = address.getHostAddress();
cacheRedisCaffeineProperties.setLocalIp(host);
} catch (UnknownHostException e) {
logger.error("获取ip错误");
}
}
return new RedisCaffeineCacheManager(cacheRedisCaffeineProperties, redisTemplate);
}
/**
* 注册一个RedisMessageListerContainer
* @param redisTemplate
* @param redisCaffeineCacheManager
* @return
*/
@Bean
public RedisMessageListenerContainer cacheRedisCaffeineContainer(RedisTemplate<Object, Object> redisTemplate,
RedisCaffeineCacheManager redisCaffeineCacheManager) {
RedisMessageListenerContainer container = new RedisMessageListenerContainer();
container.setConnectionFactory(redisTemplate.getConnectionFactory());
CacheMessageListener cacheMessageListener = new CacheMessageListener(redisTemplate, redisCaffeineCacheManager,cacheRedisCaffeineProperties);
container.addMessageListener(cacheMessageListener,
new ChannelTopic(cacheRedisCaffeineProperties.getRedis().getTopic()));
return container;
}
}
这个配置文件,在配置文件RedisAutoConfiguration.class和属性文件CacheRedisCaffeineProperties.class之后执行;它注册了我们建立的缓存管理器,和一个redis消息的监听容器.
四、优缺点
1、成功实现两级缓存。
2、消息通知清除缓存,不清除自身发送的。
3、缓存创建比较费时,后续继续跟进。