javascript
Spring官方都说废掉GuavaCache用Caffeine,你还不换?
最近來了一個實習生小張,看了我在公司項目中使用的緩存框架Caffeine,三天兩頭跑來找我取經,說是要把Caffeine吃透,為此無奈的也只能一個個細心解答了。
后來這件事情被總監直到了,說是后面還有新人,讓我將相關問題和細節匯總成一份教程,權當共享好了,該份教程也算是全網第一份,結合了目前我司游戲中業務場景的應用和思考,以及踩過的坑。
?實習生小張:稀飯稀飯,以前我們游戲中應用的緩存其實是谷歌提供的ConcurrentLinkedHashMap,為什么后面你強烈要求換成用Caffeine呢??
關于上面的問題,具體有以下幾個原因:
使用谷歌提供的ConcurrentLinkedHashMap有個漏洞,那就是緩存的過期只會發生在緩存達到上限的情況,否則便只會一直放在緩存中。咋一看,這個機制沒問題,是沒問題,可是卻不合理,舉個例子,有玩家上線后加載了一堆的數據放在緩存中,之后便不再上線了,那么這份緩存便會一直存在,知道緩存達到上限。
ConcurrentLinkedHashMap沒有提供基于時間淘汰時間的機制,而Caffeine有,并且有多種淘汰機制,并且支持淘汰通知。
目前Spring也在推薦使用,Caffeine 因使用 Window TinyLfu 回收策略,提供了一個近乎最佳的命中率。
?實習生小張:哦哦哦,我了解了,是否可以跟我介紹下Caffeine呢??
可以的,Caffeine是基于Java8的高性能緩存庫,可提供接近最佳的命中率。Caffeine的底層使用了ConcurrentHashMap,支持按照一定的規則或者自定義的規則使緩存的數據過期,然后銷毀。
再說一個勁爆的消息,很多人都聽說過Google的GuavaCache,而沒有聽說過Caffeine,其實和Caffeine相比,GuavaCache簡直就是個弟中弟,這不SpringFramework5.0(SpringBoot2.0)已經放棄了Google的GuavaCache,轉而選擇了Caffeine。
caffeine對比為什么敢這么夸Caffeine呢?我們可以用官方給出的數據說話。
Caffeine提供了多種靈活的構造方法,從而可以創建多種特性的本地緩存。
自動把數據加載到本地緩存中,并且可以配置異步;
基于數量剔除策略;
基于失效時間剔除策略,這個時間是從最后一次操作算起【訪問或者寫入】;
異步刷新;
Key會被包裝成Weak引用;
Value會被包裝成Weak或者Soft引用,從而能被GC掉,而不至于內存泄漏;
數據剔除提醒;
寫入廣播機制;
緩存訪問可以統計;
?實習生小張:我擦,這么強大,為什么可以這么強大呢,稀飯你不是自稱最熟悉Caffeine的人嗎?能否給我大概所說內部結構呢??
我日,我沒有,我只是說在我們項目組我最熟悉,別污蔑我
那接下來我大概介紹下Caffeine的內部結構
Cache的內部包含著一個ConcurrentHashMap,這也是存放我們所有緩存數據的地方,眾所周知,ConcurrentHashMap是一個并發安全的容器,這點很重要,可以說Caffeine其實就是一個被強化過的ConcurrentHashMap。
Scheduler,定期清空數據的一個機制,可以不設置,如果不設置則不會主動的清空過期數據。
Executor,指定運行異步任務時要使用的線程池。可以不設置,如果不設置則會使用默認的線程池,也就是ForkJoinPool.commonPool()
?實習生小張:聽起來就是一個強化版的ConcurrentHashMap,那么需要導入什么包嗎??
Caffeine的依賴,其實還是很簡單的,直接引入maven依賴即可。
<dependency><groupId>com.github.ben-manes.caffeine</groupId><artifactId>caffeine</artifactId> </dependency>?實習生小張:可以,導入成功了,你一直和我說Caffeine的數據填充機制設計的很優美,不就是put數據嗎?有什么優美的?說說看嗎?
?
是put數據,只是針對put數據,Caffeine提供了三種機制,分別是
手動加載
同步加載
異步加載
我分別舉個例子,比如手動加載
/***?@author?xifanxiaxue*?@date?2020/11/17?0:16*?@desc?手動填充數據*/ public?class?CaffeineManualTest?{@Testpublic?void?test()?{//?初始化緩存,設置了1分鐘的寫過期,100的緩存最大個數Cache<Integer,?Integer>?cache?=?Caffeine.newBuilder().expireAfterWrite(1,?TimeUnit.MINUTES).maximumSize(100).build();int?key1?=?1;//?使用getIfPresent方法從緩存中獲取值。如果緩存中不存指定的值,則方法將返回 null:System.out.println(cache.getIfPresent(key1));//?也可以使用 get 方法獲取值,該方法將一個參數為 key 的 Function 作為參數傳入。如果緩存中不存在該 key//?則該函數將用于提供默認值,該值在計算后插入緩存中:System.out.println(cache.get(key1,?new?Function<Integer,?Integer>()?{@Overridepublic?Integer?apply(Integer?integer)?{return?2;}}));//?校驗key1對應的value是否插入緩存中System.out.println(cache.getIfPresent(key1));//?手動put數據填充緩存中int?value1?=?2;cache.put(key1,?value1);//?使用getIfPresent方法從緩存中獲取值。如果緩存中不存指定的值,則方法將返回 null:System.out.println(cache.getIfPresent(1));//?移除數據,讓數據失效cache.invalidate(1);System.out.println(cache.getIfPresent(1));} }上面提到了兩個get數據的方式,一個是getIfPercent,沒數據會返回Null,而get數據的話則需要提供一個Function對象,當緩存中不存在查詢的key則將該函數用于提供默認值,并且會插入緩存中。
?實習生小張:如果同時有多個線程進行get,那么這個Function對象是否會被執行多次呢??
實際上不會的,可以從結構圖看出,Caffeine內部最主要的數據結構就是一個ConcurrentHashMap,而get的過程最終執行的便是ConcurrentHashMap.compute,這里僅會被執行一次。
接下來說說同步加載數據
/***?@author?xifanxiaxue*?@date?2020/11/19?9:47*?@desc?同步加載數據*/ public?class?CaffeineLoadingTest?{/***?模擬從數據庫中讀取key**?@param?key*?@return*/private?int?getInDB(int?key)?{return?key?+?1;}@Testpublic?void?test()?{//?初始化緩存,設置了1分鐘的寫過期,100的緩存最大個數LoadingCache<Integer,?Integer>?cache?=?Caffeine.newBuilder().expireAfterWrite(1,?TimeUnit.MINUTES).maximumSize(100).build(new?CacheLoader<Integer,?Integer>()?{@Nullable@Overridepublic?Integer?load(@NonNull?Integer?key)?{return?getInDB(key);}});int?key1?=?1;// get數據,取不到則從數據庫中讀取相關數據,該值也會插入緩存中:Integer?value1?=?cache.get(key1);System.out.println(value1);//?支持直接get一組值,支持批量查找Map<Integer,?Integer>?dataMap=?cache.getAll(Arrays.asList(1,?2,?3));System.out.println(dataMap);} }所謂的同步加載數據指的是,在get不到數據時最終會調用build構造時提供的CacheLoader對象中的load函數,如果返回值則將其插入緩存中,并且返回,這是一種同步的操作,也支持批量查找。
「實際應用:在我司項目中,會利用這個同步機制,也就是在CacheLoader對象中的load函數中,當從Caffeine緩存中取不到數據的時候則從數據庫中讀取數據,通過這個機制和數據庫結合使用」
最后一種便是異步加載
/***?@author?xifanxiaxue*?@date?2020/11/19?22:34*?@desc?異步加載*/ public?class?CaffeineAsynchronousTest?{/***?模擬從數據庫中讀取key**?@param?key*?@return*/private?int?getInDB(int?key)?{return?key?+?1;}@Testpublic?void?test()?throws?ExecutionException,?InterruptedException?{//?使用executor設置線程池AsyncCache<Integer,?Integer>?asyncCache?=?Caffeine.newBuilder().expireAfterWrite(1,?TimeUnit.MINUTES).maximumSize(100).executor(Executors.newSingleThreadExecutor()).buildAsync();Integer?key?=?1;//?get返回的是CompletableFutureCompletableFuture<Integer>?future?=?asyncCache.get(key,?new?Function<Integer,?Integer>()?{@Overridepublic?Integer?apply(Integer?key)?{//?執行所在的線程不在是main,而是ForkJoinPool線程池提供的線程System.out.println("當前所在線程:"?+?Thread.currentThread().getName());int?value?=?getInDB(key);return?value;}});int?value?=?future.get();System.out.println("當前所在線程:"?+?Thread.currentThread().getName());System.out.println(value);} }執行結果如下
可以看到getInDB是在線程池ForkJoinPool提供的線程中執行的,而且asyncCache.get()返回的是一個CompletableFuture,熟悉流式編程的人對這個會比較熟悉,可以用CompletableFuture來實現異步串行的實現。
?實習生小張:我看到默認是線程池ForkJoinPool提供的線程,實際上不大可能用默認的,所以我們可以自己指定嗎??
答案是可以的,實例如下
//?使用executor設置線程池 AsyncCache<Integer,?Integer>?asyncCache?=?Caffeine.newBuilder().expireAfterWrite(1,?TimeUnit.MINUTES).maximumSize(100).executor(Executors.newSingleThreadExecutor()).buildAsync();有道無術,術可成;有術無道,止于術
歡迎大家關注Java之道公眾號
好文章,我在看??
總結
以上是生活随笔為你收集整理的Spring官方都说废掉GuavaCache用Caffeine,你还不换?的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: PHP保留小数的相关方法
- 下一篇: jmeter中生成UUID作为唯一标识符