JVM進程緩存Caffeine的使用
一、前言
Caffeine是當前最優(yōu)秀的內存緩存框架,不論讀還是寫的效率都遠高于其他緩存,而且在Spring5開始的默認緩存實現(xiàn)就將Caffeine代替原來的Google Guava
二、基本使用
<dependency> <groupId>com.github.ben-manes.caffeine</groupId> <artifactId>caffeine</artifactId> </dependency>
2.1 手動創(chuàng)建緩存
void test1() { Cache<Object, Object> cache = Caffeine.newBuilder() // 初始數(shù)量 .initialCapacity(10) // 最大條數(shù) .maximumSize(10) // expireAfterWrite和expireAfterAccess同時存在時,以expireAfterWrite為準 // 最后一次寫操作后經過指定時間過期 .expireAfterWrite(1, TimeUnit.SECONDS) // 最后一次讀或寫操作后經過指定時間過期 .expireAfterAccess(1, TimeUnit.SECONDS) // 監(jiān)聽緩存被移除 .removalListener((key, value, cause) -> {}) // 記錄命中 .recordStats() .build(); cache.put("1", "張三"); System.out.println(cache.asMap()); System.out.println(cache.getIfPresent("1")); System.out.println(cache.get("2", o -> "默認值")); }
運行結果
{1=張三}
張三
默認值
2.2 異步獲取緩存
@Test void test2() { AsyncLoadingCache<String, String> asyncLoadingCache = Caffeine.newBuilder() // 創(chuàng)建緩存或者最近一次更新緩存后經過指定時間間隔刷新緩存:僅支持LoadingCache .refreshAfterWrite(1, TimeUnit.SECONDS) .expireAfterWrite(1, TimeUnit.SECONDS) .expireAfterAccess(1, TimeUnit.SECONDS) .maximumSize(10) // 根據key查詢數(shù)據庫里面的值 .buildAsync(key -> { Thread.sleep(1000); return new Date().toString(); }); // 異步緩存返回的是CompletableFuture CompletableFuture<String> future = asyncLoadingCache.get("1"); future.thenAccept(System.out::println); }
2.3 記錄命中數(shù)據
@Test void test3() { ? ? LoadingCache<String, String> cache = Caffeine.newBuilder() ? ? ? ? ? ? // 創(chuàng)建緩存或者最近一次更新緩存后經過指定時間間隔,刷新緩存:refreshAfterWrite僅支持LoadingCache ? ? ? ? ? ? .refreshAfterWrite(1, TimeUnit.SECONDS) ? ? ? ? ? ? .expireAfterWrite(1, TimeUnit.SECONDS) ? ? ? ? ? ? .expireAfterAccess(1, TimeUnit.SECONDS) ? ? ? ? ? ? .maximumSize(10) ? ? ? ? ? ? // 開啟記錄緩存命中率等信息 ? ? ? ? ? ? .recordStats() ? ? ? ? ? ? // 根據key查詢數(shù)據庫里面的值 ? ? ? ? ? ? .build(key -> { ? ? ? ? ? ? ? ? TimeUnit.MILLISECONDS.sleep(1000); ? ? ? ? ? ? ? ? return new Date().toString(); ? ? ? ? ? ? }); ? ? cache.put("1", "小明"); ? ? cache.get("1"); ? ? /* ? ? ?* hitCount :命中的次數(shù) ? ? ?* missCount:未命中次數(shù) ? ? ?* requestCount:請求次數(shù) ? ? ?* hitRate:命中率 ? ? ?* missRate:丟失率 ? ? ?* loadSuccessCount:成功加載新值的次數(shù) ? ? ?* loadExceptionCount:失敗加載新值的次數(shù) ? ? ?* totalLoadCount:總條數(shù) ? ? ?* loadExceptionRate:失敗加載新值的比率 ? ? ?* totalLoadTime:全部加載時間 ? ? ?* evictionCount:丟失的條數(shù) ? ? ?*/ ? ? System.out.println(cache.stats()); }
會影響性能,生產環(huán)境下建議不開啟
三、淘汰策略
- LRU: 最近最少使用,淘汰最長時間沒有被使用的頁面;
- LFU:最不經常使用,淘汰一段時間內,使用次數(shù)最少的頁面;
- FIFO:先進先出
LRU的優(yōu)點:LRU相比于LFU而言性能更好一些,因為它算法相對比較簡單,不需要記錄訪問頻次,可以更好地應對突發(fā)流量;
LRU的缺點:雖然性能好一些,但是它通過歷史數(shù)據來預測未來是局限的,它會認為最后到來的數(shù)據是最可能被再次訪問的,從而給與它最高的優(yōu)先級。有些非熱點數(shù)據被訪問過后,占據了高優(yōu)先級,它會在緩存中占據相當長的時間,從而造成空間浪費;
LFU的優(yōu)點:LRU根據訪問頻次訪問,在大部分情況下,熱點數(shù)據的頻次肯定高于非熱點數(shù)據,所以它的命中率非常高;
LFU的缺點:LFU算法相對比較復雜,性能比LRU差。有問題的是下面這種情況,比如前一段時間微博有個熱點話題熱度非常高,就比如那種可以讓微博短時間停止服務的,于是趕緊緩存起來,LFU算法記錄了其中熱點詞的訪問頻率,可能高達十幾億,而過后很長一段時間,這個話題已經不是熱點了,新的熱點也來了,但是,新熱點話題的熱度沒辦法到達十幾億,也就是說訪問頻次沒有之前的話提高,那之前的熱點就會一直占據著緩存空間,長時間無法被剔除。
3.1 4種淘汰方式與例子
Caffeine有4種緩存淘汰設置
- 大?。〞褂肳-TinyLFU算法進行淘汰)
- 權重(大小與權重,只能二選一)
- 時間
- 引用(不常用)
// 緩存大小淘汰 @Test public void maximumSizeTest() throws InterruptedException { ? ? Cache<Object, Object> cache = Caffeine.newBuilder() ? ? ? ? ? ? // 超過10個后會使用W-TinyLFU算法進行淘汰 ? ? ? ? ? ? .maximumSize(10) ? ? ? ? ? ? .build(); ? ? for (int i = 1; i <= 10; i++) { ? ? ? ? cache.put(i, i); ? ? } ? ? // 緩存淘汰是異步的 ? ? TimeUnit.MILLISECONDS.sleep(500); ? ? // 打印還沒有被淘汰的緩存 ? ? System.out.println(cache.asMap()); } // 權重淘汰 @Test public void maximumWeightTest() throws InterruptedException { ? ? Cache<Integer, Integer> cache = Caffeine.newBuilder() ? ? ? ? ? ? // 限制總權值,若所有緩存的權重加起來>總權重就會淘汰權重小的緩存 ? ? ? ? ? ? .maximumWeight(100) ? ? ? ? ? ? .weigher((Weigher<Integer, Integer>) (key, value) -> key) ? ? ? ? ? ? .build(); ? ? // 總權重其實是=所有緩存的權重加起來 ? ? int maximumWeight = 0; ? ? for (int i = 1; i < 20; i++) { ? ? ? ? cache.put(i, i); ? ? ? ? maximumWeight += i; ? ? ? ? System.out.println("i = " + i + ", maximumWeight = " + maximumWeight); ? ? } ? ? System.out.println("總權重 = " + maximumWeight); ? ? // 緩存淘汰是異步的 ? ? TimeUnit.MILLISECONDS.sleep(500); ? ? // 打印還沒有被淘汰的緩存 ? ? System.out.println(cache.asMap()); } // 訪問后到期(每次訪問都會重置時間,也就是說如果一直被訪問就不會被淘汰) @Test void expireAfterAccessTest() throws InterruptedException { ? ? Cache<Object, Object> cache = Caffeine.newBuilder() ? ? ? ? ? ? .expireAfterAccess(1, TimeUnit.SECONDS) ? ? ? ? ? ? // 可以指定調度程序來及時刪除過期緩存項,而不是等待Caffeine觸發(fā)定期維護 ? ? ? ? ? ? // 若不設置scheduler,則緩存會在下一次調用get的時候才會被動刪除 ? ? ? ? ? ? .scheduler(Scheduler.systemScheduler()) ? ? ? ? ? ? .build(); ? ? cache.put(1, 2); ? ? System.out.println(cache.getIfPresent(1)); ? ? Thread.sleep(3000); ? ? System.out.println(cache.getIfPresent(1)); } // 寫入后到期 @Test void expireAfterWriteTest() throws InterruptedException { ? ? Cache<Object, Object> cache = Caffeine.newBuilder() ? ? ? ? ? ? .expireAfterWrite(1, TimeUnit.SECONDS) ? ? ? ? ? ? // 可以指定調度程序來及時刪除過期緩存項,而不是等待Caffeine觸發(fā)定期維護 ? ? ? ? ? ? // 若不設置scheduler,則緩存會在下一次調用get的時候才會被動刪除 ? ? ? ? ? ? .scheduler(Scheduler.systemScheduler()) ? ? ? ? ? ? .build(); ? ? cache.put(1, 2); ? ? TimeUnit.MILLISECONDS.sleep(3000); ? ? System.out.println(cache.getIfPresent(1)); }
另外還有一個refreshAfterWrite()表示x秒后自動刷新緩存可以配合以上的策略使用
// 另外還有一個refreshAfterWrite()表示x秒后自動刷新緩存可以配合以上的策略使用 ? ? private static int num = 0; @Test void refreshAfterWriteTest() throws InterruptedException { ? ? LoadingCache<Object, Integer> cache = Caffeine.newBuilder() ? ? ? ? ? ? .refreshAfterWrite(1, TimeUnit.SECONDS) ? ? ? ? ? ? .build(integer -> ++num); ? ? // 獲取ID=1的值,由于緩存里還沒有,所以會自動放入緩存 ? ? System.out.println(cache.get(1)); ? ? // 延遲2秒后,理論上自動刷新緩存后取到的值是2 ? ? // 但其實不是,值還是1,因為refreshAfterWrite并不是設置了n秒后重新獲取就會自動刷新 ? ? // 而是x秒后&&第二次調用getIfPresent的時候才會被動刷新 ? ? Thread.sleep(2000); ? ? System.out.println(cache.getIfPresent(1));// 1 ? ? //此時才會刷新緩存,而第一次拿到的還是舊值 ? ? System.out.println(cache.getIfPresent(1));// 2 }
3.2 最佳實踐
實踐1
- 配置:設置maxSize、refreshAfterWrite,不設置expireAfterWrite/expireAfterAccess
- 優(yōu)缺點:因為設置expireAfterWrite當緩存過期會同步加鎖獲取緩存,所以設置expireAfterWrite時性能較好,但是某些時候會取舊數(shù)據,適合允許取到舊數(shù)據的場景
實踐2
- 配置:設置maxSize、expireAfterWrite/expireAfterAccess,不設置refreshAfterWrite
- 優(yōu)缺點:與上面相反,數(shù)據一致性好,不會獲取到舊數(shù)據,但是性能沒那么好,適合獲取數(shù)據時不耗時的場景
四、配合Redis做二級緩存
緩存的解決方案一般有三種:
- 本地內存緩存,如Caffeine、Ehcache;適合單機系統(tǒng),速度最快,但是容量有限,而且重啟系統(tǒng)后緩存丟失;
- 集中式緩存,如Redis、Memcached;適合分布式系統(tǒng),解決了容量、重啟丟失緩存等問題,但是當訪問量極大時,往往性能不是首要考慮的問題,而是帶寬?,F(xiàn)象就是Redis服務負載不高,但是由于機器網卡帶寬跑滿,導致數(shù)據讀取非常慢;
- 第三種方案就是結合以上2種方案的二級緩存應運而生,以內存緩存作為一級緩存、集中式緩存作為二級緩存
到此這篇關于JVM進程緩存Caffeine的使用的文章就介紹到這了,更多相關JVM進程緩存Caffeine內容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
Java 編程如何使用 Class.forName() 加載類
在一些應用中,無法事先知道使用者將加載什么類,而必須讓使用者指定類名稱以加載類,可以使用 Class的靜態(tài)forName()方法實現(xiàn)動態(tài)加載類,這篇文章主要介紹了Java編程如何使用Class.forName()加載類,需要的朋友可以參考下2022-06-06SpringBoot整合Dubbo+Zookeeper實現(xiàn)RPC調用
這篇文章主要給大家介紹了Spring Boot整合Dubbo+Zookeeper實現(xiàn)RPC調用的步驟詳解,文中有詳細的代碼示例,對我們的學習或工作有一定的幫助,需要的朋友可以參考下2023-07-07