這篇文章給大家分享的是有關(guān)spring boot+spring cache實(shí)現(xiàn)兩級緩存的內(nèi)容。小編覺得挺實(shí)用的,因此分享給大家做個(gè)參考,一起跟隨小編過來看看吧。
十載的觀山湖網(wǎng)站建設(shè)經(jīng)驗(yàn),針對設(shè)計(jì)、前端、開發(fā)、售后、文案、推廣等六對一服務(wù),響應(yīng)快,48小時(shí)及時(shí)工作處理。營銷型網(wǎng)站的優(yōu)勢是能夠根據(jù)用戶設(shè)備顯示端的尺寸不同,自動(dòng)調(diào)整觀山湖建站的顯示方式,使網(wǎng)站能夠適用不同顯示終端,在瀏覽器中調(diào)整網(wǎng)站的寬度,無論在任何一種瀏覽器上瀏覽網(wǎng)站,都能展現(xiàn)優(yōu)雅布局與設(shè)計(jì),從而大程度地提升瀏覽體驗(yàn)。創(chuàng)新互聯(lián)建站從事“觀山湖網(wǎng)站設(shè)計(jì)”,“觀山湖網(wǎng)站推廣”以來,每個(gè)客戶項(xiàng)目都認(rèn)真落實(shí)執(zhí)行。
spring boot中集成了spring cache,并有多種緩存方式的實(shí)現(xiàn),如:redis、Caffeine、JCache、EhCache等等。但如果只用一種緩存,要么會(huì)有較大的網(wǎng)絡(luò)消耗(如Redis),要么就是內(nèi)存占用太大(如Caffeine這種應(yīng)用內(nèi)存緩存)。在很多場景下,可以結(jié)合起來實(shí)現(xiàn)一、二級緩存的方式,能夠很大程度提高應(yīng)用的處理效率。
內(nèi)容說明:
緩存、兩級緩存
spring cache:主要包含spring cache定義的接口方法說明和注解中的屬性說明
spring boot + spring cache:RedisCache實(shí)現(xiàn)中的缺陷
caffeine簡介
spring boot + spring cache 實(shí)現(xiàn)兩級緩存(redis + caffeine)
簡單的理解,緩存就是將數(shù)據(jù)從讀取較慢的介質(zhì)上讀取出來放到讀取較快的介質(zhì)上,如磁盤-->內(nèi)存。平時(shí)我們會(huì)將數(shù)據(jù)存儲到磁盤上,如:數(shù)據(jù)庫。如果每次都從數(shù)據(jù)庫里去讀取,會(huì)因?yàn)榇疟P本身的IO影響讀取速度,所以就有了像redis這種的內(nèi)存緩存。可以將數(shù)據(jù)讀取出來放到內(nèi)存里,這樣當(dāng)需要獲取數(shù)據(jù)時(shí),就能夠直接從內(nèi)存中拿到數(shù)據(jù)返回,能夠很大程度的提高速度。但是一般redis是單獨(dú)部署成集群,所以會(huì)有網(wǎng)絡(luò)IO上的消耗,雖然與redis集群的鏈接已經(jīng)有連接池這種工具,但是數(shù)據(jù)傳輸上也還是會(huì)有一定消耗。所以就有了應(yīng)用內(nèi)緩存,如:caffeine。當(dāng)應(yīng)用內(nèi)緩存有符合條件的數(shù)據(jù)時(shí),就可以直接使用,而不用通過網(wǎng)絡(luò)到redis中去獲取,這樣就形成了兩級緩存。應(yīng)用內(nèi)緩存叫做一級緩存,遠(yuǎn)程緩存(如redis)叫做二級緩存
spring cache
當(dāng)使用緩存的時(shí)候,一般是如下的流程:
從流程圖中可以看出,為了使用緩存,在原有業(yè)務(wù)處理的基礎(chǔ)上,增加了很多對于緩存的操作,如果將這些耦合到業(yè)務(wù)代碼當(dāng)中,開發(fā)起來就有很多重復(fù)性的工作,并且不太利于根據(jù)代碼去理解業(yè)務(wù)。
spring cache是spring-context包中提供的基于注解方式使用的緩存組件,定義了一些標(biāo)準(zhǔn)接口,通過實(shí)現(xiàn)這些接口,就可以通過在方法上增加注解來實(shí)現(xiàn)緩存。這樣就能夠避免緩存代碼與業(yè)務(wù)處理耦合在一起的問題。spring cache的實(shí)現(xiàn)是使用spring aop中對方法切面(MethodInterceptor)封裝的擴(kuò)展,當(dāng)然spring aop也是基于Aspect來實(shí)現(xiàn)的。
spring cache核心的接口就兩個(gè):Cache和CacheManager
Cache接口
提供緩存的具體操作,比如緩存的放入、讀取、清理,spring框架中默認(rèn)提供的實(shí)現(xiàn)有:
除了RedisCache是在spring-data-redis包中,其他的基本都是在spring-context-support包中
#Cache.java package org.springframework.cache; import java.util.concurrent.Callable; public interface Cache { // cacheName,緩存的名字,默認(rèn)實(shí)現(xiàn)中一般是CacheManager創(chuàng)建Cache的bean時(shí)傳入cacheName String getName(); // 獲取實(shí)際使用的緩存,如:RedisTemplate、com.github.benmanes.caffeine.cache.Cache
CacheManager接口
主要提供Cache實(shí)現(xiàn)bean的創(chuàng)建,每個(gè)應(yīng)用里可以通過cacheName來對Cache進(jìn)行隔離,每個(gè)cacheName對應(yīng)一個(gè)Cache實(shí)現(xiàn)。spring框架中默認(rèn)提供的實(shí)現(xiàn)與Cache的實(shí)現(xiàn)都是成對出現(xiàn),包結(jié)構(gòu)也在上圖中
#CacheManager.java package org.springframework.cache; import java.util.Collection; public interface CacheManager { // 通過cacheName創(chuàng)建Cache的實(shí)現(xiàn)bean,具體實(shí)現(xiàn)中需要存儲已創(chuàng)建的Cache實(shí)現(xiàn)bean,避免重復(fù)創(chuàng)建,也避免內(nèi)存緩存對象(如Caffeine)重新創(chuàng)建后原來緩存內(nèi)容丟失的情況 Cache getCache(String name); // 返回所有的cacheName CollectiongetCacheNames(); }
常用注解說明
@Cacheable:主要應(yīng)用到查詢數(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就是通過這個(gè)名稱創(chuàng)建對應(yīng)的Cache實(shí)現(xiàn)bean @AliasFor("cacheNames") String[] value() default {}; @AliasFor("value") String[] cacheNames() default {}; // 緩存的key,支持SpEL表達(dá)式。默認(rèn)是使用所有參數(shù)及其計(jì)算的hashCode包裝后的對象(SimpleKey) String key() default ""; // 緩存key生成器,默認(rèn)實(shí)現(xiàn)是SimpleKeyGenerator String keyGenerator() default ""; // 指定使用哪個(gè)CacheManager String cacheManager() default ""; // 緩存解析器 String cacheResolver() default ""; // 緩存的條件,支持SpEL表達(dá)式,當(dāng)達(dá)到滿足的條件時(shí)才緩存數(shù)據(jù)。在調(diào)用方法前后都會(huì)判斷 String condition() default ""; // 滿足條件時(shí)不更新緩存,支持SpEL表達(dá)式,只在調(diào)用方法后判斷 String unless() default ""; // 回源到實(shí)際方法獲取數(shù)據(jù)時(shí),是否要保持同步,如果為false,調(diào)用的是Cache.get(key)方法;如果為true,調(diào)用的是Cache.get(key, Callable)方法 boolean sync() default false; }
@CacheEvict:清除緩存,主要應(yīng)用到刪除數(shù)據(jù)的方法上。相比Cacheable多了兩個(gè)屬性
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時(shí)調(diào)用的是Cache.evict(key)方法;為true時(shí)調(diào)用的是Cache.clear()方法 boolean allEntries() default false; // 調(diào)用方法之前或之后清除緩存 boolean beforeInvocation() default false; }
@CachePut:放入緩存,主要用到對數(shù)據(jù)有更新的方法上。屬性說明參考@Cacheable
@Caching:用于在一個(gè)方法上配置多種注解
@EnableCaching:啟用spring cache緩存,作為總的開關(guān),在spring boot的啟動(dòng)類或配置類上需要加上此注解才會(huì)生效
spring boot中已經(jīng)整合了spring cache,并且提供了多種緩存的配置,在使用時(shí)只需要配置使用哪個(gè)緩存(enum CacheType)即可。
spring boot中多增加了一個(gè)可以擴(kuò)展的東西,就是CacheManagerCustomizer接口,可以自定義實(shí)現(xiàn)這個(gè)接口,然后對CacheManager做一些設(shè)置,比如:
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) { // 默認(rèn)過期時(shí)間,單位秒 cacheManager.setDefaultExpiration(1000); cacheManager.setUsePrefix(false); Map expires = new ConcurrentHashMap (); expires.put("userIdCache", 2000L); cacheManager.setExpires(expires); } }
加載這個(gè)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接口的實(shí)現(xiàn)是在spring-data-redis包中
這里提下我認(rèn)為的RedisCache實(shí)現(xiàn)中的缺陷:
1.在緩存失效的瞬間,如果有線程獲取緩存數(shù)據(jù),可能出現(xiàn)返回null的情況,原因是RedisCache實(shí)現(xiàn)中是如下步驟:
判斷緩存key是否存在
如果key存在,再獲取緩存數(shù)據(jù),并返回
因此當(dāng)判斷key存在后緩存失效了,再去獲取緩存是沒有數(shù)據(jù)的,就返回null了。
2.RedisCacheManager中是否允許存儲空值的屬性(cacheNullValues)默認(rèn)為false,即不允許存儲空值,這樣會(huì)存在緩存穿透的風(fēng)險(xiǎn)。缺陷是這個(gè)屬性是final類型的,只能在創(chuàng)建對象是通過構(gòu)造方法傳入,所以要避免緩存穿透就只能自己在應(yīng)用內(nèi)聲明RedisCacheManager這個(gè)bean了
3.RedisCacheManager中的屬性無法通過配置文件直接配置,只能在應(yīng)用內(nèi)實(shí)現(xiàn)CacheManagerCustomizer接口來進(jìn)行設(shè)置,個(gè)人認(rèn)為不太方便
Caffeine是一個(gè)基于Google開源的Guava設(shè)計(jì)理念的一個(gè)高性能內(nèi)存緩存,使用java8開發(fā),spring boot引入Caffeine后已經(jīng)逐步廢棄Guava的整合了。Caffeine源碼及介紹地址:caffeine
caffeine提供了多種緩存填充策略、值回收策略,同時(shí)也包含了緩存命中次數(shù)等統(tǒng)計(jì)數(shù)據(jù),對緩存的優(yōu)化能夠提供很大幫助
caffeine的介紹可以參考:https://www.jb51.net/article/134242.htm
這里簡單說下caffeine基于時(shí)間的回收策略有以下幾種:
expireAfterAccess:訪問后到期,從上次讀或?qū)懓l(fā)生后的過期時(shí)間
expireAfterWrite:寫入后到期,從上次寫入發(fā)生之后的過期時(shí)間
自定義策略:到期時(shí)間由實(shí)現(xiàn)Expiry接口后單獨(dú)計(jì)算
本人開頭提到了,就算是使用了redis緩存,也會(huì)存在一定程度的網(wǎng)絡(luò)傳輸上的消耗,在實(shí)際應(yīng)用當(dāng)中,會(huì)存在一些變更頻率非常低的數(shù)據(jù),就可以直接緩存在應(yīng)用內(nèi)部,對于一些實(shí)時(shí)性要求不太高的數(shù)據(jù),也可以在應(yīng)用內(nèi)部緩存一定時(shí)間,減少對redis的訪問,提高響應(yīng)速度
由于spring-data-redis框架中redis對spring cache的實(shí)現(xiàn)有一些不足,在使用起來可能會(huì)出現(xiàn)一些問題,所以就不基于原來的實(shí)現(xiàn)去擴(kuò)展了,直接參考實(shí)現(xiàn)方式,去實(shí)現(xiàn)Cache和CacheManager接口
還需要注意一點(diǎn),一般應(yīng)用都部署了多個(gè)節(jié)點(diǎn),一級緩存是在應(yīng)用內(nèi)的緩存,所以當(dāng)對數(shù)據(jù)更新和清除時(shí),需要通知所有節(jié)點(diǎn)進(jìn)行清理緩存的操作??梢杂卸喾N方式來實(shí)現(xiàn)這種效果,比如:zookeeper、MQ等,但是既然用了redis緩存,redis本身是有支持訂閱/發(fā)布功能的,所以就不依賴其他組件了,直接使用redis的通道來通知其他節(jié)點(diǎn)進(jìn)行清理緩存的操作
以下就是對spring boot + spring cache實(shí)現(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 SetcacheNames = new HashSet<>(); /** 是否存儲空值,默認(rèn)true,防止緩存穿透*/ private boolean cacheNullValues = true; /** 是否動(dòng)態(tài)根據(jù)cacheName創(chuàng)建Cache的實(shí)現(xiàn),默認(rèn)true*/ private boolean dynamic = true; /** 緩存key的前綴*/ private String cachePrefix; private Redis redis = new Redis(); private Caffeine caffeine = new Caffeine(); public class Redis { /** 全局過期時(shí)間,單位毫秒,默認(rèn)不過期*/ private long defaultExpiration = 0; /** 每個(gè)cacheName的過期時(shí)間,單位毫秒,優(yōu)先級比defaultExpiration高*/ private Map expires = new HashMap<>(); /** 緩存更新時(shí)通知其他節(jié)點(diǎn)的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 { /** 訪問后過期時(shí)間,單位毫秒*/ private long expireAfterAccess; /** 寫入后過期時(shí)間,單位毫秒*/ private long expireAfterWrite; /** 寫入后刷新時(shí)間,單位毫秒*/ private long refreshAfterWrite; /** 初始化大小*/ private int initialCapacity; /** 最大緩存對象個(gè)數(shù),超過此數(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 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中有實(shí)現(xiàn)Cache接口的一個(gè)抽象類AbstractValueAdaptingCache,包含了空值的包裝和緩存值的包裝,所以就不用實(shí)現(xiàn)Cache接口了,直接實(shí)現(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
實(shí)現(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 ConcurrentMapcacheMap = 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消息發(fā)布/訂閱,傳輸?shù)南㈩?/p>
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消息需要實(shí)現(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 RedisTemplateredisTemplate; 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(RedisTemplateredisTemplate) { 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
在啟動(dòng)類上增加@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
擴(kuò)展
個(gè)人認(rèn)為redisson的封裝更方便一些
對于spring cache緩存的實(shí)現(xiàn)沒有那么多的缺陷
使用redis的HASH結(jié)構(gòu),可以針對不同的hashKey設(shè)置過期時(shí)間,清理的時(shí)候會(huì)更方便
如果基于redisson來實(shí)現(xiàn)多級緩存,可以繼承RedissonCache,在對應(yīng)方法增加一級緩存的操作即可
如果有使用分布式鎖的情況就更方便了,可以直接使用Redisson中封裝的分布式鎖
redisson中的發(fā)布訂閱封裝得更好用
感謝各位的閱讀!關(guān)于“spring boot+spring cache實(shí)現(xiàn)兩級緩存”這篇文章就分享到這里了,希望以上內(nèi)容可以對大家有一定的幫助,讓大家可以學(xué)到更多知識,如果覺得文章不錯(cuò),可以把它分享出去讓更多的人看到吧!