SpringBoot+SpringCache實現兩級緩存(Redis+Caffeine)


 

 

spring boot中集成了spring cache,並有多種緩存方式的實現,如:Redis、Caffeine、JCache、EhCache等等。但如果只用一種緩存,要么會有較大的網絡消耗(如Redis),要么就是內存占用太大(如Caffeine這種應用內存緩存)。在很多場景下,可以結合起來實現一、二級緩存的方式,能夠很大程度提高應用的處理效率。

內容說明:

緩存、兩級緩存
spring cache:主要包含spring cache定義的接口方法說明和注解中的屬性說明
spring boot + spring cache:RedisCache實現中的缺陷
caffeine簡介
spring boot + spring cache 實現兩級緩存(redis + caffeine)

 

緩存、兩級緩存

簡單的理解,緩存就是將數據從讀取較慢的介質上讀取出來放到讀取較快的介質上,如磁盤-->內存。平時我們會將數據存儲到磁盤上,如:數據庫。如果每次都從數據庫里去讀取,會因為磁盤本身的IO影響讀取速度,所以就有了像redis這種的內存緩存。可以將數據讀取出來放到內存里,這樣當需要獲取數據時,就能夠直接從內存中拿到數據返回,能夠很大程度的提高速度。但是一般redis是單獨部署成集群,所以會有網絡IO上的消耗,雖然與redis集群的鏈接已經有連接池這種工具,但是數據傳輸上也還是會有一定消耗。所以就有了應用內緩存,如:caffeine。當應用內緩存有符合條件的數據時,就可以直接使用,而不用通過網絡到redis中去獲取,這樣就形成了兩級緩存。應用內緩存叫做一級緩存,遠程緩存(如redis)叫做二級緩存

spring cache

當使用緩存的時候,一般是如下的流程:

從流程圖中可以看出,為了使用緩存,在原有業務處理的基礎上,增加了很多對於緩存的操作,如果將這些耦合到業務代碼當中,開發起來就有很多重復性的工作,並且不太利於根據代碼去理解業務。

spring cache是spring-context包中提供的基於注解方式使用的緩存組件,定義了一些標准接口,通過實現這些接口,就可以通過在方法上增加注解來實現緩存。這樣就能夠避免緩存代碼與業務處理耦合在一起的問題。spring cache的實現是使用spring aop中對方法切面(MethodInterceptor)封裝的擴展,當然spring aop也是基於Aspect來實現的。

spring cache核心的接口就兩個:Cache和CacheManager

Cache接口

提供緩存的具體操作,比如緩存的放入、讀取、清理,spring框架中默認提供的實現有:

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

#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<Object, Object>。暫時沒發現實際用處,可能只是提供獲取原生緩存的bean,以便需要擴展一些緩存操作或統計之類的東西
 Object getNativeCache();
 
 // 通過key獲取緩存值,注意返回的是ValueWrapper,為了兼容存儲空值的情況,將返回值包裝了一層,通過get方法獲取實際值
 ValueWrapper get(Object key);
 
 // 通過key獲取緩存值,返回的是實際值,即方法的返回值類型
 <T> T get(Object key, Class<T> type);
 
 // 通過key獲取緩存值,可以使用valueLoader.call()來調使用@Cacheable注解的方法。當@Cacheable注解的sync屬性配置為true時使用此方法。因此方法內需要保證回源到數據庫的同步性。避免在緩存失效時大量請求回源到數據庫
 <T> T get(Object key, Callable<T> 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<String> 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;
}
@CachePut:放入緩存,主要用到對數據有更新的方法上。屬性說明參考@Cacheable
@Caching:用於在一個方法上配置多種注解
@EnableCaching:啟用spring cache緩存,作為總的開關,在spring boot的啟動類或配置類上需要加上此注解才會生效

spring boot + spring cache

spring boot中已經整合了spring cache,並且提供了多種緩存的配置,在使用時只需要配置使用哪個緩存(enum CacheType)即可。

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<RedisCacheManager> {
 
 @Override
 public void customize(RedisCacheManager cacheManager) {
 // 默認過期時間,單位秒
 cacheManager.setDefaultExpiration(1000);
 cacheManager.setUsePrefix(false);
 Map<String, Long> expires = new ConcurrentHashMap<String, Long>();
 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包中

這里提下我認為的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的介紹可以參考:https://www.jb51.net/article/134242.htm

這里簡單說下caffeine基於時間的回收策略有以下幾種:

expireAfterAccess:訪問后到期,從上次讀或寫發生后的過期時間
expireAfterWrite:寫入后到期,從上次寫入發生之后的過期時間
自定義策略:到期時間由實現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<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 Set<String> getCacheNames() {
 return cacheNames;
 }
 
 public void setCacheNames(Set<String> 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<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.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> T get(Object key, Callable<T> 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<Object> 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<String, Cache> cacheMap = new ConcurrentHashMap<String, Cache>();
  
 private CacheRedisCaffeineProperties cacheRedisCaffeineProperties;
  
 private RedisTemplate<Object, Object> redisTemplate;
 
 private boolean dynamic = true;
 
 private Set<String> cacheNames;
 
 public RedisCaffeineCacheManager(CacheRedisCaffeineProperties cacheRedisCaffeineProperties,
  RedisTemplate<Object, Object> 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<Object, Object> caffeineCache(){
 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() {
 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;
 
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;

public class CacheMessageListener implements MessageListener {
 private final Logger logger = LoggerFactory.getLogger(CacheMessageListener.class);
 private RedisTemplate<Object, Object> redisTemplate;
 private RedisCaffeineCacheManager redisCaffeineCacheManager;
 public CacheMessageListener(RedisTemplate<Object, 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());
 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;

@Configuration
@AutoConfigureAfter(RedisAutoConfiguration.class)
@EnableConfigurationProperties(CacheRedisCaffeineProperties.class)
public class CacheRedisCaffeineAutoConfiguration {
  
 @Autowired
 private CacheRedisCaffeineProperties cacheRedisCaffeineProperties;
  
 @Bean
 @ConditionalOnBean(RedisTemplate.class)
 public RedisCaffeineCacheManager cacheManager(RedisTemplate<Object, Object> redisTemplate) {
 return new RedisCaffeineCacheManager(cacheRedisCaffeineProperties, redisTemplate);
 }
  
 @Bean
 public RedisMessageListenerContainer redisMessageListenerContainer(RedisTemplate<Object, Object> 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引入使用了

<dependency>
  <groupId>com.itopener</groupId>
  <artifactId>cache-redis-caffeine-spring-boot-starter</artifactId>
  <version>1.0.0-SNAPSHOT</version>
  <type>pom</type>
</dependency>

 

在啟動類上增加@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的封裝更方便一些

對於spring cache緩存的實現沒有那么多的缺陷
使用redis的HASH結構,可以針對不同的hashKey設置過期時間,清理的時候會更方便
如果基於redisson來實現多級緩存,可以繼承RedissonCache,在對應方法增加一級緩存的操作即可
如果有使用分布式鎖的情況就更方便了,可以直接使用Redisson中封裝的分布式鎖
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

以上就是本文的全部內容,希望對大家的學習有所幫助,也希望大家多多支持腳本之家。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM