1. 程式人生 > >一文深入瞭解史上最強的Java堆內快取框架Caffeine

一文深入瞭解史上最強的Java堆內快取框架Caffeine

> 它提供了一個近乎最佳的命中率。從效能上秒殺其他一堆程序內快取框架,Spring5更是為了它放棄了使用多年的GuavaCache 快取,在我們的日常開發中用的非常多,是我們應對各種效能問題支援高併發的一大利器。我們熟知的快取有堆快取(Ehcache3.x、Guava Cache等)、堆外快取(Ehcache3.x、MapDB等)、分散式快取(Redis、 memcached等)等等。今天要上場的主角是Caffeine,它其實是Google基於Java8對GuavaCache的重寫升級版本,支援豐富的快取過期策略,尤其是TinyLfu 淘汰演算法,提供了一個**近乎最佳的命中率**。從效能上(讀、寫、讀/寫)也足以**秒殺**其他一堆程序內快取框架。Spring5更是直接放棄了使用了多年的Guava,而採用了Caffeine。 ![在這裡插入圖片描述](https://img-blog.csdnimg.cn/20200315122650524.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0phNW9u,size_16,color_FFFFFF,t_70)(以上資料來自官方讀寫效能測試結果,更多測試結果詳見 https://github.com/ben-manes/caffeine/wiki/Benchmarks) 當然在實際使用中基本會涉及中多個快取的組合使用,比如二級快取(Caffeine+Redis)、多級快取等等,這個以後再講。接下來我們分【基礎實戰】、【高階用法】、【理論概述】三個部分來聊一聊史上最強的Java堆內快取框架。 (在“碼大叔”公眾號回覆數字136即可獲取演示原始碼及牛逼的TinyLfu論文。論文版權歸原作者所有,向大神學習致敬) # ====基礎實戰==== 接下來我們通過一些例子來演示Caffeine的基礎用法,首先我們通springboot新建一個mds-caffeine-demo的Gradle工程。 ### 一、基礎配置 #### 1、新增依賴 需要使用到 spring-boot-starter-cache和caffeine兩個包 ```java implementation 'org.springframework.boot:spring-boot-starter-cache' implementation 'com.github.ben-manes.caffeine:caffeine' ``` #### 2、在applicationyml檔案中新增配置 ```yaml spring: cache: type: caffeine ``` #### 3、添加註解 在啟動類上新增@EnableCaching ![在這裡插入圖片描述](https://img-blog.csdnimg.cn/20200315123149466.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0phNW9u,size_16,color_FFFFFF,t_70) 就是這麼地 so easy,Caffeine就已經整合到我們的專案中來了。 ## 二、實戰演示 假設我們資料庫中有一張User表,裡面有【碼大叔和小九九】2條資料 |id| name |birdhtday| |--|--|--| |1 | 碼大叔 |2012-05-12| |2 | 小九九 |1999-09-19| ### 場景1:新增及使用快取 只需要使用@Cacheable註解即可自動將資料新增到快取中,後續直接從快取中讀取資料。 **value**:表示快取的名稱,這個引數value還是比較誤導人的,不是快取的值,所以官方還提供了一種寫法:cacheNames。 **key**:表示快取的key,可以為空。如果指定需要按照SpEL表示式編寫 #### 方法1、將使用者物件以ID作為key存放到快取中。 ![在這裡插入圖片描述](https://img-blog.csdnimg.cn/20200315123520904.png) 我們訪問頁面: ![在這裡插入圖片描述](https://img-blog.csdnimg.cn/2020031512353339.png) 第一次:列印了資料庫操作的日誌 ![在這裡插入圖片描述](https://img-blog.csdnimg.cn/2020031512355379.png) 第二次:沒有列印,表示快取新增成功。 ### 方法2、將滿足條件的資料存放到快取中 @Cacheable有一個引數叫做condition,該條件為true時則放到快取到。該引數同樣需使用SpEL表示式。 ![在這裡插入圖片描述](https://img-blog.csdnimg.cn/20200315123618888.png) 接下來我們分別進行使用者1、使用者2、使用者1、使用者2 四次查詢。我們看到只打印了3條資料,第二次訪問使用者1從快取中讀取資料,使用者2每次都是從資料庫中讀取資料,沒進入快取。 ![在這裡插入圖片描述](https://img-blog.csdnimg.cn/20200315123632743.png) **【敲黑板】** - 還有一個條件引數unless,與condition的用法恰好相反。 - 使用了條件式快取後,哪怕哪怕快取裡已經有資料了,也依然會跳過快取。比如我們在其他方法中將“小九九”新增到了快取中,但通過該方法獲取小九九的資料時,依然是從資料庫中取值。 - @Cacheable註解不僅僅可以標記在一個方法上,還可以標記在一個類上,表示該類所有的方法都是支援快取的。 - 我們除了使用引數作為key之外,Spring還為我們提供了一個root物件可以用來生成key,比如 #root.methodName(當前方法名), #root.target(當前被呼叫的物件), #root.args[0]( #root.args[0])等等。 ### 場景2:更新快取 使用@CachePut,添加了該註解後每次都會觸發真實方法的呼叫 ![在這裡插入圖片描述](https://img-blog.csdnimg.cn/20200315123810804.png) 我們覺得碼大叔的年齡可能造假了,怎麼可能是2012年,把它更新為真實的年齡。 ![在這裡插入圖片描述](https://img-blog.csdnimg.cn/20200315123831146.png) 我們看到資料庫層面列印了日誌。 ![在這裡插入圖片描述](https://img-blog.csdnimg.cn/20200315123855343.png) 此時我們再訪問獲取使用者資訊方法,已經獲取到了最新的資料,但服務端卻沒有任何日誌。 ![在這裡插入圖片描述](https://img-blog.csdnimg.cn/20200315123935275.png) 這表明該註解已幫我們把最新的資訊更新到了快取中。 **【敲黑板】** - 在方法上使用了@CachePut註解如果方法返回了void或者null,也會同步更新快取,快取的物件為空,所以使用時務必要注意。快取預設是支援儲存nul的,這也符合我們使用快取的訴求。如果在某些特殊的場景下不希望快取null物件,可以使用condition條件:condition = "#result != null" 即可。 ### 場景3:刪除快取 使用@CacheEvict註解,可以手動將物件從快取中刪除。 ![在這裡插入圖片描述](https://img-blog.csdnimg.cn/20200315124025549.png) 比如上面的方法,表示將指定id的使用者從快取中刪除。如果期望將USER的所有快取刪除,則可以使用引數 allEntries = true(預設為false) 即可。 **【敲黑板】** - 如果方法裡有程式碼邏輯,那麼是先刪除快取還是先執行方法呢?答案是先執行方法,後清除快取。如果期望先清除快取後執行方法,則新增引數 beforeInvocation = true即可。 # ==高階用法== ## 1:執行緒鎖定 前面我們提到了@Cacheable可以新增快取,當快取過期之後如果多個執行緒同時請求過來,而該方法執行較慢時可能會導致大量請求堆積,甚至導致快取瞬間被擊穿,所有請求同時去到資料庫,資料庫瞬間負荷增高。所以該註解還提供了一個引數 sync:預設為false,如果為true時表示多個執行緒同時呼叫此時只有一個執行緒能夠成功呼叫,其他執行緒直接取這次呼叫的返回值。不過它在程式碼註釋上也寫了,這僅僅是個hint,具體還是要看快取提供者。 ![在這裡插入圖片描述](https://img-blog.csdnimg.cn/20200315124207128.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0phNW9u,size_16,color_FFFFFF,t_70) 不管sync設定是true還是false,Caffeine預設使用的都是單執行緒 :只允許一個執行緒去載入資料,其餘執行緒阻塞。這樣其實也會導致效率低下,使用者等待。因此建議配合refreshAfterWrite一起使用:只阻塞載入資料的執行緒,其餘執行緒返回舊資料。 ## 2:快取失效 初始化快取時,我們還可以設定3個引數:expireAfterAccess、expireAfterWrite、refreshAfterWrite。千萬不要被這三個單詞的表面意思誤導,網上很多寫法也是錯的。比如expireAfterAccess,不是表示訪問完多長時間就過期,而是多長時間沒有訪問就失效。 - expireAfterAccess=[duration]:指在指定時間內沒有被讀或寫就回收 - expireAfterWrite=[duration]: 指在指定時間內沒有被建立或覆蓋就回收 - refreshAfterWrite=[duration]:指在指定時間內沒有被建立/覆蓋,則指定時間過後再次訪問時會去重新整理該快取,在新值沒有到來之前,始終返回舊值 我們以expireAfterWrite為例,配置如下,然後不停地訪問,我們看到每隔5秒後就自動更新一次快取。 ![在這裡插入圖片描述](https://img-blog.csdnimg.cn/20200315124306146.png)![在這裡插入圖片描述](https://img-blog.csdnimg.cn/20200315124318126.png) **【敲黑板】** - 如果是yml檔案要注意寫法,這幾個都是spec的value值,caffeine會自行解析,不要像下面這種寫法,是錯誤的。 ![在這裡插入圖片描述](https://img-blog.csdnimg.cn/20200315124408678.png) - 以expireAfterWrite為例,假設設定的是5秒,並不是指5秒後自動更新,而是在5秒後的下一次訪問時才更新 - 如果expireAfterWrite和expireAfterAccess同時存在,以expireAfterWrite為準。 ## 3:refreshAfterWrite 這個引數在前面也提到了在日常使用中用的比較多,尤其是對於網際網路高併發的場景,所以額外再補充講幾點。 1、使用了refreshAfterWrite後,啟動專案會報如下的錯誤, ```java 2020-03-08 13:51:51,144|o.s.boot.SpringApplication|reportFailure|Application run failed org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'cacheManager' defined in class path resource [org/springframework/boot/autoconfigure/cache/CaffeineCacheConfiguration.class]: Bean instantiation via factory method failed; nested exception is org.springframework.beans.BeanInstantiationException: Failed to instantiate [org.springframework.cache.caffeine.CaffeineCacheManager]: Factory method 'cacheManager' threw exception; nested exception is java.lang.IllegalStateException: refreshAfterWrite requires a LoadingCache at org.springframework.beans.factory.support.ConstructorResolver.instantiate(ConstructorResolver.java:656) at com.qiaojs.mds.MDSApplication.main(MDSApplication.java:16) Caused by: org.springframework.beans.BeanInstantiationException: Failed to instantiate [org.springframework.cache.caffeine.CaffeineCacheManager]: Factory method 'cacheManager' threw exception; nested exception is java.lang.IllegalStateException: refreshAfterWrite requires a LoadingCache ... 19 common frames omitted Caused by: java.lang.IllegalStateException: refreshAfterWrite requires a LoadingCache ... 20 common frames omitted ``` 這需要我們去實現一個CacheLoader,再重啟就OK了。 ```java @Bean public Cac