還在用 Guava Cache?它才是 Java 本地緩存之王!
作者 | rickiyang
前面剛說(shuō)到Guava Cache,他的優(yōu)點(diǎn)是封裝了get,put操作;提供線程安全的緩存操作;提供過(guò)期策略;提供回收策略;緩存監(jiān)控。
當(dāng)緩存的數(shù)據(jù)超過(guò)最大值時(shí),使用LRU算法替換。這一篇我們將要談到一個(gè)新的本地緩存框架:Caffeine Cache。它也是站在巨人的肩膀上-Guava Cache,借著他的思想優(yōu)化了算法發(fā)展而來(lái)。
本篇博文主要介紹Caffine Cache 的使用方式,以及Caffine Cache在SpringBoot中的使用。
1. Caffine Cache 在算法上的優(yōu)點(diǎn)-W-TinyLFU
說(shuō)到優(yōu)化,Caffine Cache到底優(yōu)化了什么呢?我們剛提到過(guò)LRU,常見(jiàn)的緩存淘汰算法還有FIFO,LFU:
FIFO:先進(jìn)先出,在這種淘汰算法中,先進(jìn)入緩存的會(huì)先被淘汰,會(huì)導(dǎo)致命中率很低。
LRU:最近最少使用算法,每次訪問(wèn)數(shù)據(jù)都會(huì)將其放在我們的隊(duì)尾,如果需要淘汰數(shù)據(jù),就只需要淘汰隊(duì)首即可。仍然有個(gè)問(wèn)題,如果有個(gè)數(shù)據(jù)在 1 分鐘訪問(wèn)了 1000次,再后 1 分鐘沒(méi)有訪問(wèn)這個(gè)數(shù)據(jù),但是有其他的數(shù)據(jù)訪問(wèn),就導(dǎo)致了我們這個(gè)熱點(diǎn)數(shù)據(jù)被淘汰。
LFU:最近最少頻率使用,利用額外的空間記錄每個(gè)數(shù)據(jù)的使用頻率,然后選出頻率最低進(jìn)行淘汰。這樣就避免了 LRU 不能處理時(shí)間段的問(wèn)題。
上面三種策略各有利弊,實(shí)現(xiàn)的成本也是一個(gè)比一個(gè)高,同時(shí)命中率也是一個(gè)比一個(gè)好。Guava Cache雖然有這么多的功能,但是本質(zhì)上還是對(duì)LRU的封裝,如果有更優(yōu)良的算法,并且也能提供這么多功能,相比之下就相形見(jiàn)絀了。
LFU的局限性:在 LFU 中只要數(shù)據(jù)訪問(wèn)模式的概率分布隨時(shí)間保持不變時(shí),其命中率就能變得非常高。比如有部新劇出來(lái)了,我們使用 LFU 給他緩存下來(lái),這部新劇在這幾天大概訪問(wèn)了幾億次,這個(gè)訪問(wèn)頻率也在我們的 LFU 中記錄了幾億次。但是新劇總會(huì)過(guò)氣的,比如一個(gè)月之后這個(gè)新劇的前幾集其實(shí)已經(jīng)過(guò)氣了,但是他的訪問(wèn)量的確是太高了,其他的電視劇根本無(wú)法淘汰這個(gè)新劇,所以在這種模式下是有局限性。
LRU的優(yōu)點(diǎn)和局限性:LRU可以很好的應(yīng)對(duì)突發(fā)流量的情況,因?yàn)樗恍枰塾?jì)數(shù)據(jù)頻率。但LRU通過(guò)歷史數(shù)據(jù)來(lái)預(yù)測(cè)未來(lái)是局限的,它會(huì)認(rèn)為最后到來(lái)的數(shù)據(jù)是最可能被再次訪問(wèn)的,從而給與它最高的優(yōu)先級(jí)。
在現(xiàn)有算法的局限性下,會(huì)導(dǎo)致緩存數(shù)據(jù)的命中率或多或少的受損,而命中略又是緩存的重要指標(biāo)。HighScalability網(wǎng)站刊登了一篇文章,由前Google工程師發(fā)明的W-TinyLFU——一種現(xiàn)代的緩存 。Caffine Cache就是基于此算法而研發(fā)。
Caffeine 因使用 Window TinyLfu 回收策略,提供了一個(gè)近乎最佳的命中率。
當(dāng)數(shù)據(jù)的訪問(wèn)模式不隨時(shí)間變化的時(shí)候,LFU的策略能夠帶來(lái)最佳的緩存命中率。然而LFU有兩個(gè)缺點(diǎn):
首先,它需要給每個(gè)記錄項(xiàng)維護(hù)頻率信息,每次訪問(wèn)都需要更新,這是個(gè)巨大的開(kāi)銷(xiāo);
其次,如果數(shù)據(jù)訪問(wèn)模式隨時(shí)間有變,LFU的頻率信息無(wú)法隨之變化,因此早先頻繁訪問(wèn)的記錄可能會(huì)占據(jù)緩存,而后期訪問(wèn)較多的記錄則無(wú)法被命中。
因此,大多數(shù)的緩存設(shè)計(jì)都是基于LRU或者其變種來(lái)進(jìn)行的。相比之下,LRU并不需要維護(hù)昂貴的緩存記錄元信息,同時(shí)也能夠反應(yīng)隨時(shí)間變化的數(shù)據(jù)訪問(wèn)模式。然而,在許多負(fù)載之下,LRU依然需要更多的空間才能做到跟LFU一致的緩存命中率。因此,一個(gè)“現(xiàn)代”的緩存,應(yīng)當(dāng)能夠綜合兩者的長(zhǎng)處。
TinyLFU維護(hù)了近期訪問(wèn)記錄的頻率信息,作為一個(gè)過(guò)濾器,當(dāng)新記錄來(lái)時(shí),只有滿足TinyLFU要求的記錄才可以被插入緩存。如前所述,作為現(xiàn)代的緩存,它需要解決兩個(gè)挑戰(zhàn):
一個(gè)是如何避免維護(hù)頻率信息的高開(kāi)銷(xiāo);
另一個(gè)是如何反應(yīng)隨時(shí)間變化的訪問(wèn)模式。
首先來(lái)看前者,TinyLFU借助了數(shù)據(jù)流Sketching技術(shù),Count-Min Sketch顯然是解決這個(gè)問(wèn)題的有效手段,它可以用小得多的空間存放頻率信息,而保證很低的False Positive Rate。但考慮到第二個(gè)問(wèn)題,就要復(fù)雜許多了,因?yàn)槲覀冎?,任何Sketching數(shù)據(jù)結(jié)構(gòu)如果要反應(yīng)時(shí)間變化都是一件困難的事情,在Bloom Filter方面,我們可以有Timing Bloom Filter,但對(duì)于CMSketch來(lái)說(shuō),如何做到Timing CMSketch就不那么容易了。
TinyLFU采用了一種基于滑動(dòng)窗口的時(shí)間衰減設(shè)計(jì)機(jī)制,借助于一種簡(jiǎn)易的reset操作:每次添加一條記錄到Sketch的時(shí)候,都會(huì)給一個(gè)計(jì)數(shù)器上加1,當(dāng)計(jì)數(shù)器達(dá)到一個(gè)尺寸W的時(shí)候,把所有記錄的Sketch數(shù)值都除以2,該reset操作可以起到衰減的作用 。
W-TinyLFU主要用來(lái)解決一些稀疏的突發(fā)訪問(wèn)元素。在一些數(shù)目很少但突發(fā)訪問(wèn)量很大的場(chǎng)景下,TinyLFU將無(wú)法保存這類(lèi)元素,因?yàn)樗鼈儫o(wú)法在給定時(shí)間內(nèi)積累到足夠高的頻率。因此W-TinyLFU就是結(jié)合LFU和LRU,前者用來(lái)應(yīng)對(duì)大多數(shù)場(chǎng)景,而LRU用來(lái)處理突發(fā)流量。
在處理頻率記錄的方案中,你可能會(huì)想到用hashMap去存儲(chǔ),每一個(gè)key對(duì)應(yīng)一個(gè)頻率值。那如果數(shù)據(jù)量特別大的時(shí)候,是不是這個(gè)hashMap也會(huì)特別大呢。由此可以聯(lián)想到 Bloom Filter,對(duì)于每個(gè)key,用n個(gè)byte每個(gè)存儲(chǔ)一個(gè)標(biāo)志用來(lái)判斷key是否在集合中。原理就是使用k個(gè)hash函數(shù)來(lái)將key散列成一個(gè)整數(shù)。
在W-TinyLFU中使用Count-Min Sketch記錄我們的訪問(wèn)頻率,而這個(gè)也是布隆過(guò)濾器的一種變種。如下圖所示:

比如張三和李四,他們兩有可能hash值都是相同,比如都是1那byte[1]這個(gè)位置就會(huì)增加相應(yīng)的頻率,張三訪問(wèn)1萬(wàn)次,李四訪問(wèn)1次那byte[1]這個(gè)位置就是1萬(wàn)零1,如果取李四的訪問(wèn)評(píng)率的時(shí)候就會(huì)取出是1萬(wàn)零1,但是李四命名只訪問(wèn)了1次啊,為了解決這個(gè)問(wèn)題,所以用了多個(gè)hash算法可以理解為long[][]二維數(shù)組的一個(gè)概念,比如在第一個(gè)算法張三和李四沖突了,但是在第二個(gè),第三個(gè)中很大的概率不沖突,比如一個(gè)算法大概有1%的概率沖突,那四個(gè)算法一起沖突的概率是1%的四次方。通過(guò)這個(gè)模式我們?nèi)±钏牡脑L問(wèn)率的時(shí)候取所有算法中,李四訪問(wèn)最低頻率的次數(shù)。所以他的名字叫Count-Min Sketch。
2. 使用
Caffeine Cache 的github地址:
https://github.com/ben-manes/caffeine
目前的最新版本是:
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
<version>2.6.2</version>
</dependency>
2.1 緩存填充策略
Caffeine Cache提供了三種緩存填充策略:手動(dòng)、同步加載和異步加載。
1.手動(dòng)加載
在每次get key的時(shí)候指定一個(gè)同步的函數(shù),如果key不存在就調(diào)用這個(gè)函數(shù)生成一個(gè)值。
/**
* 手動(dòng)加載
* @param key
* @return
*/
public Object manulOperator(String key) {
Cache<String, Object> cache = Caffeine.newBuilder()
.expireAfterWrite(1, TimeUnit.SECONDS)
.expireAfterAccess(1, TimeUnit.SECONDS)
.maximumSize(10)
.build();
//如果一個(gè)key不存在,那么會(huì)進(jìn)入指定的函數(shù)生成value
Object value = cache.get(key, t -> setValue(key).apply(key));
cache.put("hello",value);
//判斷是否存在如果不存返回null
Object ifPresent = cache.getIfPresent(key);
//移除一個(gè)key
cache.invalidate(key);
return value;
}
public Function<String, Object> setValue(String key){
return t -> key + "value";
}
2. 同步加載
構(gòu)造Cache時(shí)候,build方法傳入一個(gè)CacheLoader實(shí)現(xiàn)類(lèi)。實(shí)現(xiàn)load方法,通過(guò)key加載value。
/**
* 同步加載
* @param key
* @return
*/
public Object syncOperator(String key){
LoadingCache<String, Object> cache = Caffeine.newBuilder()
.maximumSize(100)
.expireAfterWrite(1, TimeUnit.MINUTES)
.build(k -> setValue(key).apply(key));
return cache.get(key);
}
public Function<String, Object> setValue(String key){
return t -> key + "value";
}
3. 異步加載
AsyncLoadingCache是繼承自LoadingCache類(lèi)的,異步加載使用Executor去調(diào)用方法并返回一個(gè)CompletableFuture。異步加載緩存使用了響應(yīng)式編程模型。
如果要以同步方式調(diào)用時(shí),應(yīng)提供CacheLoader。要以異步表示時(shí),應(yīng)該提供一個(gè)AsyncCacheLoader,并返回一個(gè)CompletableFuture。
/**
* 異步加載
*
* @param key
* @return
*/
public Object asyncOperator(String key){
AsyncLoadingCache<String, Object> cache = Caffeine.newBuilder()
.maximumSize(100)
.expireAfterWrite(1, TimeUnit.MINUTES)
.buildAsync(k -> setAsyncValue(key).get());
return cache.get(key);
}
public CompletableFuture<Object> setAsyncValue(String key){
return CompletableFuture.supplyAsync(() -> {
return key + "value";
});
}
2.2 回收策略
Caffeine提供了3種回收策略:基于大小回收,基于時(shí)間回收,基于引用回收。
1. 基于大小的過(guò)期方式
基于大小的回收策略有兩種方式:一種是基于緩存大小,一種是基于權(quán)重。
// 根據(jù)緩存的計(jì)數(shù)進(jìn)行驅(qū)逐
LoadingCache<String, Object> cache = Caffeine.newBuilder()
.maximumSize(10000)
.build(key -> function(key));
// 根據(jù)緩存的權(quán)重來(lái)進(jìn)行驅(qū)逐(權(quán)重只是用于確定緩存大小,不會(huì)用于決定該緩存是否被驅(qū)逐)
LoadingCache<String, Object> cache1 = Caffeine.newBuilder()
.maximumWeight(10000)
.weigher(key -> function1(key))
.build(key -> function(key));
maximumWeight與maximumSize不可以同時(shí)使用。
2.基于時(shí)間的過(guò)期方式
// 基于固定的到期策略進(jìn)行退出
LoadingCache<String, Object> cache = Caffeine.newBuilder()
.expireAfterAccess(5, TimeUnit.MINUTES)
.build(key -> function(key));
LoadingCache<String, Object> cache1 = Caffeine.newBuilder()
.expireAfterWrite(10, TimeUnit.MINUTES)
.build(key -> function(key));
// 基于不同的到期策略進(jìn)行退出
LoadingCache<String, Object> cache2 = Caffeine.newBuilder()
.expireAfter(new Expiry<String, Object>() {
@Override
public long expireAfterCreate(String key, Object value, long currentTime) {
return TimeUnit.SECONDS.toNanos(seconds);
}
@Override
public long expireAfterUpdate(@Nonnull String s, @Nonnull Object o, long l, long l1) {
return 0;
}
@Override
public long expireAfterRead(@Nonnull String s, @Nonnull Object o, long l, long l1) {
return 0;
}
}).build(key -> function(key));
Caffeine提供了三種定時(shí)驅(qū)逐策略:
expireAfterAccess(long, TimeUnit):在最后一次訪問(wèn)或者寫(xiě)入后開(kāi)始計(jì)時(shí),在指定的時(shí)間后過(guò)期。假如一直有請(qǐng)求訪問(wèn)該key,那么這個(gè)緩存將一直不會(huì)過(guò)期。 expireAfterWrite(long, TimeUnit): 在最后一次寫(xiě)入緩存后開(kāi)始計(jì)時(shí),在指定的時(shí)間后過(guò)期。 expireAfter(Expiry): 自定義策略,過(guò)期時(shí)間由Expiry實(shí)現(xiàn)獨(dú)自計(jì)算。
緩存的刪除策略使用的是惰性刪除和定時(shí)刪除。這兩個(gè)刪除策略的時(shí)間復(fù)雜度都是O(1)。
3. 基于引用的過(guò)期方式
Java中四種引用類(lèi)型

// 當(dāng)key和value都沒(méi)有引用時(shí)驅(qū)逐緩存
LoadingCache<String, Object> cache = Caffeine.newBuilder()
.weakKeys()
.weakValues()
.build(key -> function(key));
// 當(dāng)垃圾收集器需要釋放內(nèi)存時(shí)驅(qū)逐
LoadingCache<String, Object> cache1 = Caffeine.newBuilder()
.softValues()
.build(key -> function(key));
注意:AsyncLoadingCache不支持弱引用和軟引用。
Caffeine.weakKeys():使用弱引用存儲(chǔ)key。如果沒(méi)有其他地方對(duì)該key有強(qiáng)引用,那么該緩存就會(huì)被垃圾回收器回收。由于垃圾回收器只依賴于身份(identity)相等,因此這會(huì)導(dǎo)致整個(gè)緩存使用身份 (==) 相等來(lái)比較 key,而不是使用 equals()。
Caffeine.weakValues() :使用弱引用存儲(chǔ)value。如果沒(méi)有其他地方對(duì)該value有強(qiáng)引用,那么該緩存就會(huì)被垃圾回收器回收。由于垃圾回收器只依賴于身份(identity)相等,因此這會(huì)導(dǎo)致整個(gè)緩存使用身份 (==) 相等來(lái)比較 key,而不是使用 equals()。
Caffeine.softValues() :使用軟引用存儲(chǔ)value。當(dāng)內(nèi)存滿了過(guò)后,軟引用的對(duì)象以將使用最近最少使用(least-recently-used ) 的方式進(jìn)行垃圾回收。由于使用軟引用是需要等到內(nèi)存滿了才進(jìn)行回收,所以我們通常建議給緩存配置一個(gè)使用內(nèi)存的最大值。softValues() 將使用身份相等(identity) (==) 而不是equals() 來(lái)比較值。
Caffeine.weakValues()和Caffeine.softValues()不可以一起使用。
3. 移除事件監(jiān)聽(tīng)
Cache<String, Object> cache = Caffeine.newBuilder()
.removalListener((String key, Object value, RemovalCause cause) ->
System.out.printf("Key %s was removed (%s)%n", key, cause))
.build();
4. 寫(xiě)入外部存儲(chǔ)
CacheWriter 方法可以將緩存中所有的數(shù)據(jù)寫(xiě)入到第三方。
LoadingCache<String, Object> cache2 = Caffeine.newBuilder()
.writer(new CacheWriter<String, Object>() {
@Override public void write(String key, Object value) {
// 寫(xiě)入到外部存儲(chǔ)
}
@Override public void delete(String key, Object value, RemovalCause cause) {
// 刪除外部存儲(chǔ)
}
})
.build(key -> function(key));
如果你有多級(jí)緩存的情況下,這個(gè)方法還是很實(shí)用。
注意:CacheWriter不能與弱鍵或AsyncLoadingCache一起使用。
5. 統(tǒng)計(jì)#
與Guava Cache的統(tǒng)計(jì)一樣。
Cache<String, Object> cache = Caffeine.newBuilder()
.maximumSize(10_000)
.recordStats()
.build();
通過(guò)使用Caffeine.recordStats(), 可以轉(zhuǎn)化成一個(gè)統(tǒng)計(jì)的集合. 通過(guò) Cache.stats() 返回一個(gè)CacheStats。CacheStats提供以下統(tǒng)計(jì)方法:
hitRate(): 返回緩存命中率 evictionCount(): 緩存回收數(shù)量 averageLoadPenalty(): 加載新值的平均時(shí)間
3. SpringBoot 中默認(rèn)Cache-Caffine Cache
SpringBoot 1.x版本中的默認(rèn)本地cache是Guava Cache。在2.x(Spring Boot 2.0(spring 5) )版本中已經(jīng)用Caffine Cache取代了Guava Cache。畢竟有了更優(yōu)的緩存淘汰策略。
下面我們來(lái)說(shuō)在SpringBoot2.x版本中如何使用cache。
1. 引入依賴:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
<version>2.6.2</version>
</dependency>
2. 添加注解開(kāi)啟緩存支持
添加@EnableCaching注解:
@SpringBootApplication
@EnableCaching
public class SingleDatabaseApplication {
public static void main(String[] args) {
SpringApplication.run(SingleDatabaseApplication.class, args);
}
}
3. 配置文件的方式注入相關(guān)參數(shù)
properties文件
spring.cache.cache-names=cache1
spring.cache.caffeine.spec=initialCapacity=50,maximumSize=500,expireAfterWrite=10s
或Yaml文件
spring:
cache:
type: caffeine
cache-names:
- userCache
caffeine:
spec: maximumSize=1024,refreshAfterWrite=60s
如果使用refreshAfterWrite配置,必須指定一個(gè)CacheLoader.不用該配置則無(wú)需這個(gè)bean,如上所述,該CacheLoader將關(guān)聯(lián)被該緩存管理器管理的所有緩存,所以必須定義為CacheLoader<Object, Object>,自動(dòng)配置將忽略所有泛型類(lèi)型。
import com.github.benmanes.caffeine.cache.CacheLoader;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* @author: rickiyang
* @description:
*/
@Configuration
public class CacheConfig {
/**
* 相當(dāng)于在構(gòu)建LoadingCache對(duì)象的時(shí)候 build()方法中指定過(guò)期之后的加載策略方法
* 必須要指定這個(gè)Bean,refreshAfterWrite=60s屬性才生效
* @return
*/
@Bean
public CacheLoader<String, Object> cacheLoader() {
CacheLoader<String, Object> cacheLoader = new CacheLoader<String, Object>() {
@Override
public Object load(String key) throws Exception {
return null;
}
// 重寫(xiě)這個(gè)方法將oldValue值返回回去,進(jìn)而刷新緩存
@Override
public Object reload(String key, Object oldValue) throws Exception {
return oldValue;
}
};
return cacheLoader;
}
}
Caffeine常用配置說(shuō)明:
initialCapacity=[integer]: 初始的緩存空間大小maximumSize=[long]: 緩存的最大條數(shù)maximumWeight=[long]: 緩存的最大權(quán)重expireAfterAccess=[duration]: 最后一次寫(xiě)入或訪問(wèn)后經(jīng)過(guò)固定時(shí)間過(guò)期expireAfterWrite=[duration]: 最后一次寫(xiě)入后經(jīng)過(guò)固定時(shí)間過(guò)期refreshAfterWrite=[duration]: 創(chuàng)建緩存或者最近一次更新緩存后經(jīng)過(guò)固定的時(shí)間間隔,刷新緩存weakKeys: 打開(kāi)key的弱引用weakValues:打開(kāi)value的弱引用softValues:打開(kāi)value的軟引用recordStats:開(kāi)發(fā)統(tǒng)計(jì)功能
注意:
expireAfterWrite和expireAfterAccess同時(shí)存在時(shí),以expireAfterWrite為準(zhǔn)。 maximumSize和maximumWeight不可以同時(shí)使用 weakValues和softValues不可以同時(shí)使用
需要說(shuō)明的是,使用配置文件的方式來(lái)進(jìn)行緩存項(xiàng)配置,一般情況能滿足使用需求,但是靈活性不是很高,如果我們有很多緩存項(xiàng)的情況下寫(xiě)起來(lái)會(huì)導(dǎo)致配置文件很長(zhǎng)。所以一般情況下你也可以選擇使用bean的方式來(lái)初始化Cache實(shí)例。
下面的演示使用bean的方式來(lái)注入:
package com.rickiyang.learn.cache;
import com.github.benmanes.caffeine.cache.CacheLoader;
import com.github.benmanes.caffeine.cache.Caffeine;
import org.apache.commons.compress.utils.Lists;
import org.springframework.cache.CacheManager;
import org.springframework.cache.caffeine.CaffeineCache;
import org.springframework.cache.support.SimpleCacheManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;
/**
* @author: rickiyang
* @description:
*/
@Configuration
public class CacheConfig {
/**
* 創(chuàng)建基于Caffeine的Cache Manager
* 初始化一些key存入
* @return
*/
@Bean
@Primary
public CacheManager caffeineCacheManager() {
SimpleCacheManager cacheManager = new SimpleCacheManager();
ArrayList<CaffeineCache> caches = Lists.newArrayList();
List<CacheBean> list = setCacheBean();
for(CacheBean cacheBean : list){
caches.add(new CaffeineCache(cacheBean.getKey(),
Caffeine.newBuilder().recordStats()
.expireAfterWrite(cacheBean.getTtl(), TimeUnit.SECONDS)
.maximumSize(cacheBean.getMaximumSize())
.build()));
}
cacheManager.setCaches(caches);
return cacheManager;
}
/**
* 初始化一些緩存的 key
* @return
*/
private List<CacheBean> setCacheBean(){
List<CacheBean> list = Lists.newArrayList();
CacheBean userCache = new CacheBean();
userCache.setKey("userCache");
userCache.setTtl(60);
userCache.setMaximumSize(10000);
CacheBean deptCache = new CacheBean();
deptCache.setKey("userCache");
deptCache.setTtl(60);
deptCache.setMaximumSize(10000);
list.add(userCache);
list.add(deptCache);
return list;
}
class CacheBean {
private String key;
private long ttl;
private long maximumSize;
public String getKey() {
return key;
}
public void setKey(String key) {
this.key = key;
}
public long getTtl() {
return ttl;
}
public void setTtl(long ttl) {
this.ttl = ttl;
}
public long getMaximumSize() {
return maximumSize;
}
public void setMaximumSize(long maximumSize) {
this.maximumSize = maximumSize;
}
}
}
創(chuàng)建了一個(gè)SimpleCacheManager作為Cache的管理對(duì)象,然后初始化了兩個(gè)Cache對(duì)象,分別存儲(chǔ)user,dept類(lèi)型的緩存。當(dāng)然構(gòu)建Cache的參數(shù)設(shè)置我寫(xiě)的比較簡(jiǎn)單,你在使用的時(shí)候酌情根據(jù)需要配置參數(shù)。
4. 使用注解來(lái)對(duì) cache 增刪改查
我們可以使用spring提供的 @Cacheable、@CachePut、@CacheEvict等注解來(lái)方便的使用caffeine緩存。
如果使用了多個(gè)cahce,比如redis、caffeine等,必須指定某一個(gè)CacheManage為@primary,在@Cacheable注解中沒(méi)指定 cacheManager 則使用標(biāo)記為primary的那個(gè)。
cache方面的注解主要有以下5個(gè):
@Cacheable 觸發(fā)緩存入口(這里一般放在創(chuàng)建和獲取的方法上,
@Cacheable注解會(huì)先查詢是否已經(jīng)有緩存,有會(huì)使用緩存,沒(méi)有則會(huì)執(zhí)行方法并緩存)@CacheEvict 觸發(fā)緩存的eviction(用于刪除的方法上)
@CachePut 更新緩存且不影響方法執(zhí)行(用于修改的方法上,該注解下的方法始終會(huì)被執(zhí)行)
@Caching 將多個(gè)緩存組合在一個(gè)方法上(該注解可以允許一個(gè)方法同時(shí)設(shè)置多個(gè)注解)
@CacheConfig 在類(lèi)級(jí)別設(shè)置一些緩存相關(guān)的共同配置(與其它緩存配合使用)
說(shuō)一下@Cacheable 和 @CachePut的區(qū)別:
@Cacheable:它的注解的方法是否被執(zhí)行取決于Cacheable中的條件,方法很多時(shí)候都可能不被執(zhí)行。
@CachePut:這個(gè)注解不會(huì)影響方法的執(zhí)行,也就是說(shuō)無(wú)論它配置的條件是什么,方法都會(huì)被執(zhí)行,更多的時(shí)候是被用到修改上。
簡(jiǎn)要說(shuō)一下Cacheable類(lèi)中各個(gè)方法的使用:
public @interface Cacheable {
/**
* 要使用的cache的名字
*/
@AliasFor("cacheNames")
String[] value() default {};
/**
* 同value(),決定要使用那個(gè)/些緩存
*/
@AliasFor("value")
String[] cacheNames() default {};
/**
* 使用SpEL表達(dá)式來(lái)設(shè)定緩存的key,如果不設(shè)置默認(rèn)方法上所有參數(shù)都會(huì)作為key的一部分
*/
String key() default "";
/**
* 用來(lái)生成key,與key()不可以共用
*/
String keyGenerator() default "";
/**
* 設(shè)定要使用的cacheManager,必須先設(shè)置好cacheManager的bean,這是使用該bean的名字
*/
String cacheManager() default "";
/**
* 使用cacheResolver來(lái)設(shè)定使用的緩存,用法同cacheManager,但是與cacheManager不可以同時(shí)使用
*/
String cacheResolver() default "";
/**
* 使用SpEL表達(dá)式設(shè)定出發(fā)緩存的條件,在方法執(zhí)行前生效
*/
String condition() default "";
/**
* 使用SpEL設(shè)置出發(fā)緩存的條件,這里是方法執(zhí)行完生效,所以條件中可以有方法執(zhí)行后的value
*/
String unless() default "";
/**
* 用于同步的,在緩存失效(過(guò)期不存在等各種原因)的時(shí)候,如果多個(gè)線程同時(shí)訪問(wèn)被標(biāo)注的方法
* 則只允許一個(gè)線程通過(guò)去執(zhí)行方法
*/
boolean sync() default false;
}
基于注解的使用方法:
package com.rickiyang.learn.cache;
import com.rickiyang.learn.entity.User;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.CachePut;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;
/**
* @author: rickiyang
* @description: 本地cache
*/
@Service
public class UserCacheService {
/**
* 查找
* 先查緩存,如果查不到,會(huì)查數(shù)據(jù)庫(kù)并存入緩存
* @param id
*/
@Cacheable(value = "userCache", key = "#id", sync = true)
public void getUser(long id){
//查找數(shù)據(jù)庫(kù)
}
/**
* 更新/保存
* @param user
*/
@CachePut(value = "userCache", key = "#user.id")
public void saveUser(User user){
//todo 保存數(shù)據(jù)庫(kù)
}
/**
* 刪除
* @param user
*/
@CacheEvict(value = "userCache",key = "#user.id")
public void delUser(User user){
//todo 保存數(shù)據(jù)庫(kù)
}
}
如果你不想使用注解的方式去操作緩存,也可以直接使用SimpleCacheManager獲取緩存的key進(jìn)而進(jìn)行操作。
注意到上面的key使用了spEL 表達(dá)式。Spring Cache提供了一些供我們使用的SpEL上下文數(shù)據(jù),下表直接摘自Spring官方文檔:

注意:
1.當(dāng)我們要使用root對(duì)象的屬性作為key時(shí)我們也可以將“#root”省略,因?yàn)镾pring默認(rèn)使用的就是root對(duì)象的屬性。如
@Cacheable(key = "targetClass + methodName +#p0")
2.使用方法參數(shù)時(shí)我們可以直接使用“#參數(shù)名”或者“#p參數(shù)index”。如:
@Cacheable(value="userCache", key="#id")
@Cacheable(value="userCache", key="#p0")
SpEL提供了多種運(yùn)算符

往 期 推 薦 1、全網(wǎng)最全 Java 日志框架適配方案!還有誰(shuí)不會(huì)? 2、Chrome瀏覽器最新高危漏洞曝光!升級(jí)最新版也沒(méi)用~ 3、Spring中毒太深,離開(kāi)Spring我居然連最基本的接口都不會(huì)寫(xiě)了 4、黑客用GitHub服務(wù)器挖礦,三天跑了3萬(wàn)個(gè)任務(wù),代碼驚現(xiàn)中文 5、瀏覽器輸入「xxxxhub」的背后..... 6、Gradle真能干掉Maven?今天體驗(yàn)了一把,賊爽! 7、如何重構(gòu)千行“又臭又長(zhǎng)”的類(lèi)?IntelliJ IDEA 幾分鐘就搞定!

點(diǎn)分享

點(diǎn)收藏

點(diǎn)點(diǎn)贊

點(diǎn)在看

