Caffeine 組件最強講解!

結論:Caffeine 是目前性能最好的本地緩存,因此,在考慮使用本地緩存時,直接選擇 Caffeine 即可。

先看一個小例子,明白如何創建一個 Caffeine 緩存實例。

Caffeine caffeine = Caffeine.newBuilder()
        .initialCapacity(3)
        .maximumSize(4);
Cache cache = caffeine.build();
cache.put("aa", 13);
System.out.println(cache.getIfPresent("aa"));

Caffeine 相當於一個緩存工廠,可以創建出多個緩存實例 Cache。這些緩存實例都繼承了 Caffeine 的參數配置,Caffeine 是如何配置的,這些緩存實例就具有什麼樣的特性和功能。

1. Caffeine 可以設置哪些緩存屬性呢?

1. 緩存初始容量

initialCapacity:整數,表示能存儲多少個緩存對象。

爲什麼要設置初始容量呢?因爲如果提前能預估緩存的使用大小,那麼可以設置緩存的初始容量,以免緩存不斷地進行擴容,致使效率不高。

2. 最大容量 最大權重

maximumSize:最大容量,如果緩存中的數據量超過這個數值,Caffeine 會有一個異步線程來專門負責清除緩存,按照指定的清除策略來清除掉多餘的緩存。

注意:比如最大容量是 2,此時已經存入了 2 個數據了,此時存入第 3 個數據,觸發異步線程清除緩存,在清除操作沒有完成之前,緩存中仍然有 3 個數據,且 3 個數據均可讀,緩存的大小也是 3,只有當緩存操作完成了,緩存中才只剩 2 個數據,至於清除掉了哪個數據,這就要看清除策略了。

maximumWeight:最大權重,存入緩存的每個元素都要有一個權重值,當緩存中所有元素的權重值超過最大權重時,就會觸發異步清除。

下面給個例子:

class Person{
        Integer age;
        String name;
}
Caffeine<String, Person> caffeine = Caffeine.newBuilder()
            .maximumWeight(30)
            .weigher((String key, Person value)-> value.getAge());
Cache<String, Person> cache = caffeine.build();
cache.put("one", new Person(12, "one"));
cache.put("two", new Person(18, "two"));
cache.put("three", new Person(1, "three"));
Thread.sleep(10);
System.out.println(cache.estimatedSize());
System.out.println(cache.getIfPresent("two"));

運行結果:

2
null

要使用權重來衡量的話,就要規定權重是什麼,每個元素的權重怎麼計算,weigher 方法就是設置權重規則的,它的參數是一個函數,函數的參數是 key 和 value,函數的返回值就是元素的權重,比如上述代碼中,caffeine 設置了最大權重值爲 30,然後將每個 Person 對象的 age 年齡作爲權重值,所以整個意思就是:緩存中存儲的是 Person 對象,但是限制所有對象的 age 總和不能超過 30,否則就觸發異步清除緩存。

特別要注意一點:最大容量 和 最大權重 只能二選一作爲緩存空間的限制。

3. 緩存狀態

3.1 默認的緩存狀態收集器 CacheStats

默認情況下,緩存的狀態會用一個 CacheStats 對象記錄下來,通過訪問 CacheStats 對象就可以知道當前緩存的各種狀態指標,那究竟有哪些指標呢?

先說一下什麼是 “加載”,當查詢緩存時,緩存未命中,那就需要去第三方數據庫中查詢,然後將查詢出的數據先存入緩存,再返回給查詢者,這個過程就是加載。

CacheStats 類包含了 2 個方法,瞭解一下:

舉個例子說明:

Caffeine<String, Person> caffeine = Caffeine.newBuilder()
            .maximumWeight(30)
            .recordStats()
            .weigher((String key, Person value)-> value.getAge());
Cache<String, Person> cache = caffeine.build();
cache.put("one", new Person(12, "one"));
cache.put("two", new Person(18, "two"));
cache.put("three", new Person(1, "three"));
CacheStats stats = cache.stats();

System.out.println(stats.hitCount());

3.2 自定義的緩存狀態收集器

自定義的緩存狀態收集器的作用:每當緩存有操作發生時,不管是查詢,加載,存入,都會使得緩存的某些狀態指標發生改變,哪些狀態指標發生了改變,就會自動觸發收集器中對應的方法執行,如果我們在方法中自定義的代碼是收集代碼,比如將指標數值發送到 kafka,那麼其它程序從 kafka 讀取到數值,再進行分析與可視化展示,就能實現對緩存的實時監控了。

收集器接口爲 StatsCounter ,我們只需實現這個接口的所有抽象方法即可。下面舉例說明。

public class MyStatsCounter implements StatsCounter {
    @Override
    public void recordHits(int i) {
        System.out.println("命中次數:" + i);
    }

    @Override
    public void recordMisses(int i) {
        System.out.println("未命中次數:" + i);
    }

    @Override
    public void recordLoadSuccess(long l) {
        System.out.println("加載成功次數:" + l);
    }

    @Override
    public void recordLoadFailure(long l) {
        System.out.println("加載失敗次數:" + l);
    }

    @Override
    public void recordEviction() {
        System.out.println("因爲緩存大小限制,執行了一次緩存清除工作");
    }

    @Override
    public void recordEviction(int weight) {
        System.out.println("因爲緩存權重限制,執行了一次緩存清除工作,清除的數據的權重爲:" + weight);
    }

    @Override
    public CacheStats snapshot() {
        return null;
    }
}

上述代碼爲自定義的緩存狀態收集器,收集到的狀態指標只是簡單地打印出來,snapshot 方法有什麼作用,暫時不清楚。

特別需要注意的是:收集器中那些方法得到的狀態值,只是當前緩存操作所產生的結果,比如當前 cache.getIfPresent() 查詢一個值,查詢到了,說明命中了,但是 recordHits(int i) 方法的參數 i = 1,因爲本次操作命中了 1 次。

再將收集器與某個緩存掛鉤,如下:

MyStatsCounter myStatsCounter = new MyStatsCounter();
Caffeine<String, Person> caffeine = Caffeine.newBuilder()
        .maximumWeight(30)
        .recordStats(()->myStatsCounter)
        .weigher((String key, Person value)-> value.getAge());
Cache<String, Person> cache = caffeine.build();
cache.put("one", new Person(12, "one"));
cache.put("two", new Person(18, "two"));
cache.put("three", new Person(1, "three"));
cache.getIfPresent("ww");
CacheStats stats = myStatsCounter.snapshot();
Thread.sleep(1000);

最後的執行結果爲:

未命中次數:1
因爲緩存權重限制,執行了一次緩存清除工作,清除的數據的權重爲:18
4. 線程池

Caffeine 緩衝池總有一些異步任務要執行,所以它包含了一個線程池,用於執行這些異步任務,默認使用的是 ForkJoinPool.commonPool() 線程池,個人覺得沒有必要去自定義線程池,或者使用其它的線程池,因爲 Caffeine 的作者在設計的時候就考慮了線程池的選擇,既然別人選擇了,就有一定道理。

如果一定要用其它的線程池,可以通過 executor() 方法設置,方法參數是一個 線程池對象。

5. 數據過期策略

5.1 expireAfterAccess

最後一次訪問之後,隔多久沒有被再次訪問的話,就過期。訪問包括了 讀 和 寫。舉個例子:

Caffeine<String, Person> caffeine = Caffeine.newBuilder()
        .maximumWeight(30)
        .expireAfterAccess(2, TimeUnit.SECONDS)
        .weigher((String key, Person value)-> value.getAge());
Cache<String, Person> cache = caffeine.build();
cache.put("one", new Person(12, "one"));
cache.put("two", new Person(18, "two"));
Thread.sleep(3000);
System.out.println(cache.getIfPresent("one"));
System.out.println(cache.getIfPresent("two"));

運行結果:

null
null

expireAfterAccess 包含兩個參數,第二個參數是時間單位,第一個參數是時間大小,比如上述代碼中設置過期時間爲 2 秒,在過了 3 秒之後,再次訪問數據,發現數據不存在了,即觸發過期清除了。

5.2 expireAfterWrite

某個數據在多久沒有被更新後,就過期。舉個例子

Caffeine<String, Person> caffeine = Caffeine.newBuilder()
        .maximumWeight(30)
        .expireAfterWrite(2, TimeUnit.SECONDS)
        .weigher((String key, Person value)-> value.getAge());
Cache<String, Person> cache = caffeine.build();
cache.put("one", new Person(12, "one"));
cache.put("two", new Person(18, "two"));
Thread.sleep(1000);
System.out.println(cache.getIfPresent("one").getName());
Thread.sleep(2000);
System.out.println(cache.getIfPresent("one"));

運行結果:

one
null

只能是被更新,才能延續數據的生命,即便是數據被讀取了,也不行,時間一到,也會過期。

5.2 expireAfter

實話實說,關於這個設置項,官網沒有說明白,網上其它博客更是千篇一律,沒有一個講明白的。此處簡單講講我個人的測試用例與理解,如果有誤,歡迎評論指正。

Caffeine<String, Person> caffeine = Caffeine.newBuilder()
        .maximumWeight(30)
        .expireAfter(new Expiry<String, Person>() {
            @Override
            public long expireAfterCreate(String s, Person person, long l) {
                if(person.getAge() > 60){ //首次存入緩存後,年齡大於 60 的,過期時間爲 4 秒
                    return 4000000000L;
                }
                return 2000000000L; // 否則爲 2 秒
            }

            @Override
            public long expireAfterUpdate(String s, Person person, long l, long l1) {
                if(person.getName().equals("one")){ // 更新 one 這個人之後,過期時間爲 8 秒
                    return 8000000000L;
                }
                return 4000000000L; // 更新其它人後,過期時間爲 4 秒
            }

            @Override
            public long expireAfterRead(String s, Person person, long l, long l1) {
                return 3000000000L; // 每次被讀取後,過期時間爲 3 秒
            }
        })
        .weigher((String key, Person value)-> value.getAge());
Cache<String, Person> cache = caffeine.build();

expireAfter 方法的參數是一個 Expiry 對象,Expiry 是一個接口,上述代碼用了匿名類。需要實現 Expiry 的三個方法。

6. refreshAfterWrite 延遲刷新
refreshAfterWrite(long duration, TimeUnit unit)

寫操作完成後多久纔將數據刷新進緩存中,兩個參數只是用於設置時間長短的。

只適用於 LoadingCacheAsyncLoadingCache,如果刷新操作沒有完成,讀取的數據只是舊數據。 同理,不想寫了。

7. removalListener 清除、更新監聽

當緩存中的數據發送更新,或者被清除時,就會觸發監聽器,在監聽器裏可以自定義一些處理手段,比如打印出哪個數據被清除,原因是什麼。這個觸發和監聽的過程是異步的,就是說可能數據都被刪除一小會兒了,監聽器才監聽到。

舉個例子:

MyStatsCounter myStatsCounter = new MyStatsCounter();
Caffeine<String, Person> caffeine = Caffeine.newBuilder()
        .maximumWeight(30)
        .removalListener((String key, Person value, RemovalCause cause)->{
            System.out.println("被清除人的年齡:" + value.getAge() + ";  清除的原因是:" + cause);
        })
        .weigher((String key, Person value)-> value.getAge());
Cache<String, Person> cache = caffeine.build();
cache.put("one", new Person(12, "one"));
cache.put("two", new Person(18, "two"));
cache.put("one", new Person(14, "one"));
cache.invalidate("one");
cache.put("three", new Person(31, "three"));
Thread.sleep(2000);

運行結果:

被清除人的年齡:12;  清除的原因是:REPLACED
被清除人的年齡:14;  清除的原因是:EXPLICIT
被清除人的年齡:18;  清除的原因是:SIZE

removalListener 方法的參數是一個 RemovalListener 對象,但是可以函數式傳參,如上述代碼,當數據被更新或者清除時,會給監聽器提供三個內容,(鍵,值,原因)分別對應代碼中的三個參數,(鍵,值)都是更新前,清除前的舊值, 這樣可以瞭解到清除的詳細了。

清除的原因有 5 個,存儲在枚舉類 RemovalCause 中:

8. 緩存的數據使用弱引用,軟引用

AsyncCache 緩存不支持軟引用和弱引用。

關於軟引用,弱引用,強引用,虛引用,可以參考:

https://blog.csdn.net/dgh112233/article/details/107288545

因此,弱引用 ,軟引用的設置,只是爲了方便回收空間,節省空間,但是使用的時候注意一點,緩存查詢時,是用 == 來判斷兩個 key 是否相等,比較的是地址,不是 key 本身的內容,很容易造成一種現象:命名 key 是對的,但就是無法命中,因爲 key 的內容相等,但是地址卻不同,會被認爲是兩個 key。

9. 時間源 ticker

不瞭解,感覺默認用系統的時鐘就好了。

10. 同步監聽器

之前的 removalListener 是異步監聽,此處的 writer 方法可以設置同步監聽器,同步監聽器一個實現了接口 CacheWriter 的實例化對象,我們需要自定義接口的實現類,比如:

public class MyCacheWriter implements CacheWriter<String, Application.Person> {
    @Override
    public void write(String s, Application.Person person) {
        System.out.println("新增/更新了一個新數據:" + person.getName());
    }

    @Override
    public void delete(String s, Application.Person person, RemovalCause removalCause) {
        System.out.println("刪除了一個數據:" + person.getName());
    }
}

關鍵是要實現 CacheWriter 接口的兩個方法,當新增,更新某個數據時,會同步觸發 write 方法的執行。當刪除某個數據時,會觸發 delete 方法的執行。

Caffeine<String, Person> caffeine = Caffeine.newBuilder()
        .maximumWeight(30)
        .writer(new MyCacheWriter())
        .weigher((String key, Person value)-> value.getAge());
Cache<String, Person> cache = caffeine.build();
cache.put("one", new Person(12, "one"));
cache.put("two", new Person(18, "two"));
cache.invalidate("two");

運行結果:

新增/更新了一個新數據:one
新增/更新了一個新數據:two
刪除了一個數據:two

2. Cache 可以有的操作

Caffeine<String, Person> caffeine = Caffeine.newBuilder()
        .maximumWeight(30)
        .weigher((String key, Person value)-> value.getAge());
Cache<String, Person> cache = caffeine.build();
cache.put("one", new Person(12, "one"));
cache.get("hello"(k)-> new Person(13, k));
System.out.println(cache.getIfPresent("hello").getName());

可以着重考慮一下第二個參數的寫法,如果寫成從數據庫查詢的話,那就很完整了。

還有另外兩種緩存:LoadingCache, AsyncLoadingCache。

來源:blog.csdn.net/dgh112233/article/

details/118915259

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/JBXtlYHGX3gfV9ZOfqR2nw