本地緩存性能之王Caffeine

前言

隨着互聯網的高速發展,市面上也出現了越來越多的網站和app。我們判斷一個軟件是否好用,用戶體驗就是一個重要的衡量標準。比如說我們經常用的微信,打開一個頁面要十幾秒,發個語音要幾分鐘對方纔能收到。相信這樣的軟件大家肯定是都不願意用的。軟件要做到用戶體驗好,響應速度快,緩存就是必不可少的一個神器。緩存又分進程內緩存和分佈式緩存兩種:分佈式緩存如redis、memcached等,還有本地(進程內)緩存如ehcache、GuavaCache、Caffeine等。說起Guava Cache,很多人都不會陌生,它是Google Guava工具包中的一個非常方便易用的本地化緩存實現,基於LRU算法實現,支持多種緩存過期策略。由於Guava的大量使用,Guava Cache也得到了大量的應用。但是,Guava Cache的性能一定是最好的嗎?也許,曾經它的性能是非常不錯的。正所謂長江後浪推前浪,前浪被拍在沙灘上。我們就來介紹一個比Guava Cache性能更高的緩存框架:Caffeine

Tips: Spring5(SpringBoot2)開始用Caffeine取代guava.詳見官方信息SPR-13797
https://jira.spring.io/browse/SPR-13797

官方性能比較

以下測試都是基於jmh測試的,官網地址
測試爲什麼要基於jmh測試,可以參考知乎上R回答

在HotSpot VM上跑microbenchmark切記不要在main()裏跑循環計時就完事。這是典型錯誤。重要的事情重複三遍:請用JMH,請用JMH,請用JMH。除非非常瞭解HotSpot的實現細節,在main裏這樣跑循環計時得到的結果其實對一般程序員來說根本沒有任何意義,因爲無法解釋。

  • 8個線程讀,100%的讀操作

  • 6個線程讀,2個線程寫,也就是75%的讀操作,25%的寫操作。

  • 8個線程寫,100%的寫操作

對比結論

可以從數據看出來Caffeine的性能都比Guava要好。然後Caffeine的API的操作功能和Guava是基本保持一致的,並且  Caffeine爲了兼容之前是Guava的用戶,做了一個Guava的Adapter給大家使用也是十分的貼心。

如何使用

  • 在 pom.xml 中添加 caffeine 依賴

1<!-- https://mvnrepository.com/artifact/com.github.ben-manes.caffeine/caffeine -->
2<dependency>
3    <groupId>com.github.ben-manes.caffeine</groupId>
4    <artifactId>caffeine</artifactId>
5    <version>2.8.2</version>
6</dependency>

創建對象

1 Cache<String, Object> cache = Caffeine.newBuilder()
2                .initialCapacity(100)//初始大小
3                .maximumSize(200)//最大數量
4                .expireAfterWrite(3, TimeUnit.SECONDS)//過期時間
5                .build();
創建參數介紹
  • initialCapacity: 初始的緩存空間大小

  • maximumSize: 緩存的最大數量

  • maximumWeight: 緩存的最大權重

  • expireAfterAccess: 最後一次讀或寫操作後經過指定時間過期

  • expireAfterWrite: 最後一次寫操作後經過指定時間過期

  • refreshAfterWrite: 創建緩存或者最近一次更新緩存後經過指定時間間隔,刷新緩存

  • weakKeys: 打開key的弱引用

  • weakValues:打開value的弱引用

  • softValues:打開value的軟引用

  • recordStats:開發統計功能

注意:
expireAfterWrite和expireAfterAccess同時存在時,以expireAfterWrite爲準。
maximumSize和maximumWeight不可以同時使用。

添加數據

Caffeine 爲我們提供了手動、同步和異步這幾種填充策略。
下面我們來演示下手動填充策略吧,其他幾種如果大家感興趣的可以去官網瞭解下

1  Cache<String, String> cache = Caffeine.newBuilder()
2                .build();
3        cache.put("java金融", "java金融");
4        System.out.println(cache.getIfPresent("java金融"));

自動添加(自定義添加函數)

 1  public static void main(String[] args) {
 2        Cache<String, String> cache = Caffeine.newBuilder()
 3                .build();
 4        // 1.如果緩存中能查到,則直接返回
 5        // 2.如果查不到,則從我們自定義的getValue方法獲取數據,並加入到緩存中
 6        String val = cache.get("java金融", k -> getValue(k));
 7        System.out.println(val);
 8    }
 9    /**
10     * 緩存中找不到,則會進入這個方法。一般是從數據庫獲取內容
11     * @param k
12     * @return
13     */
14    private static String getValue(String k) {
15        return k + ":value";
16    }

過期策略

Caffeine 爲我們提供了三種過期策略
,分別是基於大小(size-based)、基於時間(time-based)、基於引用(reference-based)

基於大小(size-based)
 1      LoadingCache<String, String> cache = Caffeine.newBuilder()
 2                // 最大容量爲1
 3                .maximumSize(1)
 4                .build(k->getValue(k));
 5        cache.put("java金融1","java金融1");
 6        cache.put("java金融2","java金融2");
 7        cache.put("java金融3","java金融3");
 8        cache.cleanUp();
 9        System.out.println(cache.getIfPresent("java金融1"));
10        System.out.println(cache.getIfPresent("java金融2"));
11        System.out.println(cache.getIfPresent("java金融3"));

運行結果如下:淘汰了兩個只剩下一個。

1null
2null
3java金融3
基於時間(time-based)

Caffeine提供了三種定時驅逐策略:

expireAfterWrite(long, TimeUnit)
  • 在最後一次寫入緩存後開始計時,在指定的時間後過期。

 1  LoadingCache<String, String> cache =  Caffeine.newBuilder()
 2                // 最大容量爲1
 3                .maximumSize(1)
 4                .expireAfterWrite(3, TimeUnit.SECONDS)
 5                .build(k->getValue(k));
 6        cache.put("java金融","java金融");
 7        Thread.sleep(1*1000);
 8        System.out.println(cache.getIfPresent("java金融"));
 9        Thread.sleep(1*1000);
10        System.out.println(cache.getIfPresent("java金融"));
11        Thread.sleep(1*1000);
12        System.out.println(cache.getIfPresent("java金融"));

運行結果第三秒的時候取值爲空。

1java金融
2java金融
3null
expireAfterAccess
  • 在最後一次讀或者寫入後開始計時,在指定的時間後過期。假如一直有請求訪問該key,那麼這個緩存將一直不會過期。

 1LoadingCache<String, String> cache =  Caffeine.newBuilder()
 2                // 最大容量爲1
 3                .maximumSize(1)
 4                .expireAfterAccess(3, TimeUnit.SECONDS)
 5                .build(k->getValue(k));
 6        cache.put("java金融","java金融");
 7        Thread.sleep(1*1000);
 8        System.out.println(cache.getIfPresent("java金融"));
 9        Thread.sleep(1*1000);
10        System.out.println(cache.getIfPresent("java金融"));
11        Thread.sleep(1*1000);
12        System.out.println(cache.getIfPresent("java金融"));
13        Thread.sleep(3001);
14        System.out.println(cache.getIfPresent("java金融"));

運行結果:讀和寫都沒有的情況下,3秒後才過期,然後就輸出了null。

1java金融
2java金融
3java金融
4null
expireAfter(Expiry)
  • 在expireAfter中需要自己實現Expiry接口,這個接口支持expireAfterCreate,expireAfterUpdate,以及expireAfterRead了之後多久過期。注意這個是和expireAfterAccess、expireAfterAccess是互斥的。這裏和expireAfterAccess、expireAfterAccess不同的是,需要你告訴緩存框架,他應該在具體的某個時間過期,獲取具體的過期時間。

 1 LoadingCache<String, String> cache = Caffeine.newBuilder()
 2                // 最大容量爲1
 3                .maximumSize(1)
 4                .removalListener((key, value, cause) ->
 5                        System.out.println("key:" + key + ",value:" + value + ",刪除原因:" + cause))
 6                .expireAfter(new Expiry<String, String>() {
 7                    @Override
 8                    public long expireAfterCreate(@NonNull String key, @NonNull String value, long currentTime) {
 9                        return currentTime;
10                    }
11                    @Override
12                    public long expireAfterUpdate(@NonNull String key, @NonNull String value, long currentTime, @NonNegative long currentDuration) {
13                        return currentTime;
14                    }
15
16                    @Override
17                    public long expireAfterRead(@NonNull String key, @NonNull String value, long currentTime, @NonNegative long currentDuration) {
18                        return currentTime;
19                    }
20                })
21                .build(k -> getValue(k));

刪除

  • 單個刪除:Cache.invalidate(key)

  • 批量刪除:Cache.invalidateAll(keys)

  • 刪除所有緩存項:Cache.invalidateAll

總結

本文只是對Caffeine的一個簡單使用的介紹,它還有很多不錯的東西,比如緩存監控、事件監聽、W-TinyLFU算法(高命中率、低內存佔用)感興趣的同學可以去官網查看。

結束

  • 由於自己才疏學淺,難免會有紕漏,假如你發現了錯誤的地方,還望留言給我指出來,我會對其加以修正。

  • 如果你覺得文章還不錯,你的轉發、分享、讚賞、點贊、留言就是對我最大的鼓勵。

  • 感謝您的閱讀,十分歡迎並感謝您的關注。

    我建了一個羣,羣裏有很多大佬,歡迎大家入羣探討

參考

https://www.itcodemonkey.com/article/9498.html
https://juejin.im/post/5dede1f2518825121f699339
https://www.cnblogs.com/CrankZ/p/10889859.html
https://blog.csdn.net/hy245120020/article/details/78080686

https://www.zhihu.com/question/58735131/answer/307771944

往期推薦

  超長JVM總結,面試必備

  ArrayList哪種遍歷效率最好,你真的弄明白了嗎?

  史上最全 Java 中各種鎖的介紹

  有了它(powerMocker)再也不怕單元測試不達標了!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章