溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務條款》

spring boot+spring cache實現(xiàn)兩級緩存

發(fā)布時間:2021-08-06 11:24:34 來源:億速云 閱讀:385 作者:小新 欄目:編程語言

這篇文章給大家分享的是有關(guān)spring boot+spring cache實現(xiàn)兩級緩存的內(nèi)容。小編覺得挺實用的,因此分享給大家做個參考,一起跟隨小編過來看看吧。

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

內(nèi)容說明:

  1. 緩存、兩級緩存

  2. spring cache:主要包含spring cache定義的接口方法說明和注解中的屬性說明

  3. spring boot + spring cache:RedisCache實現(xiàn)中的缺陷

  4. caffeine簡介

  5. spring boot + spring cache 實現(xiàn)兩級緩存(redis + caffeine)

緩存、兩級緩存

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

spring cache

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

spring boot+spring cache實現(xiàn)兩級緩存

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

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

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

spring boot+spring cache實現(xiàn)兩級緩存

Cache接口

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

spring boot+spring cache實現(xiàn)兩級緩存

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

spring boot+spring cache實現(xiàn)兩級緩存

#Cache.java

package org.springframework.cache;

import java.util.concurrent.Callable;

public interface Cache {

 // cacheName,緩存的名字,默認實現(xiàn)中一般是CacheManager創(chuàng)建Cache的bean時傳入cacheName
 String getName();

 // 獲取實際使用的緩存,如:RedisTemplate、com.github.benmanes.caffeine.cache.Cache<Object, Object>。暫時沒發(fā)現(xiàn)實際用處,可能只是提供獲取原生緩存的bean,以便需要擴展一些緩存操作或統(tǒng)計之類的東西
 Object getNativeCache();

 // 通過key獲取緩存值,注意返回的是ValueWrapper,為了兼容存儲空值的情況,將返回值包裝了一層,通過get方法獲取實際值
 ValueWrapper get(Object key);

 // 通過key獲取緩存值,返回的是實際值,即方法的返回值類型
 <T> T get(Object key, Class<T> type);

 // 通過key獲取緩存值,可以使用valueLoader.call()來調(diào)使用@Cacheable注解的方法。當@Cacheable注解的sync屬性配置為true時使用此方法。因此方法內(nèi)需要保證回源到數(shù)據(jù)庫的同步性。避免在緩存失效時大量請求回源到數(shù)據(jù)庫
 <T> T get(Object key, Callable<T> valueLoader);

 // 將@Cacheable注解方法返回的數(shù)據(jù)放入緩存中
 void put(Object key, Object value);

 // 當緩存中不存在key時才放入緩存。返回值是當key存在時原有的數(shù)據(jù)
 ValueWrapper putIfAbsent(Object key, Object value);

 // 刪除緩存
 void evict(Object key);

 // 刪除緩存中的所有數(shù)據(jù)。需要注意的是,具體實現(xiàn)中只刪除使用@Cacheable注解緩存的所有數(shù)據(jù),不要影響應用內(nèi)的其他緩存
 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實現(xiàn)bean的創(chuàng)建,每個應用里可以通過cacheName來對Cache進行隔離,每個cacheName對應一個Cache實現(xiàn)。spring框架中默認提供的實現(xiàn)與Cache的實現(xiàn)都是成對出現(xiàn),包結(jié)構(gòu)也在上圖中

#CacheManager.java

package org.springframework.cache;

import java.util.Collection;

public interface CacheManager {

 // 通過cacheName創(chuàng)建Cache的實現(xiàn)bean,具體實現(xiàn)中需要存儲已創(chuàng)建的Cache實現(xiàn)bean,避免重復創(chuàng)建,也避免內(nèi)存緩存對象(如Caffeine)重新創(chuàng)建后原來緩存內(nèi)容丟失的情況
 Cache getCache(String name);

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

常用注解說明

@Cacheable:主要應用到查詢數(shù)據(jù)的方法上

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就是通過這個名稱創(chuàng)建對應的Cache實現(xiàn)bean
 @AliasFor("cacheNames")
 String[] value() default {};

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

    // 緩存的key,支持SpEL表達式。默認是使用所有參數(shù)及其計算的hashCode包裝后的對象(SimpleKey)
 String key() default "";

 // 緩存key生成器,默認實現(xiàn)是SimpleKeyGenerator
 String keyGenerator() default "";

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

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

 // 緩存的條件,支持SpEL表達式,當達到滿足的條件時才緩存數(shù)據(jù)。在調(diào)用方法前后都會判斷
 String condition() default "";
    
    // 滿足條件時不更新緩存,支持SpEL表達式,只在調(diào)用方法后判斷
 String unless() default "";

 // 回源到實際方法獲取數(shù)據(jù)時,是否要保持同步,如果為false,調(diào)用的是Cache.get(key)方法;如果為true,調(diào)用的是Cache.get(key, Callable)方法
 boolean sync() default false;
}

@CacheEvict:清除緩存,主要應用到刪除數(shù)據(jù)的方法上。相比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中的說明

 // 是否要清除所有緩存的數(shù)據(jù),為false時調(diào)用的是Cache.evict(key)方法;為true時調(diào)用的是Cache.clear()方法
 boolean allEntries() default false;

 // 調(diào)用方法之前或之后清除緩存
 boolean beforeInvocation() default false;
}
  1. @CachePut:放入緩存,主要用到對數(shù)據(jù)有更新的方法上。屬性說明參考@Cacheable

  2. @Caching:用于在一個方法上配置多種注解

  3. @EnableCaching:啟用spring cache緩存,作為總的開關(guān),在spring boot的啟動類或配置類上需要加上此注解才會生效

spring boot + spring cache

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

spring boot+spring cache實現(xiàn)兩級緩存

spring boot中多增加了一個可以擴展的東西,就是CacheManagerCustomizer接口,可以自定義實現(xiàn)這個接口,然后對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接口的實現(xiàn)是在spring-data-redis包中

spring boot+spring cache實現(xiàn)兩級緩存

這里提下我認為的RedisCache實現(xiàn)中的缺陷:

1.在緩存失效的瞬間,如果有線程獲取緩存數(shù)據(jù),可能出現(xiàn)返回null的情況,原因是RedisCache實現(xiàn)中是如下步驟:

  1. 判斷緩存key是否存在

  2. 如果key存在,再獲取緩存數(shù)據(jù),并返回

因此當判斷key存在后緩存失效了,再去獲取緩存是沒有數(shù)據(jù)的,就返回null了。

2.RedisCacheManager中是否允許存儲空值的屬性(cacheNullValues)默認為false,即不允許存儲空值,這樣會存在緩存穿透的風險。缺陷是這個屬性是final類型的,只能在創(chuàng)建對象是通過構(gòu)造方法傳入,所以要避免緩存穿透就只能自己在應用內(nèi)聲明RedisCacheManager這個bean了

3.RedisCacheManager中的屬性無法通過配置文件直接配置,只能在應用內(nèi)實現(xiàn)CacheManagerCustomizer接口來進行設置,個人認為不太方便

Caffeine

Caffeine是一個基于Google開源的Guava設計理念的一個高性能內(nèi)存緩存,使用java8開發(fā),spring boot引入Caffeine后已經(jīng)逐步廢棄Guava的整合了。Caffeine源碼及介紹地址:caffeine

caffeine提供了多種緩存填充策略、值回收策略,同時也包含了緩存命中次數(shù)等統(tǒng)計數(shù)據(jù),對緩存的優(yōu)化能夠提供很大幫助

caffeine的介紹可以參考:https://www.jb51.net/article/134242.htm

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

  1. expireAfterAccess:訪問后到期,從上次讀或?qū)懓l(fā)生后的過期時間

  2. expireAfterWrite:寫入后到期,從上次寫入發(fā)生之后的過期時間

  3. 自定義策略:到期時間由實現(xiàn)Expiry接口后單獨計算

spring boot + spring cache 實現(xiàn)兩級緩存(redis + caffeine)

本人開頭提到了,就算是使用了redis緩存,也會存在一定程度的網(wǎng)絡傳輸上的消耗,在實際應用當中,會存在一些變更頻率非常低的數(shù)據(jù),就可以直接緩存在應用內(nèi)部,對于一些實時性要求不太高的數(shù)據(jù),也可以在應用內(nèi)部緩存一定時間,減少對redis的訪問,提高響應速度

由于spring-data-redis框架中redis對spring cache的實現(xiàn)有一些不足,在使用起來可能會出現(xiàn)一些問題,所以就不基于原來的實現(xiàn)去擴展了,直接參考實現(xiàn)方式,去實現(xiàn)Cache和CacheManager接口

還需要注意一點,一般應用都部署了多個節(jié)點,一級緩存是在應用內(nèi)的緩存,所以當對數(shù)據(jù)更新和清除時,需要通知所有節(jié)點進行清理緩存的操作??梢杂卸喾N方式來實現(xiàn)這種效果,比如:zookeeper、MQ等,但是既然用了redis緩存,redis本身是有支持訂閱/發(fā)布功能的,所以就不依賴其他組件了,直接使用redis的通道來通知其他節(jié)點進行清理緩存的操作

以下就是對spring boot + spring cache實現(xiàn)兩級緩存(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; 
 /** 是否動態(tài)根據(jù)cacheName創(chuàng)建Cache的實現(xiàn),默認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的過期時間,單位毫秒,優(yōu)先級比defaultExpiration高*/
 private Map<String, Long> expires = new HashMap<>();
 
 /** 緩存更新時通知其他節(jié)點的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;
 
 /** 最大緩存對象個數(shù),超過此數(shù)量時之前放入的緩存將失效*/
 private long maximumSize;
 
 /** 由于權(quán)重需要緩存對象來提供,對于使用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中有實現(xiàn)Cache接口的一個抽象類AbstractValueAdaptingCache,包含了空值的包裝和緩存值的包裝,所以就不用實現(xiàn)Cache接口了,直接實現(xiàn)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中緩存數(shù)據(jù),然后清除caffeine中的緩存,避免短時間內(nèi)如果先清除caffeine緩存后其他請求會再從redis里加載到caffeine中
 redisTemplate.delete(getKey(key));
 
 push(new CacheMessage(this.name, key));
 
 caffeineCache.invalidate(key);
 }

 @Override
 public void clear() {
 // 先清除redis中緩存數(shù)據(jù),然后清除caffeine中的緩存,避免短時間內(nèi)如果先清除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 緩存變更時通知其他節(jié)點清理本地緩存
 * @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);
 }
 }
}

實現(xiàn)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消息發(fā)布/訂閱,傳輸?shù)南㈩?br/>

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

監(jiān)聽redis消息需要實現(xiàn)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<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;
/** 
 * @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<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的封裝更方便一些

  1. 對于spring cache緩存的實現(xiàn)沒有那么多的缺陷

  2. 使用redis的HASH結(jié)構(gòu),可以針對不同的hashKey設置過期時間,清理的時候會更方便

  3. 如果基于redisson來實現(xiàn)多級緩存,可以繼承RedissonCache,在對應方法增加一級緩存的操作即可

  4. 如果有使用分布式鎖的情況就更方便了,可以直接使用Redisson中封裝的分布式鎖

  5. redisson中的發(fā)布訂閱封裝得更好用

感謝各位的閱讀!關(guān)于“spring boot+spring cache實現(xiàn)兩級緩存”這篇文章就分享到這里了,希望以上內(nèi)容可以對大家有一定的幫助,讓大家可以學到更多知識,如果覺得文章不錯,可以把它分享出去讓更多的人看到吧!

向AI問一下細節(jié)

免責聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進行舉報,并提供相關(guān)證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI