spring boot+spring cache实现两级缓存(redis+caffeine)

spring boot中集成了spring cache,并有多种缓存方式的实现,如:Redis、Caffeine、JCache、EhCache等等。但如果只用一种缓存,要么会有较大的网络消耗(如Redis),要么就是内存占用太大(如Caffeine这种应用内存缓存)。在很多场景下,可以结合起来实现一、二级缓存的方式,能够很大程度提高应用的处理效率。

内容说明:

  1. 缓存、两级缓存
  2. spring cache:主要包含spring cache定义的接口方法说明和注解中的属性说明
  3. spring boot + spring cache:RedisCache实现中的缺陷
  4. caffeine简介
  5. spring boot + spring cache 实现两级缓存(redis + caffeine)

缓存、两级缓存

简单的理解,缓存就是将数据从读取较慢的介质上读取出来放到读取较快的介质上,如磁盘-->内存。平时我们会将数据存储到磁盘上,如:数据库。如果每次都从数据库里去读取,会因为磁盘本身的IO影响读取速度,所以就有了像redis这种的内存缓存。可以将数据读取出来放到内存里,这样当需要获取数据时,就能够直接从内存中拿到数据返回,能够很大程度的提高速度。但是一般redis是单独部署成集群,所以会有网络IO上的消耗,虽然与redis集群的链接已经有连接池这种工具,但是数据传输上也还是会有一定消耗。所以就有了应用内缓存,如:caffeine。当应用内缓存有符合条件的数据时,就可以直接使用,而不用通过网络到redis中去获取,这样就形成了两级缓存。应用内缓存叫做一级缓存,远程缓存(如redis)叫做二级缓存

spring cache

当使用缓存的时候,一般是如下的流程:

spring boot+spring cache实现两级缓存(redis+caffeine)_第1张图片

从流程图中可以看出,为了使用缓存,在原有业务处理的基础上,增加了很多对于缓存的操作,如果将这些耦合到业务代码当中,开发起来就有很多重复性的工作,并且不太利于根据代码去理解业务。

spring cache是spring-context包中提供的基于注解方式使用的缓存组件,定义了一些标准接口,通过实现这些接口,就可以通过在方法上增加注解来实现缓存。这样就能够避免缓存代码与业务处理耦合在一起的问题。spring cache的实现是使用spring aop中对方法切面(MethodInterceptor)封装的扩展,当然spring aop也是基于Aspect来实现的。

spring cache核心的接口就两个:Cache和CacheManager

spring boot+spring cache实现两级缓存(redis+caffeine)_第2张图片

Cache接口

提供缓存的具体操作,比如缓存的放入、读取、清理,spring框架中默认提供的实现有:

spring boot+spring cache实现两级缓存(redis+caffeine)_第3张图片

除了RedisCache是在spring-data-redis包中,其他的基本都是在spring-context-support包中

spring boot+spring cache实现两级缓存(redis+caffeine)_第4张图片

#Cache.java

package org.springframework.cache;

import java.util.concurrent.Callable;

public interface Cache {

 // cacheName,缓存的名字,默认实现中一般是CacheManager创建Cache的bean时传入cacheName
 String getName();

 // 获取实际使用的缓存,如:RedisTemplate、com.github.benmanes.caffeine.cache.Cache。暂时没发现实际用处,可能只是提供获取原生缓存的bean,以便需要扩展一些缓存操作或统计之类的东西
 Object getNativeCache();

 // 通过key获取缓存值,注意返回的是ValueWrapper,为了兼容存储空值的情况,将返回值包装了一层,通过get方法获取实际值
 ValueWrapper get(Object key);

 // 通过key获取缓存值,返回的是实际值,即方法的返回值类型
  T get(Object key, Class type);

 // 通过key获取缓存值,可以使用valueLoader.call()来调使用@Cacheable注解的方法。当@Cacheable注解的sync属性配置为true时使用此方法。因此方法内需要保证回源到数据库的同步性。避免在缓存失效时大量请求回源到数据库
  T get(Object key, Callable valueLoader);

 // 将@Cacheable注解方法返回的数据放入缓存中
 void put(Object key, Object value);

 // 当缓存中不存在key时才放入缓存。返回值是当key存在时原有的数据
 ValueWrapper putIfAbsent(Object key, Object value);

 // 删除缓存
 void evict(Object key);

 // 删除缓存中的所有数据。需要注意的是,具体实现中只删除使用@Cacheable注解缓存的所有数据,不要影响应用内的其他缓存
 void clear();

 // 缓存返回值的包装
 interface ValueWrapper {

 // 返回实际缓存的对象
 Object get();
 }

 // 当{@link #get(Object, Callable)}抛出异常时,会包装成此异常抛出
 @SuppressWarnings("serial")
 class ValueRetrievalException extends RuntimeException {

 private final Object key;

 public ValueRetrievalException(Object key, Callable loader, Throwable ex) {
  super(String.format("Value for key '%s' could not be loaded using '%s'", key, loader), ex);
  this.key = key;
 }

 public Object getKey() {
  return this.key;
 }
 }
}

CacheManager接口

主要提供Cache实现bean的创建,每个应用里可以通过cacheName来对Cache进行隔离,每个cacheName对应一个Cache实现。spring框架中默认提供的实现与Cache的实现都是成对出现,包结构也在上图中

#CacheManager.java

package org.springframework.cache;

import java.util.Collection;

public interface CacheManager {

 // 通过cacheName创建Cache的实现bean,具体实现中需要存储已创建的Cache实现bean,避免重复创建,也避免内存缓存对象(如Caffeine)重新创建后原来缓存内容丢失的情况
 Cache getCache(String name);

 // 返回所有的cacheName
 Collection getCacheNames();
}

常用注解说明

@Cacheable:主要应用到查询数据的方法上

package org.springframework.cache.annotation;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.util.concurrent.Callable;
import org.springframework.core.annotation.AliasFor;
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface Cacheable {
    // cacheNames,CacheManager就是通过这个名称创建对应的Cache实现bean
 @AliasFor("cacheNames")
 String[] value() default {};

 @AliasFor("value")
 String[] cacheNames() default {};

    // 缓存的key,支持SpEL表达式。默认是使用所有参数及其计算的hashCode包装后的对象(SimpleKey)
 String key() default "";

 // 缓存key生成器,默认实现是SimpleKeyGenerator
 String keyGenerator() default "";

 // 指定使用哪个CacheManager
 String cacheManager() default "";

 // 缓存解析器
 String cacheResolver() default "";

 // 缓存的条件,支持SpEL表达式,当达到满足的条件时才缓存数据。在调用方法前后都会判断
 String condition() default "";
    
    // 满足条件时不更新缓存,支持SpEL表达式,只在调用方法后判断
 String unless() default "";

 // 回源到实际方法获取数据时,是否要保持同步,如果为false,调用的是Cache.get(key)方法;如果为true,调用的是Cache.get(key, Callable)方法
 boolean sync() default false;
}

@CacheEvict:清除缓存,主要应用到删除数据的方法上。相比Cacheable多了两个属性

package org.springframework.cache.annotation;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.core.annotation.AliasFor;
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface CacheEvict {
    // ...相同属性说明请参考@Cacheable中的说明

 // 是否要清除所有缓存的数据,为false时调用的是Cache.evict(key)方法;为true时调用的是Cache.clear()方法
 boolean allEntries() default false;

 // 调用方法之前或之后清除缓存
 boolean beforeInvocation() default false;
}

  1. @CachePut:放入缓存,主要用到对数据有更新的方法上。属性说明参考@Cacheable
  2. @Caching:用于在一个方法上配置多种注解
  3. @EnableCaching:启用spring cache缓存,作为总的开关,在spring boot的启动类或配置类上需要加上此注解才会生效

spring boot + spring cache

spring boot中已经整合了spring cache,并且提供了多种缓存的配置,在使用时只需要配置使用哪个缓存(enum CacheType)即可。

spring boot+spring cache实现两级缓存(redis+caffeine)_第5张图片

spring boot中多增加了一个可以扩展的东西,就是CacheManagerCustomizer接口,可以自定义实现这个接口,然后对CacheManager做一些设置,比如:

package com.itopener.demo.cache.redis.config;

import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

import org.springframework.boot.autoconfigure.cache.CacheManagerCustomizer;
import org.springframework.data.redis.cache.RedisCacheManager;

public class RedisCacheManagerCustomizer implements CacheManagerCustomizer {

 @Override
 public void customize(RedisCacheManager cacheManager) {
 // 默认过期时间,单位秒
 cacheManager.setDefaultExpiration(1000);
 cacheManager.setUsePrefix(false);
 Map expires = new ConcurrentHashMap();
 expires.put("userIdCache", 2000L);
 cacheManager.setExpires(expires);
 }

}

加载这个bean:

package com.itopener.demo.cache.redis.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * @author fuwei.deng
 * @date 2017年12月22日 上午10:24:54
 * @version 1.0.0
 */
@Configuration
public class CacheRedisConfiguration {
 
 @Bean
 public RedisCacheManagerCustomizer redisCacheManagerCustomizer() {
 return new RedisCacheManagerCustomizer();
 }
}

常用的缓存就是Redis了,Redis对于spring cache接口的实现是在spring-data-redis包中

spring boot+spring cache实现两级缓存(redis+caffeine)_第6张图片

这里提下我认为的RedisCache实现中的缺陷:

1.在缓存失效的瞬间,如果有线程获取缓存数据,可能出现返回null的情况,原因是RedisCache实现中是如下步骤:

  1. 判断缓存key是否存在
  2. 如果key存在,再获取缓存数据,并返回

因此当判断key存在后缓存失效了,再去获取缓存是没有数据的,就返回null了。

2.RedisCacheManager中是否允许存储空值的属性(cacheNullValues)默认为false,即不允许存储空值,这样会存在缓存穿透的风险。缺陷是这个属性是final类型的,只能在创建对象是通过构造方法传入,所以要避免缓存穿透就只能自己在应用内声明RedisCacheManager这个bean了

3.RedisCacheManager中的属性无法通过配置文件直接配置,只能在应用内实现CacheManagerCustomizer接口来进行设置,个人认为不太方便

Caffeine

Caffeine是一个基于Google开源的Guava设计理念的一个高性能内存缓存,使用java8开发,spring boot引入Caffeine后已经逐步废弃Guava的整合了。Caffeine源码及介绍地址:caffeine

caffeine提供了多种缓存填充策略、值回收策略,同时也包含了缓存命中次数等统计数据,对缓存的优化能够提供很大帮助

caffeine的介绍可以参考://www.jb51.net/article/134242.htm

这里简单说下caffeine基于时间的回收策略有以下几种:

  1. expireAfterAccess:访问后到期,从上次读或写发生后的过期时间
  2. expireAfterWrite:写入后到期,从上次写入发生之后的过期时间
  3. 自定义策略:到期时间由实现Expiry接口后单独计算

spring boot + spring cache 实现两级缓存(redis + caffeine)

本人开头提到了,就算是使用了redis缓存,也会存在一定程度的网络传输上的消耗,在实际应用当中,会存在一些变更频率非常低的数据,就可以直接缓存在应用内部,对于一些实时性要求不太高的数据,也可以在应用内部缓存一定时间,减少对redis的访问,提高响应速度

由于spring-data-redis框架中redis对spring cache的实现有一些不足,在使用起来可能会出现一些问题,所以就不基于原来的实现去扩展了,直接参考实现方式,去实现Cache和CacheManager接口

还需要注意一点,一般应用都部署了多个节点,一级缓存是在应用内的缓存,所以当对数据更新和清除时,需要通知所有节点进行清理缓存的操作。可以有多种方式来实现这种效果,比如:zookeeper、MQ等,但是既然用了redis缓存,redis本身是有支持订阅/发布功能的,所以就不依赖其他组件了,直接使用redis的通道来通知其他节点进行清理缓存的操作

以下就是对spring boot + spring cache实现两级缓存(redis + caffeine)的starter封装步骤和源码

定义properties配置属性类

package com.itopener.cache.redis.caffeine.spring.boot.autoconfigure;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import org.springframework.boot.context.properties.ConfigurationProperties;
/** 
 * @author fuwei.deng
 * @date 2018年1月29日 上午11:32:15
 * @version 1.0.0
 */
@ConfigurationProperties(prefix = "spring.cache.multi")
public class CacheRedisCaffeineProperties { 
 private Set 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 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 getExpires() {
  return expires;
 }

 public void setExpires(Map 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 Set getCacheNames() {
 return cacheNames;
 }

 public void setCacheNames(Set cacheNames) {
 this.cacheNames = cacheNames;
 }

 public boolean isCacheNullValues() {
 return 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;
 }
}

spring cache中有实现Cache接口的一个抽象类AbstractValueAdaptingCache,包含了空值的包装和缓存值的包装,所以就不用实现Cache接口了,直接实现AbstractValueAdaptingCache抽象类

package com.itopener.cache.redis.caffeine.spring.boot.autoconfigure.support;
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.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.cache.support.AbstractValueAdaptingCache;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.util.StringUtils;
import com.github.benmanes.caffeine.cache.Cache;
import com.itopener.cache.redis.caffeine.spring.boot.autoconfigure.CacheRedisCaffeineProperties;

/**
 * @author fuwei.deng
 * @date 2018年1月26日 下午5:24:11
 * @version 1.0.0
 */
public class RedisCaffeineCache extends AbstractValueAdaptingCache { 
 private final Logger logger = LoggerFactory.getLogger(RedisCaffeineCache.class);
 private String name;
 private RedisTemplate redisTemplate;
 private Cache caffeineCache;
 private String cachePrefix;
 private long defaultExpiration = 0;
 private Map expires;
 private String topic = "cache:redis:caffeine:topic"; 
 protected RedisCaffeineCache(boolean allowNullValues) {
 super(allowNullValues);
 }
 
 public RedisCaffeineCache(String name, RedisTemplate redisTemplate, Cache caffeineCache, CacheRedisCaffeineProperties cacheRedisCaffeineProperties) {
 super(cacheRedisCaffeineProperties.isCacheNullValues());
 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();
 }

 @Override
 public String getName() {
 return this.name;
 }

 @Override
 public Object getNativeCache() {
 return this;
 }

 @SuppressWarnings("unchecked")
 @Override
 public  T get(Object key, Callable valueLoader) {
 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) {
 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));
 }
 
 push(new CacheMessage(this.name, key));
 
 caffeineCache.put(key, value);
 }

 @Override
 public ValueWrapper putIfAbsent(Object key, Object value) {
 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));
  }
  
  push(new CacheMessage(this.name, key));
  
  caffeineCache.put(key, toStoreValue(value));
  }
 }
 return toValueWrapper(prevValue);
 }

 @Override
 public void evict(Object key) {
 // 先清除redis中缓存数据,然后清除caffeine中的缓存,避免短时间内如果先清除caffeine缓存后其他请求会再从redis里加载到caffeine中
 redisTemplate.delete(getKey(key));
 
 push(new CacheMessage(this.name, key));
 
 caffeineCache.invalidate(key);
 }

 @Override
 public void clear() {
 // 先清除redis中缓存数据,然后清除caffeine中的缓存,避免短时间内如果先清除caffeine缓存后其他请求会再从redis里加载到caffeine中
 Set keys = redisTemplate.keys(this.name.concat(":"));
 for(Object key : keys) {
  redisTemplate.delete(key);
 }
 
 push(new CacheMessage(this.name, null));
 
 caffeineCache.invalidateAll();
 }

 @Override
 protected Object lookup(Object key) {
 Object cacheKey = getKey(key);
 Object value = caffeineCache.getIfPresent(key);
 if(value != null) {
  logger.debug("get cache from caffeine, the key is : {}", cacheKey);
  return value;
 }
 
 value = redisTemplate.opsForValue().get(cacheKey);
 
 if(value != null) {
  logger.debug("get cache from redis and put in caffeine, the key is : {}", 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();
 }
 
 /**
 * @description 缓存变更时通知其他节点清理本地缓存
 * @author fuwei.deng
 * @date 2018年1月31日 下午3:20:28
 * @version 1.0.0
 * @param message
 */
 private void push(CacheMessage message) {
 redisTemplate.convertAndSend(topic, message);
 }
 
 /**
 * @description 清理本地缓存
 * @author fuwei.deng
 * @date 2018年1月31日 下午3:15:39
 * @version 1.0.0
 * @param key
 */
 public void clearLocal(Object key) {
 logger.debug("clear local cache, the key is : {}", key);
 if(key == null) {
  caffeineCache.invalidateAll();
 } else {
  caffeineCache.invalidate(key);
 }
 }
} 
  
 

实现CacheManager接口

package com.itopener.cache.redis.caffeine.spring.boot.autoconfigure.support;

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.github.benmanes.caffeine.cache.Caffeine;
import com.itopener.cache.redis.caffeine.spring.boot.autoconfigure.CacheRedisCaffeineProperties;

/**
 * @author fuwei.deng
 * @date 2018年1月26日 下午5:24:52
 * @version 1.0.0
 */
public class RedisCaffeineCacheManager implements CacheManager {
 
 private final Logger logger = LoggerFactory.getLogger(RedisCaffeineCacheManager.class);
 
 private ConcurrentMap cacheMap = new ConcurrentHashMap();
 
 private CacheRedisCaffeineProperties cacheRedisCaffeineProperties;
 
 private RedisTemplate redisTemplate;

 private boolean dynamic = true;

 private Set cacheNames;

 public RedisCaffeineCacheManager(CacheRedisCaffeineProperties cacheRedisCaffeineProperties,
  RedisTemplate redisTemplate) {
 super();
 this.cacheRedisCaffeineProperties = cacheRedisCaffeineProperties;
 this.redisTemplate = redisTemplate;
 this.dynamic = cacheRedisCaffeineProperties.isDynamic();
 this.cacheNames = cacheRedisCaffeineProperties.getCacheNames();
 }

 @Override
 public Cache getCache(String name) {
 Cache cache = cacheMap.get(name);
 if(cache != null) {
  return cache;
 }
 if(!dynamic && !cacheNames.contains(name)) {
  return cache;
 }
 
 cache = new RedisCaffeineCache(name, redisTemplate, caffeineCache(), cacheRedisCaffeineProperties);
 Cache oldCache = cacheMap.putIfAbsent(name, cache);
 logger.debug("create cache instance, the cache name is : {}", name);
 return oldCache == null ? cache : oldCache;
 }
 
 public com.github.benmanes.caffeine.cache.Cache caffeineCache(){
 Caffeine 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 getCacheNames() {
 return this.cacheNames;
 }
 
 public void clearLocal(String cacheName, Object key) {
 Cache cache = cacheMap.get(cacheName);
 if(cache == null) {
  return ;
 }
 
 RedisCaffeineCache redisCaffeineCache = (RedisCaffeineCache) cache;
 redisCaffeineCache.clearLocal(key);
 }
}

redis消息发布/订阅,传输的消息类

package com.itopener.cache.redis.caffeine.spring.boot.autoconfigure.support;
import java.io.Serializable;

/** 
 * @author fuwei.deng
 * @date 2018年1月29日 下午1:31:17
 * @version 1.0.0
 */
public class CacheMessage implements Serializable {

 /** */
 private static final long serialVersionUID = 5987219310442078193L;

 private String cacheName; 
 private Object key;
 public CacheMessage(String cacheName, Object key) {
 super();
 this.cacheName = cacheName;
 this.key = key;
 }

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

监听redis消息需要实现MessageListener接口

package com.itopener.cache.redis.caffeine.spring.boot.autoconfigure.support;
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;
/** 
 * @author fuwei.deng
 * @date 2018年1月30日 下午5:22:33
 * @version 1.0.0
 */
public class CacheMessageListener implements MessageListener { 
 private final Logger logger = LoggerFactory.getLogger(CacheMessageListener.class);
 private RedisTemplate redisTemplate;
 private RedisCaffeineCacheManager redisCaffeineCacheManager;
 public CacheMessageListener(RedisTemplate 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());
 logger.debug("recevice a redis topic message, clear local cache, the cacheName is {}, the key is {}", cacheMessage.getCacheName(), cacheMessage.getKey());
 redisCaffeineCacheManager.clearLocal(cacheMessage.getCacheName(), cacheMessage.getKey());
 }
}

增加spring boot配置类

package com.itopener.cache.redis.caffeine.spring.boot.autoconfigure;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
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;
import com.itopener.cache.redis.caffeine.spring.boot.autoconfigure.support.CacheMessageListener;
import com.itopener.cache.redis.caffeine.spring.boot.autoconfigure.support.RedisCaffeineCacheManager;
/** 
 * @author fuwei.deng
 * @date 2018年1月26日 下午5:23:03
 * @version 1.0.0
 */
@Configuration
@AutoConfigureAfter(RedisAutoConfiguration.class)
@EnableConfigurationProperties(CacheRedisCaffeineProperties.class)
public class CacheRedisCaffeineAutoConfiguration {
 
 @Autowired
 private CacheRedisCaffeineProperties cacheRedisCaffeineProperties;
 
 @Bean
 @ConditionalOnBean(RedisTemplate.class)
 public RedisCaffeineCacheManager cacheManager(RedisTemplate redisTemplate) {
 return new RedisCaffeineCacheManager(cacheRedisCaffeineProperties, redisTemplate);
 }
 
 @Bean
 public RedisMessageListenerContainer redisMessageListenerContainer(RedisTemplate redisTemplate, 
  RedisCaffeineCacheManager redisCaffeineCacheManager) {
 RedisMessageListenerContainer redisMessageListenerContainer = new RedisMessageListenerContainer();
 redisMessageListenerContainer.setConnectionFactory(redisTemplate.getConnectionFactory());
 CacheMessageListener cacheMessageListener = new CacheMessageListener(redisTemplate, redisCaffeineCacheManager);
 redisMessageListenerContainer.addMessageListener(cacheMessageListener, new ChannelTopic(cacheRedisCaffeineProperties.getRedis().getTopic()));
 return redisMessageListenerContainer;
 }
}

在resources/META-INF/spring.factories文件中增加spring boot配置扫描

# Auto Configure
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.itopener.cache.redis.caffeine.spring.boot.autoconfigure.CacheRedisCaffeineAutoConfiguration

接下来就可以使用maven引入使用了


  com.itopener
  cache-redis-caffeine-spring-boot-starter
  1.0.0-SNAPSHOT
  pom

在启动类上增加@EnableCaching注解,在需要缓存的方法上增加@Cacheable注解

package com.itopener.demo.cache.redis.caffeine.service;
import java.util.Random;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.CachePut;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;
import com.itopener.demo.cache.redis.caffeine.vo.UserVO;
import com.itopener.utils.TimestampUtil;

@Service
public class CacheRedisCaffeineService {
 
 private final Logger logger = LoggerFactory.getLogger(CacheRedisCaffeineService.class);

 @Cacheable(key = "'cache_user_id_' + #id", value = "userIdCache", cacheManager = "cacheManager")
 public UserVO get(long id) {
 logger.info("get by id from db");
 UserVO user = new UserVO();
 user.setId(id);
 user.setName("name" + id);
 user.setCreateTime(TimestampUtil.current());
 return user;
 }
 
 @Cacheable(key = "'cache_user_name_' + #name", value = "userNameCache", cacheManager = "cacheManager")
 public UserVO get(String name) {
 logger.info("get by name from db");
 UserVO user = new UserVO();
 user.setId(new Random().nextLong());
 user.setName(name);
 user.setCreateTime(TimestampUtil.current());
 return user;
 }
 
 @CachePut(key = "'cache_user_id_' + #userVO.id", value = "userIdCache", cacheManager = "cacheManager")
 public UserVO update(UserVO userVO) {
 logger.info("update to db");
 userVO.setCreateTime(TimestampUtil.current());
 return userVO;
 }
 
 @CacheEvict(key = "'cache_user_id_' + #id", value = "userIdCache", cacheManager = "cacheManager")
 public void delete(long id) {
 logger.info("delete from db");
 }
}

properties文件中redis的配置跟使用redis是一样的,可以增加两级缓存的配置

#两级缓存的配置
spring.cache.multi.caffeine.expireAfterAccess=5000
spring.cache.multi.redis.defaultExpiration=60000

#spring cache配置
spring.cache.cache-names=userIdCache,userNameCache

#redis配置
#spring.redis.timeout=10000
#spring.redis.password=redispwd
#redis pool
#spring.redis.pool.maxIdle=10
#spring.redis.pool.minIdle=2
#spring.redis.pool.maxActive=10
#spring.redis.pool.maxWait=3000
#redis cluster
spring.redis.cluster.nodes=127.0.0.1:7001,127.0.0.1:7002,127.0.0.1:7003,127.0.0.1:7004,127.0.0.1:7005,127.0.0.1:7006
spring.redis.cluster.maxRedirects=3

扩展

个人认为redisson的封装更方便一些

  1. 对于spring cache缓存的实现没有那么多的缺陷
  2. 使用redis的HASH结构,可以针对不同的hashKey设置过期时间,清理的时候会更方便
  3. 如果基于redisson来实现多级缓存,可以继承RedissonCache,在对应方法增加一级缓存的操作即可
  4. 如果有使用分布式锁的情况就更方便了,可以直接使用Redisson中封装的分布式锁
  5. redisson中的发布订阅封装得更好用

后续可以增加对于缓存命中率的统计endpoint,这样就可以更好的监控各个缓存的命中情况,以便对缓存配置进行优化

源码下载

starter目录:springboot / itopener-parent / spring-boot-starters-parent / cache-redis-caffeine-spring-boot-starter-parent

示例代码目录: springboot / itopener-parent / demo-parent / demo-cache-redis-caffeine

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持脚本之家。

你可能感兴趣的:(spring boot+spring cache实现两级缓存(redis+caffeine))