1. 程式人生 > 實用技巧 >SpringBoot+SpringCache實現兩級快取(Redis+Caffeine)

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

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

您可能感興趣的文章: