为什么需要二级缓存
redis会需要网络通信,本地缓存不需要,如果采用二级缓存会提高效率
本地缓存也可以叫做应用缓存,网络开销很小
如果采用用redis,会涉及到请求,有网络上的开销
本地缓存常用选型
本人现在没有过多研究选型相关,参考链接如下:
Java本地缓存技术选型(Guava Cache、Caffeine、Encache)本地缓存选型(Guava/Caffeine/Ohc)及性能对比
我本次使用Caffeine
springboot中集成spring cache,已经有了多种缓存方式的实现,例如Redis、Caffeine、JCache、EhCache等
使用缓存流程
- 从一级缓存读出数据,如果存在则直接进行业务
- 不存在则读取二级缓存,如果存在则更新一级缓存
- 不存在则从数据库读,然后依次更新二级缓存、一级缓存,然后业务处理
spring cache
springcache本身并没有提供缓存的实现,但是他提供了一套的接口、代码规范、配置、注解等,这样很容易就可以整合各种缓存,springcache主要包含了两个核心的接口来统一管理缓存相关的实现。分别是:
- org.springframework.cache.Cache 定义了通用缓存操作的接口
- org.springframework.cache.CacheManager 是一个spring的缓存管理器,主要用于管理不同的缓存
引入pom
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
Caffeine
我们使用Caffeine来作为本地的缓存,只需要引入
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
</dependency>
实现步骤
- 定义缓存的配置文件
- 实现Cache 接口
- 实现CacheManager接口
- 增加spring boot配置类
具体实现
定义缓存的配置文件
定义properties配置属性类,主要用于以配置文件进行配置。
@ConfigurationProperties(prefix = "model.cache.multi")
public class RedisCaffeineCacheProperties {
/**
* 静态缓存名称
*/
private Set<String> cacheNames = new HashSet<>();
/**
* 一级缓存名称
*/
private Set<String> firstCacheNames = new HashSet<>();
/**
* 是否存储空值,默认true
*/
private boolean storeNullValues = true;
/**
* 缓存redis key的前缀
*/
private String redisKeyPrefix = "";
private Redis redis = new Redis();
private Caffeine caffeine = new Caffeine();
public class Redis {
//设置全局过期时间,默认8天
private long defaultExpiration = 60 * 60 * 24 * 8;
//缓存更新时通知其他节点的topic名称
private String topic = "cache:redis:caffeine:topic";
//每个cacheName的过期时间,单位毫秒,优先级比defaultExpiration高
private Map<String, Long> expires = new HashMap<>();
public Map<String, Long> getExpires() {
return expires;
}
public void setExpires(Map<String, Long> expires) {
this.expires = expires;
}
public long getDefaultExpiration() {
return defaultExpiration;
}
public void setDefaultExpiration(long defaultExpiration) {
this.defaultExpiration = defaultExpiration;
}
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 maximumSize;
//初始化大小
private int initialCapacity;
//每个cacheName的写入后过期时间(expireAfterWrite),找不到就用上面定义的配置。单位毫秒
private Map<String, Long> expires = new HashMap<>();
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 getMaximumSize() {
return maximumSize;
}
public void setMaximumSize(int maximumSize) {
this.maximumSize = maximumSize;
}
public int getInitialCapacity() {
return initialCapacity;
}
public void setInitialCapacity(int initialCapacity) {
this.initialCapacity = initialCapacity;
}
public Map<String, Long> getExpires() {
return expires;
}
public void setExpires(Map<String, Long> expires) {
this.expires = expires;
}
}
public boolean isStoreNullValues() {
return storeNullValues;
}
public void setStoreNullValues(boolean storeNullValues) {
this.storeNullValues = storeNullValues;
}
public String getRedisKeyPrefix() {
return redisKeyPrefix;
}
public void setRedisKeyPrefix(String redisKeyPrefix) {
this.redisKeyPrefix = redisKeyPrefix;
}
public Set<String> getCacheNames() {
return cacheNames;
}
public void setCacheNames(Set<String> cacheNames) {
this.cacheNames = cacheNames;
}
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;
}
public Set<String> getFirstCacheNames() {
return firstCacheNames;
}
public void setFirstCacheNames(Set<String> firstCacheNames) {
this.firstCacheNames = firstCacheNames;
}
}
实现Cache 接口
对多级缓存的具体操作实现,例如对缓存的get、put、evict操作
public class RedisCaffeineCache extends AbstractValueAdaptingCache {
private static final Logger log = LoggerFactory.getLogger(RedisCaffeineCache.class);
private RedisTemplate<String, Object> redisTemplate;
/**
* 多级缓存的唯一标识
*/
private String uniqueKey = UUID.randomUUID().toString();
/**
* 多级缓存的名称
*/
private String name;
/**
* redis key前缀
*/
private String cachePrefix;
/**
* redis key 的过期时间
*/
private Map<String, Long> expires;
/**
* redis 全局过期时间
*/
private long defaultExpiration = 0;
/**
* 定义一个全局的公平锁
*/
private static ReentrantLock fairLock = new ReentrantLock(true);
/**
* 是否开启一级缓存
*/
private boolean firstCache;
/**
* 一级缓存
*/
private Cache<Object, Object> caffeineCache;
/**
* redis消息队列的topic
*/
private String topic = "cache:redis:caffeine:topic";
public RedisCaffeineCache(String name, RedisTemplate<String, Object> redisTemplate, Cache<Object, Object> caffeineCache,RedisCaffeineCacheProperties redisCaffeineCacheProperties) {
super(redisCaffeineCacheProperties.isStoreNullValues());
this.name = name;
this.redisTemplate = redisTemplate;
this.cachePrefix = redisCaffeineCacheProperties.getRedisKeyPrefix();
this.expires = redisCaffeineCacheProperties.getRedis().getExpires();
this.defaultExpiration = redisCaffeineCacheProperties.getRedis().getDefaultExpiration();
this.topic = redisCaffeineCacheProperties.getRedis().getTopic();
this.caffeineCache = caffeineCache;
if (this.caffeineCache == null) {
this.firstCache = false;
}else{
this.firstCache = true;
}
}
/**
* 执行底层查询操作
* @param key
* @return
*/
@Override
protected Object lookup(Object key) {
//如果开启了一级缓存,则先从一级缓存中查询
if(firstCache){
Object value = caffeineCache.getIfPresent(key);
if(ObjectUtils.isNotEmpty(value)) {
log.info("从caffeine中获取数据:{}",value);
return value;
}
}
String redisKey = getKey(key);
Object value = redisTemplate.opsForValue().get(redisKey);
if(ObjectUtils.isNotEmpty(value)) {
log.info("从redis中获取数据:{}",value);
caffeineCache.put(key, value);
}
return value;
}
@Override
public String getName() {
return this.name;
}
@Override
public Object getNativeCache() {
return this;
}
/**
* get cache value from RedisCaffeineCache
* @param key
* @param valueLoader
* @param <T>
* @return
*/
@Override
public <T> T get(Object key, Callable<T> valueLoader) {
Object value = lookup(key);
if(ObjectUtils.isNotEmpty(value)) {
return (T) value;
}
fairLock.lock();
try {
value = lookup(key);
if(ObjectUtils.isNotEmpty(value)) {
return (T) value;
}
value = valueLoader.call();
Object storeValue = toStoreValue(value);
put(key, storeValue);
return (T) value;
} catch (Exception e) {
throw new ValueRetrievalException(key, valueLoader, e);
}finally {
fairLock.unlock();
}
}
/**
* put value into RedisCaffeineCache
* @param key
* @param value
*/
@Override
public void put(Object key, Object value) {
if (!super.isAllowNullValues() && ObjectUtils.isEmpty(value)) {
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));
}
if (firstCache) {
//发送消息到消息队列
push(new CacheMessage(name,key,uniqueKey));
caffeineCache.put(key, value);
}
}
/**
* 如果不能存在put,存在返回oldValue
* @param key
* @param value
* @return
*/
@Override
public ValueWrapper putIfAbsent(Object key, Object value) {
String cacheKey = getKey(key);
Object oldValue = null;
synchronized (key) {
oldValue = redisTemplate.opsForValue().get(cacheKey);
if (ObjectUtils.isEmpty(oldValue)) {
long expire = getExpire();
if (expire > 0) {
redisTemplate.opsForValue().set(cacheKey, toStoreValue(value), expire, TimeUnit.MILLISECONDS);
}else {
redisTemplate.opsForValue().set(cacheKey, toStoreValue(value));
}
if (firstCache) {
push(new CacheMessage(name,key,uniqueKey));
caffeineCache.put(key, value);
}
}
}
return toValueWrapper(oldValue);
}
@Override
public void evict(Object key) {
//删除redis中的缓存
redisTemplate.delete(getKey(key));
//删除一级缓存
if (firstCache) {
push(new CacheMessage(name,key,uniqueKey));
caffeineCache.invalidate(key);
}
}
@Override
public void clear() {
Set<String> scan = RedisHelper.scan(redisTemplate, this.name.concat(":*"));
redisTemplate.delete(scan);
if (firstCache) {
push(new CacheMessage(name,null,uniqueKey));
caffeineCache.invalidateAll();
}
}
/**
* 重写fromStoreValue方法 ,避免获取NULLValue报错的问题
* @param storeValue
* @return
*/
@Nullable
@Override
protected Object fromStoreValue(Object storeValue) {
if (super.isAllowNullValues() && (storeValue == NullValue.INSTANCE || storeValue instanceof NullValue)) {
return null;
}
return storeValue;
}
/**
* 获取redisKey缓存名称
*
* @return
*/
private String getKey(Object key) {
return name.concat(":").concat(StringUtils.isEmpty(cachePrefix) ? key.toString() : cachePrefix.concat(":").concat(key.toString()));
}
/**
* 获取redis 缓存过期时间
* @return
*/
private long getExpire() {
Long cacheNameExpire = expires.get(name);
return cacheNameExpire == null ? defaultExpiration : cacheNameExpire;
}
public Cache<Object, Object> getLocalCache() {
return caffeineCache;
}
/**
* 通知其他缓存删除一级缓存
* @param message
*/
private void push(CacheMessage message) {
redisTemplate.convertAndSend(topic, message);
}
/**
* 清除多级缓存值
* @param key
*/
public void clearLocal(Object key) {
if (!firstCache) {
return;
}
if (key == null) {
caffeineCache.invalidateAll();
} else {
caffeineCache.invalidate(key);
}
}
public String getUniqueKey(){
return this.uniqueKey;
}
}
其中的push方法主要是通知其他缓存删除特定的一级缓存
CacheMessage实现如下
public class CacheMessage implements Serializable {
private static final long serialVersionUID = -3742394520710710440L;
/**
* 多级缓存的名称
*/
private String name;
/**
* 一级缓存的key
*/
private Object key;
/**
* 多级缓存的唯一标识
*/
private String redisCaffeineCacheUniqueKey;
public CacheMessage() {
}
public CacheMessage(String name, Object key, String redisCaffeineCacheUniqueKey) {
this.name = name;
this.key = key;
this.redisCaffeineCacheUniqueKey = redisCaffeineCacheUniqueKey;
}
public CacheMessage(String name, Object key) {
this.name = name;
this.key = key;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Object getKey() {
return key;
}
public void setKey(Object key) {
this.key = key;
}
public String getRedisCaffeineCacheUniqueKey() {
return redisCaffeineCacheUniqueKey;
}
public void setRedisCaffeineCacheUniqueKey(String redisCaffeineCacheUniqueKey) {
this.redisCaffeineCacheUniqueKey = redisCaffeineCacheUniqueKey;
}
}
当给redis发送消息后,redis接收到消息会做处理,需要重写redis的消息监听器
CacheMessageListener实现如下
public class CacheMessageListener implements MessageListener {
private static final Logger log = LoggerFactory.getLogger(CacheMessageListener.class);
private RedisTemplate<String, Object> redisTemplate;
private RedisCaffeineCacheManager redisCaffeineCacheManager;
public CacheMessageListener(RedisTemplate<String, Object> redisTemplate, RedisCaffeineCacheManager redisCaffeineCacheManager) {
super();
this.redisTemplate = redisTemplate;
this.redisCaffeineCacheManager = redisCaffeineCacheManager;
}
@Override
public void onMessage(Message message, byte[] pattern) {
//处理收到的消息,去清除其他多级缓存的一级缓存
CacheMessage cacheMessage = (CacheMessage) redisTemplate.getValueSerializer().deserialize(message.getBody());
log.debug("receive a redis topic message, clear local cache, the cacheName is {}, the key is {},the uniqueKey is {}", cacheMessage.getName(), cacheMessage.getKey(), cacheMessage.getRedisCaffeineCacheUniqueKey());
redisCaffeineCacheManager.clearLocal(cacheMessage.getName(), cacheMessage.getKey(), cacheMessage.getRedisCaffeineCacheUniqueKey());
}
}
实现CacheManager接口
主要是对多级缓存的管理,核心方法就是getCache() 。
public class RedisCaffeineCacheManager implements CacheManager {
/**
* key: cacheName value: redisCaffeineCache
*/
private ConcurrentMap<String, Cache> cacheMap = new ConcurrentHashMap<String, Cache>();
private RedisTemplate<String, Object> stringKeyRedisTemplate;
private RedisCaffeineCacheProperties redisCaffeineCacheProperties;
/**
* 是否根据cacheName动态生成
*/
private boolean dynamic = true;
/**
* 不动态根据cacheName创建Cache的实现时,自定义设置的缓存名
*/
private Set<String> cacheNames;
/**
* 一级缓存名集合
*/
private Set<String> firstCacheNames;
/**
* 一级缓存的过期时间
*/
private Map<String, Long> expires;
public RedisCaffeineCacheManager(RedisTemplate<String, Object> redisTemplate, RedisCaffeineCacheProperties redisCaffeineCacheProperties) {
super();
this.stringKeyRedisTemplate = redisTemplate;
this.redisCaffeineCacheProperties = redisCaffeineCacheProperties;
this.cacheNames = redisCaffeineCacheProperties.getCacheNames();
this.firstCacheNames = redisCaffeineCacheProperties.getFirstCacheNames();
this.expires = redisCaffeineCacheProperties.getCaffeine().getExpires();
}
@Override
public Cache getCache(String name) {
Cache cache = cacheMap.get(name);
if (cache != null) {
return cache;
}
//如果不动态根据cacheName创建Cache的实现,且没有配置静态缓存 则返回空
if (!dynamic && cacheNames.isEmpty()) {
return null;
}
//如果开启一级缓存
if (firstCacheNames.contains(name)) {
//开启一级缓存
cache = new RedisCaffeineCache(name, stringKeyRedisTemplate, caffeineCache(name), redisCaffeineCacheProperties);
}else {
// 只开启二级缓存 即只开启redis缓存
cache = new RedisCaffeineCache(name, stringKeyRedisTemplate, null, redisCaffeineCacheProperties);
}
Cache oldCache = cacheMap.putIfAbsent(name, cache);
return oldCache == null ? cache : oldCache;
}
/**
* 构建一个caffeineCache 即构建一个一级缓存
* @param name
* @return
*/
private com.github.benmanes.caffeine.cache.Cache caffeineCache(String name) {
Caffeine<Object, Object> caffeineBuilder = Caffeine.newBuilder();
if(redisCaffeineCacheProperties.getCaffeine().getExpireAfterAccess() > 0) {
caffeineBuilder.expireAfterAccess(redisCaffeineCacheProperties.getCaffeine().getExpireAfterAccess(), TimeUnit.MILLISECONDS);
}
Long expire = expires.get(name);
if (expire != null && expire > 0) {
caffeineBuilder.expireAfterWrite(expire, TimeUnit.MILLISECONDS);
}else if (redisCaffeineCacheProperties.getCaffeine().getExpireAfterWrite() > 0){
caffeineBuilder.expireAfterWrite(redisCaffeineCacheProperties.getCaffeine().getExpireAfterAccess(), TimeUnit.MILLISECONDS);
}
if (redisCaffeineCacheProperties.getCaffeine().getRefreshAfterWrite() > 0) {
caffeineBuilder.refreshAfterWrite(redisCaffeineCacheProperties.getCaffeine().getRefreshAfterWrite(), TimeUnit.MILLISECONDS);
}
if (redisCaffeineCacheProperties.getCaffeine().getInitialCapacity() > 0) {
caffeineBuilder.initialCapacity(redisCaffeineCacheProperties.getCaffeine().getInitialCapacity());
}
if (redisCaffeineCacheProperties.getCaffeine().getMaximumSize() > 0) {
caffeineBuilder.maximumSize(redisCaffeineCacheProperties.getCaffeine().getMaximumSize());
}
return caffeineBuilder.build();
}
@Override
public Collection<String> getCacheNames() {
return this.cacheNames;
}
/**
* 清除 多级缓存中的某一级缓存
* @param name
* @param key
* @param redisCaffeineCacheUniqueKey
*/
public void clearLocal(String name, Object key,String redisCaffeineCacheUniqueKey) {
Cache cache = cacheMap.get(name);
if(cache == null) {
return ;
}
RedisCaffeineCache redisCaffeineCache = (RedisCaffeineCache) cache;
//判断是否是自己发送的消息 如果是自己发送的就不需要清楚缓存了
if (StringUtils.isNoneEmpty(redisCaffeineCacheUniqueKey) && redisCaffeineCache.getUniqueKey().equals(redisCaffeineCacheUniqueKey)) {
return;
}
redisCaffeineCache.clearLocal(key);
}
}
增加spring boot配置类
只是我自己的一种实现,具体根据自己的项目来进行配置
@Configuration
@EnableConfigurationProperties(RedisCaffeineCacheProperties.class)
@EnableCaching
public class RedisCaffeineCacheAutoConfiguration {
@Autowired
private RedisCaffeineCacheProperties redisCaffeineCacheProperties;
@Bean
public RedisCaffeineCacheManager cacheManager(RedisTemplate<String, Object> redisTemplate) {
return new RedisCaffeineCacheManager(redisTemplate, redisCaffeineCacheProperties);
}
@Bean
@ConditionalOnBean(name = {"redisTemplate", "cacheManager"})
public RedisMessageListenerContainer redisMessageListenerContainer(RedisTemplate<String, Object> redisTemplate,RedisCaffeineCacheManager redisCaffeineCacheManager) {
RedisMessageListenerContainer redisMessageListenerContainer = new RedisMessageListenerContainer();
redisMessageListenerContainer.setConnectionFactory(redisTemplate.getConnectionFactory());
CacheMessageListener cacheMessageListener = new CacheMessageListener(redisTemplate, redisCaffeineCacheManager);
redisMessageListenerContainer.addMessageListener(cacheMessageListener, new ChannelTopic(redisCaffeineCacheProperties.getRedis().getTopic()));
return redisMessageListenerContainer;
}
}
以上就是利用spring cache 实现多级缓存的核心,仅供参考。
总结
这是我第一次接触了解这种缓存实现,不足之处请见谅。
关于设置锁的地方,建议使用Redisson操作会更方便。