前面剛說到Guava Cache,他的優勢是封裝了get,put操做;提供線程安全的緩存操做;提供過時策略;提供回收策略;緩存監控。當緩存的數據超過最大值時,使用LRU算法替換。這一篇咱們將要談到一個新的本地緩存框架:Caffeine Cache。它也是站在巨人的肩膀上-Guava Cache,藉着他的思想優化了算法發展而來。java
本篇博文主要介紹Caffine Cache 的使用方式,以及Caffine Cache在SpringBoot中的使用。git
說到優化,Caffine Cache到底優化了什麼呢?咱們剛提到過LRU,常見的緩存淘汰算法還有FIFO,LFU:github
上面三種策略各有利弊,實現的成本也是一個比一個高,同時命中率也是一個比一個好。Guava Cache雖然有這麼多的功能,可是本質上仍是對LRU的封裝,若是有更優良的算法,而且也能提供這麼多功能,相比之下就相形見絀了。web
LFU的侷限性:在 LFU 中只要數據訪問模式的機率分佈隨時間保持不變時,其命中率就能變得很是高。好比有部新劇出來了,咱們使用 LFU 給他緩存下來,這部新劇在這幾天大概訪問了幾億次,這個訪問頻率也在咱們的 LFU 中記錄了幾億次。可是新劇總會過氣的,好比一個月以後這個新劇的前幾集其實已通過氣了,可是他的訪問量的確是過高了,其餘的電視劇根本沒法淘汰這個新劇,因此在這種模式下是有侷限性。正則表達式
LRU的優勢和侷限性:LRU能夠很好的應對突發流量的狀況,由於他不須要累計數據頻率。但LRU經過歷史數據來預測將來是侷限的,它會認爲最後到來的數據是最可能被再次訪問的,從而給與它最高的優先級。redis
在現有算法的侷限性下,會致使緩存數據的命中率或多或少的受損,而命中略又是緩存的重要指標。HighScalability網站刊登了一篇文章,由前Google工程師發明的W-TinyLFU——一種現代的緩存 。Caffine Cache就是基於此算法而研發。Caffeine 因使用 Window TinyLfu 回收策略,提供了一個近乎最佳的命中率。算法
當數據的訪問模式不隨時間變化的時候,LFU的策略可以帶來最佳的緩存命中率。然而LFU有兩個缺點:spring
首先,它須要給每一個記錄項維護頻率信息,每次訪問都須要更新,這是個巨大的開銷;數據庫
其次,若是數據訪問模式隨時間有變,LFU的頻率信息沒法隨之變化,所以早先頻繁訪問的記錄可能會佔據緩存,然後期訪問較多的記錄則沒法被命中。apache
所以,大多數的緩存設計都是基於LRU或者其變種來進行的。相比之下,LRU並不須要維護昂貴的緩存記錄元信息,同時也可以反應隨時間變化的數據訪問模式。然而,在許多負載之下,LRU依然須要更多的空間才能作到跟LFU一致的緩存命中率。所以,一個「現代」的緩存,應當可以綜合二者的長處。
TinyLFU維護了近期訪問記錄的頻率信息,做爲一個過濾器,當新記錄來時,只有知足TinyLFU要求的記錄才能夠被插入緩存。如前所述,做爲現代的緩存,它須要解決兩個挑戰:
一個是如何避免維護頻率信息的高開銷;
另外一個是如何反應隨時間變化的訪問模式。
首先來看前者,TinyLFU藉助了數據流Sketching技術,Count-Min Sketch顯然是解決這個問題的有效手段,它能夠用小得多的空間存放頻率信息,而保證很低的False Positive Rate。但考慮到第二個問題,就要複雜許多了,由於咱們知道,任何Sketching數據結構若是要反應時間變化都是一件困難的事情,在Bloom Filter方面,咱們能夠有Timing Bloom Filter,但對於CMSketch來講,如何作到Timing CMSketch就不那麼容易了。TinyLFU採用了一種基於滑動窗口的時間衰減設計機制,藉助於一種簡易的reset操做:每次添加一條記錄到Sketch的時候,都會給一個計數器上加1,當計數器達到一個尺寸W的時候,把全部記錄的Sketch數值都除以2,該reset操做能夠起到衰減的做用 。
W-TinyLFU主要用來解決一些稀疏的突發訪問元素。在一些數目不多但突發訪問量很大的場景下,TinyLFU將沒法保存這類元素,由於它們沒法在給定時間內積累到足夠高的頻率。所以W-TinyLFU就是結合LFU和LRU,前者用來應對大多數場景,而LRU用來處理突發流量。
在處理頻率記錄的方案中,你可能會想到用hashMap去存儲,每個key對應一個頻率值。那若是數據量特別大的時候,是否是這個hashMap也會特別大呢。由此能夠聯想到 Bloom Filter,對於每一個key,用n個byte每一個存儲一個標誌用來判斷key是否在集合中。原理就是使用k個hash函數來將key散列成一個整數。
在W-TinyLFU中使用Count-Min Sketch記錄咱們的訪問頻率,而這個也是布隆過濾器的一種變種。以下圖所示:
若是須要記錄一個值,那咱們須要經過多種Hash算法對其進行處理hash,而後在對應的hash算法的記錄中+1,爲何須要多種hash算法呢?因爲這是一個壓縮算法一定會出現衝突,好比咱們創建一個byte的數組,經過計算出每一個數據的hash的位置。好比張三和李四,他們兩有可能hash值都是相同,好比都是1那byte[1]這個位置就會增長相應的頻率,張三訪問1萬次,李四訪問1次那byte[1]這個位置就是1萬零1,若是取李四的訪問評率的時候就會取出是1萬零1,可是李四命名只訪問了1次啊,爲了解決這個問題,因此用了多個hash算法能夠理解爲long[][]二維數組的一個概念,好比在第一個算法張三和李四衝突了,可是在第二個,第三個中很大的機率不衝突,好比一個算法大概有1%的機率衝突,那四個算法一塊兒衝突的機率是1%的四次方。經過這個模式咱們取李四的訪問率的時候取全部算法中,李四訪問最低頻率的次數。因此他的名字叫Count-Min Sketch。
Caffeine Cache 的github地址:點我。
目前的最新版本是:
<dependency> <groupId>com.github.ben-manes.caffeine</groupId> <artifactId>caffeine</artifactId> <version>2.6.2</version> </dependency>
Caffeine Cache提供了三種緩存填充策略:手動、同步加載和異步加載。
在每次get key的時候指定一個同步的函數,若是key不存在就調用這個函數生成一個值。
/** * 手動加載 * @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(); //若是一個key不存在,那麼會進入指定的函數生成value Object value = cache.get(key, t -> setValue(key).apply(key)); cache.put("hello",value); //判斷是否存在若是不存返回null Object ifPresent = cache.getIfPresent(key); //移除一個key cache.invalidate(key); return value; } public Function<String, Object> setValue(String key){ return t -> key + "value"; }
構造Cache時候,build方法傳入一個CacheLoader實現類。實現load方法,經過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"; }
AsyncLoadingCache是繼承自LoadingCache類的,異步加載使用Executor去調用方法並返回一個CompletableFuture。異步加載緩存使用了響應式編程模型。
若是要以同步方式調用時,應提供CacheLoader。要以異步表示時,應該提供一個AsyncCacheLoader,並返回一個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"; }); }
Caffeine提供了3種回收策略:基於大小回收,基於時間回收,基於引用回收。
基於大小的回收策略有兩種方式:一種是基於緩存大小,一種是基於權重。
// 根據緩存的計數進行驅逐 LoadingCache<String, Object> cache = Caffeine.newBuilder() .maximumSize(10000) .build(key -> function(key)); // 根據緩存的權重來進行驅逐(權重只是用於肯定緩存大小,不會用於決定該緩存是否被驅逐) LoadingCache<String, Object> cache1 = Caffeine.newBuilder() .maximumWeight(10000) .weigher(key -> function1(key)) .build(key -> function(key));
maximumWeight與maximumSize不能夠同時使用。
// 基於固定的到期策略進行退出 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)); // 基於不一樣的到期策略進行退出 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提供了三種定時驅逐策略:
expireAfterAccess(long, TimeUnit):在最後一次訪問或者寫入後開始計時,在指定的時間後過時。假如一直有請求訪問該key,那麼這個緩存將一直不會過時。
expireAfterWrite(long, TimeUnit): 在最後一次寫入緩存後開始計時,在指定的時間後過時。
expireAfter(Expiry): 自定義策略,過時時間由Expiry實現獨自計算。
緩存的刪除策略使用的是惰性刪除和定時刪除。這兩個刪除策略的時間複雜度都是O(1)。
Java中四種引用類型
引用類型 | 被垃圾回收時間 | 用途 | 生存時間 |
---|---|---|---|
強引用 Strong Reference | 歷來不會 | 對象的通常狀態 | JVM中止運行時終止 |
軟引用 Soft Reference | 在內存不足時 | 對象緩存 | 內存不足時終止 |
弱引用 Weak Reference | 在垃圾回收時 | 對象緩存 | gc運行後終止 |
虛引用 Phantom Reference | 歷來不會 | 能夠用虛引用來跟蹤對象被垃圾回收器回收的活動,當一個虛引用關聯的對象被垃圾收集器回收以前會收到一條系統通知 | JVM中止運行時終止 |
// 當key和value都沒有引用時驅逐緩存 LoadingCache<String, Object> cache = Caffeine.newBuilder() .weakKeys() .weakValues() .build(key -> function(key)); // 當垃圾收集器須要釋放內存時驅逐 LoadingCache<String, Object> cache1 = Caffeine.newBuilder() .softValues() .build(key -> function(key));
注意:AsyncLoadingCache不支持弱引用和軟引用。
Caffeine.weakKeys(): 使用弱引用存儲key。若是沒有其餘地方對該key有強引用,那麼該緩存就會被垃圾回收器回收。因爲垃圾回收器只依賴於身份(identity)相等,所以這會致使整個緩存使用身份 (==) 相等來比較 key,而不是使用 equals()。
Caffeine.weakValues() :使用弱引用存儲value。若是沒有其餘地方對該value有強引用,那麼該緩存就會被垃圾回收器回收。因爲垃圾回收器只依賴於身份(identity)相等,所以這會致使整個緩存使用身份 (==) 相等來比較 key,而不是使用 equals()。
Caffeine.softValues() :使用軟引用存儲value。當內存滿了事後,軟引用的對象以將使用最近最少使用(least-recently-used ) 的方式進行垃圾回收。因爲使用軟引用是須要等到內存滿了才進行回收,因此咱們一般建議給緩存配置一個使用內存的最大值。 softValues() 將使用身份相等(identity) (==) 而不是equals() 來比較值。
Caffeine.weakValues()和Caffeine.softValues()不能夠一塊兒使用。
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();
CacheWriter 方法能夠將緩存中全部的數據寫入到第三方。
LoadingCache<String, Object> cache2 = Caffeine.newBuilder() .writer(new CacheWriter<String, Object>() { @Override public void write(String key, Object value) { // 寫入到外部存儲 } @Override public void delete(String key, Object value, RemovalCause cause) { // 刪除外部存儲 } }) .build(key -> function(key));
若是你有多級緩存的狀況下,這個方法仍是很實用。
注意:CacheWriter不能與弱鍵或AsyncLoadingCache一塊兒使用。
與Guava Cache的統計同樣。
Cache<String, Object> cache = Caffeine.newBuilder() .maximumSize(10_000) .recordStats() .build();
經過使用Caffeine.recordStats(), 能夠轉化成一個統計的集合. 經過 Cache.stats() 返回一個CacheStats。CacheStats提供如下統計方法:
hitRate(): 返回緩存命中率 evictionCount(): 緩存回收數量 averageLoadPenalty(): 加載新值的平均時間
SpringBoot 1.x版本中的默認本地cache是Guava Cache。在2.x(Spring Boot 2.0(spring 5) )版本中已經用Caffine Cache取代了Guava Cache。畢竟有了更優的緩存淘汰策略。
下面咱們來講在SpringBoot2.x版本中如何使用cache。
<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>
添加@EnableCaching註解:
@SpringBootApplication @EnableCaching public class SingleDatabaseApplication { public static void main(String[] args) { SpringApplication.run(SingleDatabaseApplication.class, args); } }
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配置,必須指定一個CacheLoader.不用該配置則無需這個bean,如上所述,該CacheLoader將關聯被該緩存管理器管理的全部緩存,因此必須定義爲CacheLoader<Object, Object>,自動配置將忽略全部泛型類型。
import com.github.benmanes.caffeine.cache.CacheLoader; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; /** * @author: rickiyang * @date: 2019/6/15 * @description: */ @Configuration public class CacheConfig { /** * 至關於在構建LoadingCache對象的時候 build()方法中指定過時以後的加載策略方法 * 必需要指定這個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; } // 重寫這個方法將oldValue值返回回去,進而刷新緩存 @Override public Object reload(String key, Object oldValue) throws Exception { return oldValue; } }; return cacheLoader; } }
Caffeine經常使用配置說明:
initialCapacity=[integer]: 初始的緩存空間大小 maximumSize=[long]: 緩存的最大條數 maximumWeight=[long]: 緩存的最大權重 expireAfterAccess=[duration]: 最後一次寫入或訪問後通過固定時間過時 expireAfterWrite=[duration]: 最後一次寫入後通過固定時間過時 refreshAfterWrite=[duration]: 建立緩存或者最近一次更新緩存後通過固定的時間間隔,刷新緩存 weakKeys: 打開key的弱引用 weakValues:打開value的弱引用 softValues:打開value的軟引用 recordStats:開發統計功能 注意: expireAfterWrite和expireAfterAccess同時存在時,以expireAfterWrite爲準。 maximumSize和maximumWeight不能夠同時使用 weakValues和softValues不能夠同時使用
須要說明的是,使用配置文件的方式來進行緩存項配置,通常狀況能知足使用需求,可是靈活性不是很高,若是咱們有不少緩存項的狀況下寫起來會致使配置文件很長。因此通常狀況下你也能夠選擇使用bean的方式來初始化Cache實例。
下面的演示使用bean的方式來注入:
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 * @date: 2019/6/15 * @description: */ @Configuration public class CacheConfig { /** * 建立基於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; } } }
建立了一個SimpleCacheManager
做爲Cache的管理對象,而後初始化了兩個Cache對象,分別存儲user,dept類型的緩存。固然構建Cache的參數設置我寫的比較簡單,你在使用的時候酌情根據須要配置參數。
咱們可使用spring提供的 @Cacheable
、@CachePut
、@CacheEvict
等註解來方便的使用caffeine緩存。
若是使用了多個cahce,好比redis、caffeine等,必須指定某一個CacheManage爲@primary,在@Cacheable註解中沒指定 cacheManager 則使用標記爲primary的那個。
cache方面的註解主要有如下5個:
@Cacheable
註解會先查詢是否已經有緩存,有會使用緩存,沒有則會執行方法並緩存)說一下@Cacheable
和 @CachePut
的區別:
@Cacheable:它的註解的方法是否被執行取決於Cacheable中的條件,方法不少時候均可能不被執行。
@CachePut:這個註解不會影響方法的執行,也就是說不管它配置的條件是什麼,方法都會被執行,更多的時候是被用到修改上。
簡要說一下Cacheable類中各個方法的使用:
public @interface Cacheable { /** * 要使用的cache的名字 */ @AliasFor("cacheNames") String[] value() default {}; /** * 同value(),決定要使用那個/些緩存 */ @AliasFor("value") String[] cacheNames() default {}; /** * 使用SpEL表達式來設定緩存的key,若是不設置默認方法上全部參數都會做爲key的一部分 */ String key() default ""; /** * 用來生成key,與key()不能夠共用 */ String keyGenerator() default ""; /** * 設定要使用的cacheManager,必須先設置好cacheManager的bean,這是使用該bean的名字 */ String cacheManager() default ""; /** * 使用cacheResolver來設定使用的緩存,用法同cacheManager,可是與cacheManager不能夠同時使用 */ String cacheResolver() default ""; /** * 使用SpEL表達式設定出發緩存的條件,在方法執行前生效 */ String condition() default ""; /** * 使用SpEL設置出發緩存的條件,這裏是方法執行完生效,因此條件中能夠有方法執行後的value */ String unless() default ""; /** * 用於同步的,在緩存失效(過時不存在等各類緣由)的時候,若是多個線程同時訪問被標註的方法 * 則只容許一個線程經過去執行方法 */ 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 * @date: 2019/6/15 * @description: 本地cache */ @Service public class UserCacheService { /** * 查找 * 先查緩存,若是查不到,會查數據庫並存入緩存 * @param id */ @Cacheable(value = "userCache", key = "#id", sync = true) public void getUser(long id){ //查找數據庫 } /** * 更新/保存 * @param user */ @CachePut(value = "userCache", key = "#user.id") public void saveUser(User user){ //todo 保存數據庫 } /** * 刪除 * @param user */ @CacheEvict(value = "userCache",key = "#user.id") public void delUser(User user){ //todo 保存數據庫 } }
若是你不想使用註解的方式去操做緩存,也能夠直接使用SimpleCacheManager獲取緩存的key進而進行操做。
注意到上面的key使用了spEL 表達式。Spring Cache提供了一些供咱們使用的SpEL上下文數據,下表直接摘自Spring官方文檔:
名稱 | 位置 | 描述 | 示例 |
---|---|---|---|
methodName | root對象 | 當前被調用的方法名 | #root.methodname |
method | root對象 | 當前被調用的方法 | #root.method.name |
target | root對象 | 當前被調用的目標對象實例 | #root.target |
targetClass | root對象 | 當前被調用的目標對象的類 | #root.targetClass |
args | root對象 | 當前被調用的方法的參數列表 | #root.args[0] |
caches | root對象 | 當前方法調用使用的緩存列表 | #root.caches[0].name |
Argument Name | 執行上下文 | 當前被調用的方法的參數,如findArtisan(Artisan artisan),能夠經過#artsian.id得到參數 | #artsian.id |
result | 執行上下文 | 方法執行後的返回值(僅當方法執行後的判斷有效,如 unless cacheEvict的beforeInvocation=false) | #result |
注意:
1.當咱們要使用root對象的屬性做爲key時咱們也能夠將「#root」省略,由於Spring默認使用的就是root對象的屬性。 如
@Cacheable(key = "targetClass + methodName +#p0")
2.使用方法參數時咱們能夠直接使用「#參數名」或者「#p參數index」。 如:
@Cacheable(value="userCache", key="#id") @Cacheable(value="userCache", key="#p0")
SpEL提供了多種運算符
類型 | 運算符 |
---|---|
關係 | <,>,<=,>=,==,!=,lt,gt,le,ge,eq,ne |
算術 | +,- ,* ,/,%,^ |
邏輯 | &&,||,!,and,or,not,between,instanceof |
條件 | ?: (ternary),?: (elvis) |
正則表達式 | matches |
其餘類型 | ?.,?[…],![…],^[…],$[…] |