<menu id="guoca"></menu>
<nav id="guoca"></nav><xmp id="guoca">
  • <xmp id="guoca">
  • <nav id="guoca"><code id="guoca"></code></nav>
  • <nav id="guoca"><code id="guoca"></code></nav>

    本地緩存之王——Caffeine 組件最強講解!

    VSole2022-12-05 08:49:34

    結論: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 caffeine = Caffeine.newBuilder()
                .maximumWeight(30)
                .weigher((String key, Person value)-> value.getAge());
    Cache 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 對象就可以知道當前緩存的各種狀態指標,那究竟有哪些指標呢?

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

    • totalLoadTime:總共加載時間。
    • loadFailureRate:加載失敗率,= 總共加載失敗次數 / 總共加載次數
    • averageLoadPenalty:平均加載時間,單位-納秒
    • evictionCount:被淘汰出緩存的數據總個數
    • evictionWeight:被淘汰出緩存的那些數據的總權重
    • hitCount:命中緩存的次數
    • hitRate:命中緩存率
    • loadCount:加載次數
    • loadFailureCount:加載失敗次數
    • loadSuccessCount:加載成功次數
    • missCount:未命中次數
    • missRate:未命中率
    • requestCount:用戶請求查詢總次數

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

    • CacheStats minus(@Nonnull CacheStats other):當前 CacheStats 對象的各項指標減去參數 other 的各項指標,差值形成一個新的 CacheStats 對象。
    • CacheStats plus(@Nonnull CacheStats other):當前 CacheStats 對象的各項指標加上參數 other 的各項指標,和值形成一個新的 CacheStats 對象。

    舉個例子說明:

    Caffeine caffeine = Caffeine.newBuilder()
                .maximumWeight(30)
                .recordStats()
                .weigher((String key, Person value)-> value.getAge());
    Cache 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 caffeine = Caffeine.newBuilder()
            .maximumWeight(30)
            .recordStats(()->myStatsCounter)
            .weigher((String key, Person value)-> value.getAge());
    Cache 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 caffeine = Caffeine.newBuilder()
            .maximumWeight(30)
            .expireAfterAccess(2, TimeUnit.SECONDS)
            .weigher((String key, Person value)-> value.getAge());
    Cache 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 caffeine = Caffeine.newBuilder()
            .maximumWeight(30)
            .expireAfterWrite(2, TimeUnit.SECONDS)
            .weigher((String key, Person value)-> value.getAge());
    Cache 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 caffeine = Caffeine.newBuilder()
            .maximumWeight(30)
            .expireAfter(new Expiry() {
                @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 cache = caffeine.build();
    

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

    • expireAfterCreate(String s, Person person, long l) :此方法為數據 創建之后,過期時間是多久(可以理解為生命周期),單位為納秒,方法的返回值就是過期時間,這個時間設置為多久,怎么設置,可以自定義的,比如上述代碼,60 歲以上的過期時間為 4 秒,如果 4 秒內數據沒有被操作,就過期。另外還有一個參數 long l,l 表示創建時間的系統時間戳,單位為納秒。
    • expireAfterUpdate(String s, Person person, long l, long l1):此方法表示更新某個數據后,過期時間是多久(刷新生命周期),個人認為:參數 l 表示更新前的系統時間戳,l1 表示更新成功后的系統時間戳,因為在多線程下,更新操作可能會阻塞。
    • expireAfterRead(String s, Person person, long l, long l1) : 與 expireAfterUpdate 同理。
    6. refreshAfterWrite 延遲刷新
    refreshAfterWrite(long duration, TimeUnit unit)
    

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

    只適用于 LoadingCacheAsyncLoadingCache,如果刷新操作沒有完成,讀取的數據只是舊數據。

    同理,不想寫了。

    7. removalListener 清除、更新監聽

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

    舉個例子:

    MyStatsCounter myStatsCounter = new MyStatsCounter();
    Caffeine 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 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 中:

    • EXPLICIT : 表示顯式地調用刪除操作,直接將某個數據刪除。
    • REPLACED:表示某個數據被更新。
    • EXPIRED:表示因為生命周期結束(過期時間到了),而被清除。
    • SIZE:表示因為緩存空間大小受限,總權重受限,而被清除。
    • COLLECTED : 這個不明白。
    8. 緩存的數據使用弱引用,軟引用

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

    • weakKeys():將緩存的 key 使用弱引用包裝起來,只要 GC 的時候,就能被回收。
    • weakValues():將緩存的 value 使用弱引用包裝起來,只要 GC 的時候,就能被回收。
    • softValues():將緩存的 value使用軟引用包裝起來,只要 GC 的時候,有必要,就能被回收。

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

    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 caffeine = Caffeine.newBuilder()
            .maximumWeight(30)
            .writer(new MyCacheWriter())
            .weigher((String key, Person value)-> value.getAge());
    Cache 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 可以有的操作

    • V getIfPresent(K key) :如果緩存中 key 存在,則獲取 value,否則返回 null。
    • void put( K key, V value):存入一對數據 。
    • Map getAllPresent(Iterable var1) :參數是一個迭代器,表示可以批量查詢緩存。
    • void putAll( Map var1): 批量存入緩存。
    • void invalidate(K var1):刪除某個 key 對應的數據。
    • void invalidateAll(Iterable var1):批量刪除數據。
    • void invalidateAll():清空緩存。
    • long estimatedSize():返回緩存中數據的個數。
    • CacheStats stats():返回緩存當前的狀態指標集。
    • ConcurrentMap asMap():將緩存中所有的數據構成一個 map。
    • void cleanUp():會對緩存進行整體的清理,比如有一些數據過期了,但是并不會立馬被清除,所以執行一次 cleanUp 方法,會對緩存進行一次檢查,清除那些應該清除的數據。
    • V get( K var1, Function var2):第一個參數是想要獲取的 key,第二個參數是函數,例子如下:
    Caffeine caffeine = Caffeine.newBuilder()
            .maximumWeight(30)
            .weigher((String key, Person value)-> value.getAge());
    Cache 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。

    string緩存
    本作品采用《CC 協議》,轉載必須注明作者和本文鏈接
    代表的a的二進制位的修改。對應的ASCII碼是97,轉換為二進制數據是01100001. 因為bit非常節省空間,可以用來做大數據量的統計。BITOPNOTdestkeykey ,對給定 key 求邏輯非,并將結果保存到 destkey 。獲取今天點擊最多的15條:zrevrange hotNews:20190926 0 15 withscores
    介紹Runtime 是一系列采用 C++ 語言編寫的功能方法,它實現了大量 JavaScript 運行期間需要的 native 功能。本文分析 Runtime_StringToArray 方法的源碼和重要數據結構,講解 Runtime_StringToArray 方法的觸發條件。
    先看一個小例子,明白如何創建一個 Caffeine 緩存實例。因為如果提前能預估緩存的使用大小,那么可以設置緩存的初始容量,以免緩存不斷地進行擴容,致使效率不高。
    緩存投毒漏洞理解
    2022-03-21 09:43:17
    在先知上看到了一篇關于緩存投毒的分享,屬實是小刀拉屁股,開了眼。 但是那篇翻譯的文章屬實還是有點抽象,根據那篇文章,說說我理解的緩存投毒漏洞。 首先利用條件需要目標,存在CDN網絡,或者前置的緩存服務器,可見這也是個富貴病。 1.緩存服務器原理 緩存服務器可以簡單理解為一個map[string]string結構,根據不同key來返回不同的緩存內容。 但是這個key的來源各個cdn或者緩存
    Redis系列漏洞總結
    2023-06-19 10:29:18
    前言Redis的未授權漏洞一直都是一個很火的漏洞,最近看許多前輩的文章自己復現后,根據自己的實踐再次總結一下,為日后復習方便回顧。Redis簡介redis是一個key-value存儲系統。和Memcached類似,它支持存儲的value類型相對更多,包括string、list、set、zset和hash。這些數據類型都支持push/pop、add/remove及取交集并集和差集及更豐富的操作,而且這些操作都是原子性的。在此基礎上,redis支持各種不同方式的排序。
    通過反向代理,可以在不影響用戶操作的情況下無感知的獲取用戶的信息,或者誘導用戶操作。也可以通過使用代理方式達到隱藏服務端的目的。,首先創建goblin目錄,切換到目錄下,執行docker?goblin 不對因使用此工具而產生的任何風險負責。處理響應數據最大值默認?20M,超過這個值,插件中需要讀取?標簽),一種是跟著全局 js 文件走各有好處。可以為文件或者 url使用 replace 注入具體文檔可以參考 goblin Replace 模塊-?替換的 header 頭內容。
    Go:Channel使用模式
    2022-07-20 11:05:42
    有幾種重要的channel模式需要理解,因為channel實現了Goroutine之間的通信。等待結果模式這是channel的基本使用模式,創建一個goroutine來執行任務,然后將執行結果通過channel通知到對應的其他Goroutine。
    在開始介紹如何優化sql前,先附上mysql內部邏輯圖讓大家有所了解連接器:?優先在緩存中進行查詢,如果查到了則直接返回,如果緩存中查詢不到,在去數據庫中查詢。
    獲取到類之后,我們就可以通過反射來間接調用里面的方法,獲取里面的變量等。
    VSole
    網絡安全專家
      亚洲 欧美 自拍 唯美 另类