1. 程式人生 > 其它 >新一代快取Caffeine,速度確實比Guava的Cache快

新一代快取Caffeine,速度確實比Guava的Cache快

我想把記憶快取起來,等再次見到你,就能夠很快認出你。

能夠說出這麼有哲理的話,得益於我對快取的理解,以及對它的看重。沒有了快取,我的人生就沒有了意義。

快取是非常重要的,工作中大部分工作可以說是和快取打交道。由於使用廣泛,所以針對快取系統的任何優化,如果能夠提高一丁點兒效能,就會讓人無比興奮。

很長一段時間,我都在用GuavaLoadingCache。它和ConcurrentHashMap是非常像的,但在其上封裝了一些好用的逐出策略和併發優化,就顯得好用的多。

今天主要說的是Caffeine,中文名就是咖啡因,一種容易讓人精神亢奮的物質。它可以說是Guava的重寫,但是效率卻非常的高,青出於藍而勝於藍。

下圖是Caffeine的一張效能測試圖。可以看到它的效能,甩了GuavaCache老遠。這是為什麼呢? localfile://media/15938442367524/15938448827543.jpg

首先要從它的作者開始說起。作者的github是(github.com/ben-manes),曾經寫了ConcurrentLinkedHashMap這個類,而這個類又是GuavaCache的基礎。Ben Manes一拍腦袋,決定更上層樓。

為什麼說Caffeine好?

後浪Caffeine一來,GuavaCache就已經OUT了。

Caffeine支援非同步載入方式,直接返回CompletableFutures

,相對於GuavaCache的同步方式,它不用阻塞等待資料的載入。另外,它的程式設計模型是友好的,省去了很多重複的工作。

GuavaCache是基於LRU的,而Caffeine是基於LRU和LFU的,結合了兩者的優點。對這兩個演算法不太清楚的同學,可以參考xjjdog之前的文章:《3種堆內快取演算法,贈原始碼和設計思路》

兩者合體之後,變成了新的W-TinyLFU演算法,它的命中率非常高,記憶體佔用更加的小,這是主要原因所在。

Caffeine另外一個比較快的原因,就是很多操作都使用了非同步,把這些事件提交到佇列裡。佇列使用的RingBuffer,看到這個名詞,我不自覺的想到了lmaxDisruptor

,它已經成了無鎖高併發的代名詞。

測試命中率

我們決定拿線上的資料進行驗證一下。事實上,大部分比較重要的Cache,我都已經使用Caffeine替換了,完成了騷氣的升級。

由於它們的API長得非常像,這個過程是無痛的,連麻藥都不需要打。

其中有個業務,有一個大的堆內快取,快取了使用者資料。裡面包含使用者名稱、性別、地址、積分等屬性,形成了一個JSON物件,但大小不超過1KB。通過灰度,根據不同的策略,我們測試了它的實際命中率。

策略1

  • 最大快取1w使用者
  • 資料進入快取後,5分鐘失效(需要重新讀取)

命中率:

  • Caffeine 29.22 %
  • Guava 21.95%

策略2

  • 加大快取資料量到6w使用者
  • 資料進入快取後,20分鐘失效,這個和Session有的一拼了

命中率(依然是高一籌):

  • Caffeine 56.04 %
  • Guava 50.01%

策略3

  • 直接加大快取到15w使用者
  • 資料進入快取後,30分鐘失效

此時的命中率:

  • Caffeine 71.10 %
  • Guava 62.76%

Caffeine的命中率一直是領先的。命中率高,效率自然也就高。調整到50%以上,我們的快取作用就很大了。

非同步載入

再放上官方的兩張測試圖:

(1) Read (75%) / Write (25%) localfile://media/15938442367524/15938463845793.jpg

(2) Write (100%) localfile://media/15938442367524/15938464259214.jpg

(3) Read (100%) localfile://media/15938442367524/15938464366776.jpg

我們一直在提Caffeine的非同步載入。那程式碼到底長什麼樣子呢?非同步載入快取使用了響應式程式設計模型,二手遊戲買賣平臺地圖返回的是CompletableFuture物件。說實話,程式碼長得和Guava很像。

public static void main(String[] args) {
        AsyncLoadingCache<String, String> asyncLoadingCache = Caffeine.newBuilder()
                .maximumSize(1000)
                .buildAsync(key -> slowMethod(key));

        CompletableFuture<String> g = loadingCache.get("test");
        String value = g.get();
    }

    static String slowMethod(String key) throws Exception {
        Thread.sleep(1000);
        return key + ".result";
    }
複製程式碼

我記得前段時間翻Spring的原始碼時,也看到過它。 localfile://media/15938442367524/15938479597151.jpg

在SpringBoot裡,通過提供一個CacheManager的Bean,即可與Springboot-cache進行整合,可以說是很方便了。

關鍵程式碼。

//bean生成
@Bean("caffeineCacheManager")
public CacheManager cacheManager() {
    CaffeineCacheManager cacheManager = new CaffeineCacheManager();
    cacheManager.setCaffeine(Caffeine.newBuilder() .maximumSize(1000));
    return cacheManager;
}

//使用注入
@CacheConfig(cacheNames = "caffeineCacheManager")

//資訊快取
@Cacheable(key = "#id")
複製程式碼

技術框架這麼多,何時是盡頭。