spring boot+spring cache實(shí)現(xiàn)兩級(jí)緩存(redis+caffeine)
spring boot中集成了spring cache,并有多種緩存方式的實(shí)現(xiàn),如:Redis、Caffeine、JCache、EhCache等等。但如果只用一種緩存,要么會(huì)有較大的網(wǎng)絡(luò)消耗(如Redis),要么就是內(nèi)存占用太大(如Caffeine這種應(yīng)用內(nèi)存緩存)。在很多場(chǎng)景下,可以結(jié)合起來(lái)實(shí)現(xiàn)一、二級(jí)緩存的方式,能夠很大程度提高應(yīng)用的處理效率。
內(nèi)容說(shuō)明:
- 緩存、兩級(jí)緩存
- spring cache:主要包含spring cache定義的接口方法說(shuō)明和注解中的屬性說(shuō)明
- spring boot + spring cache:RedisCache實(shí)現(xiàn)中的缺陷
- caffeine簡(jiǎn)介
- spring boot + spring cache 實(shí)現(xiàn)兩級(jí)緩存(redis + caffeine)
緩存、兩級(jí)緩存
簡(jiǎn)單的理解,緩存就是將數(shù)據(jù)從讀取較慢的介質(zhì)上讀取出來(lái)放到讀取較快的介質(zhì)上,如磁盤-->內(nèi)存。平時(shí)我們會(huì)將數(shù)據(jù)存儲(chǔ)到磁盤上,如:數(shù)據(jù)庫(kù)。如果每次都從數(shù)據(jù)庫(kù)里去讀取,會(huì)因?yàn)榇疟P本身的IO影響讀取速度,所以就有了像redis這種的內(nèi)存緩存。可以將數(shù)據(jù)讀取出來(lái)放到內(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í),就可以直接使用,而不用通過(guò)網(wǎng)絡(luò)到redis中去獲取,這樣就形成了兩級(jí)緩存。應(yīng)用內(nèi)緩存叫做一級(jí)緩存,遠(yuǎn)程緩存(如redis)叫做二級(jí)緩存
spring cache
當(dāng)使用緩存的時(shí)候,一般是如下的流程:

從流程圖中可以看出,為了使用緩存,在原有業(yè)務(wù)處理的基礎(chǔ)上,增加了很多對(duì)于緩存的操作,如果將這些耦合到業(yè)務(wù)代碼當(dāng)中,開發(fā)起來(lái)就有很多重復(fù)性的工作,并且不太利于根據(jù)代碼去理解業(yè)務(wù)。
spring cache是spring-context包中提供的基于注解方式使用的緩存組件,定義了一些標(biāo)準(zhǔn)接口,通過(guò)實(shí)現(xiàn)這些接口,就可以通過(guò)在方法上增加注解來(lái)實(shí)現(xiàn)緩存。這樣就能夠避免緩存代碼與業(yè)務(wù)處理耦合在一起的問(wèn)題。spring cache的實(shí)現(xiàn)是使用spring aop中對(duì)方法切面(MethodInterceptor)封裝的擴(kuò)展,當(dāng)然spring aop也是基于Aspect來(lái)實(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<Object, Object>。暫時(shí)沒(méi)發(fā)現(xiàn)實(shí)際用處,可能只是提供獲取原生緩存的bean,以便需要擴(kuò)展一些緩存操作或統(tǒng)計(jì)之類的東西
Object getNativeCache();
// 通過(guò)key獲取緩存值,注意返回的是ValueWrapper,為了兼容存儲(chǔ)空值的情況,將返回值包裝了一層,通過(guò)get方法獲取實(shí)際值
ValueWrapper get(Object key);
// 通過(guò)key獲取緩存值,返回的是實(shí)際值,即方法的返回值類型
<T> T get(Object key, Class<T> type);
// 通過(guò)key獲取緩存值,可以使用valueLoader.call()來(lái)調(diào)使用@Cacheable注解的方法。當(dāng)@Cacheable注解的sync屬性配置為true時(shí)使用此方法。因此方法內(nèi)需要保證回源到數(shù)據(jù)庫(kù)的同步性。避免在緩存失效時(shí)大量請(qǐng)求回源到數(shù)據(jù)庫(kù)
<T> T get(Object key, Callable<T> valueLoader);
// 將@Cacheable注解方法返回的數(shù)據(jù)放入緩存中
void put(Object key, Object value);
// 當(dāng)緩存中不存在key時(shí)才放入緩存。返回值是當(dāng)key存在時(shí)原有的數(shù)據(jù)
ValueWrapper putIfAbsent(Object key, Object value);
// 刪除緩存
void evict(Object key);
// 刪除緩存中的所有數(shù)據(jù)。需要注意的是,具體實(shí)現(xiàn)中只刪除使用@Cacheable注解緩存的所有數(shù)據(jù),不要影響應(yīng)用內(nèi)的其他緩存
void clear();
// 緩存返回值的包裝
interface ValueWrapper {
// 返回實(shí)際緩存的對(duì)象
Object get();
}
// 當(dāng){@link #get(Object, Callable)}拋出異常時(shí),會(huì)包裝成此異常拋出
@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實(shí)現(xiàn)bean的創(chuàng)建,每個(gè)應(yīng)用里可以通過(guò)cacheName來(lái)對(duì)Cache進(jìn)行隔離,每個(gè)cacheName對(duì)應(yīng)一個(gè)Cache實(shí)現(xiàn)。spring框架中默認(rèn)提供的實(shí)現(xiàn)與Cache的實(shí)現(xiàn)都是成對(duì)出現(xiàn),包結(jié)構(gòu)也在上圖中
#CacheManager.java
package org.springframework.cache;
import java.util.Collection;
public interface CacheManager {
// 通過(guò)cacheName創(chuàng)建Cache的實(shí)現(xiàn)bean,具體實(shí)現(xiàn)中需要存儲(chǔ)已創(chuàng)建的Cache實(shí)現(xiàn)bean,避免重復(fù)創(chuàng)建,也避免內(nèi)存緩存對(duì)象(如Caffeine)重新創(chuàng)建后原來(lái)緩存內(nèi)容丟失的情況
Cache getCache(String name);
// 返回所有的cacheName
Collection<String> getCacheNames();
}
常用注解說(shuō)明
@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就是通過(guò)這個(gè)名稱創(chuàng)建對(duì)應(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包裝后的對(duì)象(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 {
// ...相同屬性說(shuō)明請(qǐng)參考@Cacheable中的說(shuō)明
// 是否要清除所有緩存的數(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:放入緩存,主要用到對(duì)數(shù)據(jù)有更新的方法上。屬性說(shuō)明參考@Cacheable
- @Caching:用于在一個(gè)方法上配置多種注解
- @EnableCaching:?jiǎn)⒂胹pring cache緩存,作為總的開關(guān),在spring boot的啟動(dòng)類或配置類上需要加上此注解才會(huì)生效
spring boot + spring cache
spring boot中已經(jīng)整合了spring cache,并且提供了多種緩存的配置,在使用時(shí)只需要配置使用哪個(gè)緩存(enum CacheType)即可。

spring boot中多增加了一個(gè)可以擴(kuò)展的東西,就是CacheManagerCustomizer接口,可以自定義實(shí)現(xiàn)這個(gè)接口,然后對(duì)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<RedisCacheManager> {
@Override
public void customize(RedisCacheManager cacheManager) {
// 默認(rèn)過(guò)期時(shí)間,單位秒
cacheManager.setDefaultExpiration(1000);
cacheManager.setUsePrefix(false);
Map<String, Long> expires = new ConcurrentHashMap<String, Long>();
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對(duì)于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存在后緩存失效了,再去獲取緩存是沒(méi)有數(shù)據(jù)的,就返回null了。
2.RedisCacheManager中是否允許存儲(chǔ)空值的屬性(cacheNullValues)默認(rèn)為false,即不允許存儲(chǔ)空值,這樣會(huì)存在緩存穿透的風(fēng)險(xiǎn)。缺陷是這個(gè)屬性是final類型的,只能在創(chuàng)建對(duì)象是通過(guò)構(gòu)造方法傳入,所以要避免緩存穿透就只能自己在應(yīng)用內(nèi)聲明RedisCacheManager這個(gè)bean了
3.RedisCacheManager中的屬性無(wú)法通過(guò)配置文件直接配置,只能在應(yīng)用內(nèi)實(shí)現(xiàn)CacheManagerCustomizer接口來(lái)進(jìn)行設(shè)置,個(gè)人認(rèn)為不太方便
Caffeine
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ù),對(duì)緩存的優(yōu)化能夠提供很大幫助
caffeine的介紹可以參考:http://www.dbjr.com.cn/article/134242.htm
這里簡(jiǎn)單說(shuō)下caffeine基于時(shí)間的回收策略有以下幾種:
- expireAfterAccess:訪問(wèn)后到期,從上次讀或?qū)懓l(fā)生后的過(guò)期時(shí)間
- expireAfterWrite:寫入后到期,從上次寫入發(fā)生之后的過(guò)期時(shí)間
- 自定義策略:到期時(shí)間由實(shí)現(xiàn)Expiry接口后單獨(dú)計(jì)算
spring boot + spring cache 實(shí)現(xiàn)兩級(jí)緩存(redis + caffeine)
本人開頭提到了,就算是使用了redis緩存,也會(huì)存在一定程度的網(wǎng)絡(luò)傳輸上的消耗,在實(shí)際應(yīng)用當(dāng)中,會(huì)存在一些變更頻率非常低的數(shù)據(jù),就可以直接緩存在應(yīng)用內(nèi)部,對(duì)于一些實(shí)時(shí)性要求不太高的數(shù)據(jù),也可以在應(yīng)用內(nèi)部緩存一定時(shí)間,減少對(duì)redis的訪問(wèn),提高響應(yīng)速度
由于spring-data-redis框架中redis對(duì)spring cache的實(shí)現(xiàn)有一些不足,在使用起來(lái)可能會(huì)出現(xiàn)一些問(wèn)題,所以就不基于原來(lái)的實(shí)現(xiàn)去擴(kuò)展了,直接參考實(shí)現(xiàn)方式,去實(shí)現(xiàn)Cache和CacheManager接口
還需要注意一點(diǎn),一般應(yīng)用都部署了多個(gè)節(jié)點(diǎn),一級(jí)緩存是在應(yīng)用內(nèi)的緩存,所以當(dāng)對(duì)數(shù)據(jù)更新和清除時(shí),需要通知所有節(jié)點(diǎn)進(jìn)行清理緩存的操作。可以有多種方式來(lái)實(shí)現(xiàn)這種效果,比如:zookeeper、MQ等,但是既然用了redis緩存,redis本身是有支持訂閱/發(fā)布功能的,所以就不依賴其他組件了,直接使用redis的通道來(lái)通知其他節(jié)點(diǎn)進(jìn)行清理緩存的操作
以下就是對(duì)spring boot + spring cache實(shí)現(xiàn)兩級(jí)緩存(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<>();
/** 是否存儲(chǔ)空值,默認(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 {
/** 全局過(guò)期時(shí)間,單位毫秒,默認(rèn)不過(guò)期*/
private long defaultExpiration = 0;
/** 每個(gè)cacheName的過(guò)期時(shí)間,單位毫秒,優(yōu)先級(jí)比defaultExpiration高*/
private Map<String, Long> 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<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 {
/** 訪問(wèn)后過(guò)期時(shí)間,單位毫秒*/
private long expireAfterAccess;
/** 寫入后過(guò)期時(shí)間,單位毫秒*/
private long expireAfterWrite;
/** 寫入后刷新時(shí)間,單位毫秒*/
private long refreshAfterWrite;
/** 初始化大小*/
private int initialCapacity;
/** 最大緩存對(duì)象個(gè)數(shù),超過(guò)此數(shù)量時(shí)之前放入的緩存將失效*/
private long maximumSize;
/** 由于權(quán)重需要緩存對(duì)象來(lái)提供,對(duì)于使用spring cache這種場(chǎng)景不是很適合,所以暫不支持配置*/
// 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中有實(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<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中的緩存,避免短時(shí)間內(nèi)如果先清除caffeine緩存后其他請(qǐng)求會(huì)再?gòu)膔edis里加載到caffeine中
redisTemplate.delete(getKey(key));
push(new CacheMessage(this.name, key));
caffeineCache.invalidate(key);
}
@Override
public void clear() {
// 先清除redis中緩存數(shù)據(jù),然后清除caffeine中的緩存,避免短時(shí)間內(nèi)如果先清除caffeine緩存后其他請(qǐng)求會(huì)再?gòu)膔edis里加載到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 緩存變更時(shí)通知其他節(jié)點(diǎn)清理本地緩存
* @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);
}
}
}
實(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 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消息需要實(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 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
接下來(lái)就可以使用maven引入使用了
<dependency> <groupId>com.itopener</groupId> <artifactId>cache-redis-caffeine-spring-boot-starter</artifactId> <version>1.0.0-SNAPSHOT</version> <type>pom</type> </dependency>
在啟動(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是一樣的,可以增加兩級(jí)緩存的配置
#兩級(jí)緩存的配置 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的封裝更方便一些
- 對(duì)于spring cache緩存的實(shí)現(xiàn)沒(méi)有那么多的缺陷
- 使用redis的HASH結(jié)構(gòu),可以針對(duì)不同的hashKey設(shè)置過(guò)期時(shí)間,清理的時(shí)候會(huì)更方便
- 如果基于redisson來(lái)實(shí)現(xiàn)多級(jí)緩存,可以繼承RedissonCache,在對(duì)應(yīng)方法增加一級(jí)緩存的操作即可
- 如果有使用分布式鎖的情況就更方便了,可以直接使用Redisson中封裝的分布式鎖
- redisson中的發(fā)布訂閱封裝得更好用
后續(xù)可以增加對(duì)于緩存命中率的統(tǒng)計(jì)endpoint,這樣就可以更好的監(jiān)控各個(gè)緩存的命中情況,以便對(duì)緩存配置進(jìn)行優(yōu)化
starter目錄:springboot / itopener-parent / spring-boot-starters-parent / cache-redis-caffeine-spring-boot-starter-parent
示例代碼目錄: springboot / itopener-parent / demo-parent / demo-cache-redis-caffeine
以上就是本文的全部?jī)?nèi)容,希望對(duì)大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
相關(guān)文章
Swing圖形界面實(shí)現(xiàn)可動(dòng)態(tài)刷新的驗(yàn)證碼
這篇文章主要為大家詳細(xì)介紹了Swing圖形界面實(shí)現(xiàn)可動(dòng)態(tài)刷新的驗(yàn)證碼,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-05-05
基于logback 實(shí)現(xiàn)springboot超級(jí)詳細(xì)的日志配置
java web 下有好幾種日志框架,比如:logback,log4j,log4j2(slj4f 并不是一種日志框架,它相當(dāng)于定義了規(guī)范,實(shí)現(xiàn)了這個(gè)規(guī)范的日志框架就能夠用 slj4f 調(diào)用)。這篇文章主要介紹了基于logback springboot超級(jí)詳細(xì)的日志配置,需要的朋友可以參考下2019-06-06
jdbc連接sqlserver數(shù)據(jù)庫(kù)示例
這篇文章主要介紹了jdbc連接sqlserver數(shù)據(jù)庫(kù)示例,需要的朋友可以參考下2014-04-04
java實(shí)現(xiàn)圖片上傳至本地實(shí)例詳解
我們給大家分享了關(guān)于java實(shí)現(xiàn)圖片上傳至本地的實(shí)例以及相關(guān)代碼,有需要的朋友參考下。2018-08-08
Java線程Timer定時(shí)器用法詳細(xì)總結(jié)
在本篇文章里小編給大家整理的是關(guān)于Java線程Timer定時(shí)器用法詳細(xì)總結(jié)內(nèi)容,需要的朋友們學(xué)習(xí)下吧。2020-02-02
Mybatis plus關(guān)閉駝峰命名的四種方法(防止出現(xiàn)查詢?yōu)镹ull)
這篇文章主要介紹了Mybatis plus關(guān)閉駝峰命名的四種方法(防止出現(xiàn)查詢?yōu)镹ull),數(shù)據(jù)庫(kù)的字段命名方式為使用下劃線連接,對(duì)應(yīng)的實(shí)體類應(yīng)該是駝峰命名方式,而我使用的是和數(shù)據(jù)庫(kù)同樣的命名方式,需要的朋友可以參考下2022-01-01
Java數(shù)據(jù)結(jié)構(gòu)與算法入門實(shí)例詳解
這篇文章主要介紹了Java數(shù)據(jù)結(jié)構(gòu)與算法入門實(shí)例詳解,本文給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2021-03-03

