Caffeine Cache 進程緩存利器


1、前言

緩存的使用可以大幅度提升用戶的體驗度,所以緩存就是必不可少的一個神器,在多線程高並發場景中往往是離不開cache的,需要根據不同的應用場景來選擇需要的cache,比如分布式緩存redis、memcached,還有本地(進程內)緩存ehcache、GuavaCache、Caffeine。

Guava Cache是基於LRU算法實現,支持多種緩存過期策略。而Caffeine就是一個比Guava Cache性能更高的緩存框架。

2、比較

Caffeine是使用Java8對Guava緩存的重寫版本,在Spring Boot 2.0中將取代,基於LRU算法實現,支持多種緩存過期策略。

3、如何使用

public static void main(String[] args) {
      LoadingCache<String, String> build = CacheBuilder.newBuilder().initialCapacity(1).maximumSize(100).expireAfterWrite(1, TimeUnit.DAYS)
          .build(new CacheLoader<String, String>() {
             //默認的數據加載實現,當調用get取值的時候,如果key沒有對應的值,就調用這個方法進行加載
             @Override
             public String load(String key)  {
                  return "";
             }
         });
}

參數方法

  • initialCapacity(1) 初始緩存長度為1
  • maximumSize(100) 最大長度為100
  • expireAfterWrite(1, TimeUnit.DAYS) 設置緩存策略在1天未寫入過期緩存(后面講緩存策略)

4、過期策略

在Caffeine中分為兩種緩存,一個是有界緩存,一個是無界緩存,無界緩存不需要過期並且沒有界限。在有界緩存中提供了三個過期API:

  • expireAfterWrite: 代表着寫了之后多久過期;
  • expireAfterAccess: 代表着最后一次訪問了之后多久過期;
  • expireAfter: 在expireAfter中需要自己實現Expiry接口,這個接口支持create,update,access了之后多久過期。注意!這個API和前面兩個API視乎池的。這里和前面兩個API不同的是,需要你告訴緩存框架,他應該在具體的某個時間過期,也就是通過前面的重寫create,update,access的方法,獲取具體的過期時間。

5、更新策略

何為更新策略?設定多長時間之后會自動刷新緩存。

Caffeine提供了refreshAfterWrite()方法來讓我們進行寫后多久更新策略:

LoadingCache<String, String> build = CacheBuilder.newBuilder().refreshAfterWrite(1, TimeUnit.DAYS)
   .build(new CacheLoader<String, String>() {
          @Override
          public String load(String key)  {
             return "";
          }
    });
}

上面的代碼我們需要建立一個CacheLodaer來進行刷新,這里是同步進行的,可以通過buildAsync方法進行異步構建。在實際業務中這里可以把我們代碼中的mapper傳入進去,進行數據源的刷新。

但是實際使用中,你設置了一天刷新,但是一天后你發現緩存並沒有刷新。這是因為必有在1天后這個緩存再次訪問才能刷新,如果沒人訪問,那么永遠也不會刷新。

我們來看看自動刷新他是怎么做的呢?自動刷新只存在讀操作之后,也就是我們afterRead()這個方法,其中有個方法叫refreshIfNeeded,他會根據你是同步還是異步然后進行刷新處理。

6、填充策略(Population)

Caffeine 為我們提供了三種填充策略:手動、同步和異步

6.1、手動加載(Manual)

// 初始化緩存
Cache<String, Object> manualCache = Caffeine.newBuilder()
           .expireAfterWrite(10, TimeUnit.MINUTES)
           .maximumSize(10_000)
           .build();

String key = "name1";
// 根據key查詢一個緩存,如果沒有返回NULL
graph = manualCache.getIfPresent(key);
// 如果緩存中不存在該鍵,createExpensiveGraph函數將用於提供回退值,該值在計算后插入緩存中
graph = manualCache.get(key, k -> createExpensiveGraph(k));
// 使用 put 方法手動填充緩存,如果以前有值就覆蓋以前的值
manualCache.put(key, graph);
// 刪除一個緩存
manualCache.invalidate(key);

ConcurrentMap<String, Object> map = manualCache.asMap();
cache.invalidate(key);

Cache接口允許顯式的去控制緩存的檢索,更新和刪除。

我們可以通過cache.getIfPresent(key) 方法來獲取一個key的值,通過cache.put(key, value)方法顯示的將數控放入緩存,但是這樣子會覆蓋緩原來key的數據。更加建議使用cache.get(key,k - > value) 的方式,get 方法將一個參數為 key 的 Function (createExpensiveGraph) 作為參數傳入。如果緩存中不存在該鍵,則調用這個 Function 函數,並將返回值作為該緩存的值插入緩存中。get 方法是以阻塞方式執行調用,即使多個線程同時請求該值也只會調用一次Function方法。這樣可以避免與其他線程的寫入競爭,這也是為什么使用 get 優於 getIfPresent 的原因。

注意:如果調用該方法返回NULL(如上面的 createExpensiveGraph 方法),則cache.get返回null,如果調用該方法拋出異常,則get方法也會拋出異常。可以使用Cache.asMap() 方法獲取ConcurrentMap進而對緩存進行一些更改。

6.2 同步加載(Loading)

// 初始化緩存
LoadingCache<String, Object> loadingCache = Caffeine.newBuilder()
     .maximumSize(10_000)
     .expireAfterWrite(10, TimeUnit.MINUTES)
     .build(key -> createExpensiveGraph(key));

String key = "name1";
// 采用同步方式去獲取一個緩存和上面的手動方式是一個原理。在build Cache的時候會提供一個createExpensiveGraph函數。
// 查詢並在缺失的情況下使用同步的方式來構建一個緩存
Object graph = loadingCache.get(key);

// 獲取組key的值返回一個Map
List<String> keys = new ArrayList<>();
keys.add(key);
Map<String, Object> graphs = loadingCache.getAll(keys);

LoadingCache是使用CacheLoader來構建的緩存的值。批量查找可以使用getAll方法,默認情況下,getAll將會對緩存中沒有值的key分別調用CacheLoader.load方法來構建緩存的值。我們可以重寫CacheLoader.loadAll方法來提高getAll的效率。

注意:您可以編寫一個CacheLoader.loadAll來實現為特別請求的key加載值。例如,如果計算某個組中的任何鍵的值將為該組中的所有鍵提供值,則loadAll可能會同時加載該組的其余部分。

6.3 異步加載(Asynchronously Loading)

AsyncLoadingCache<String, Object> asyncLoadingCache = Caffeine.newBuilder()
        .maximumSize(10_000)
        .expireAfterWrite(10, TimeUnit.MINUTES)
        // Either: Build with a synchronous computation that is wrapped as asynchronous
        .buildAsync(key -> createExpensiveGraph(key));
        // Or: Build with a asynchronous computation that returns a future
        // .buildAsync((key, executor) -> createExpensiveGraphAsync(key, executor));

String key = "name1";

// 查詢並在缺失的情況下使用異步的方式來構建緩存
CompletableFuture<Object> graph = asyncLoadingCache.get(key);
// 查詢一組緩存並在缺失的情況下使用異步的方式來構建緩存
List<String> keys = new ArrayList<>();
keys.add(key);
CompletableFuture<Map<String, Object>> graphs = asyncLoadingCache.getAll(keys);
// 異步轉同步
loadingCache = asyncLoadingCache.synchronous();

AsyncLoadingCache是繼承自LoadingCache類的,異步加載使用Executor去調用方法並返回一個CompletableFuture。異步加載緩存使用了響應式編程模型。

如果要以同步方式調用時,應提供CacheLoader。要以異步表示時,應該提供一個AsyncCacheLoader,並返回一個CompletableFuture。

synchronous()這個方法返回了一個LoadingCacheView視圖,LoadingCacheView也繼承自LoadingCache。調用該方法后就相當於你將一個異步加載的緩存AsyncLoadingCache轉換成了一個同步加載的緩存LoadingCache。

默認使用ForkJoinPool.commonPool()來執行異步線程,但是我們可以通過Caffeine.executor(Executor) 方法來替換線程池。

7、驅逐策略(eviction)

緩存的驅逐策略是為了預測哪些數據在短期內最可能被再次用到,從而提升緩存的命中率。LRU策略或許是最流行的驅逐策略。但LRU通過歷史數據來預測未來是局限的,它會認為最后到來的數據是最可能被再次訪問的,從而給予它最高的優先級。

Caffeine提供三類驅逐策略:基於大小(size—based),基於時間(time-based)和基於引用(reference-based)。

7.1 基於大小(size-based)

基於大小驅逐,有兩種方式:一種是基於緩存大小,一種是基於權重。

// 根據緩存的計數進行驅逐
LoadingCache<Key, Graph> graphs = Caffeine.newBuilder()
    .maximumSize(10_000)
    .build(key -> createExpensiveGraph(key));

// 根據緩存的權重來進行驅逐(權重只是用於確定緩存大小,不會用於決定該緩存是否被驅逐)
LoadingCache<Key, Graph> graphs = Caffeine.newBuilder()
    .maximumWeight(10_000)
    .weigher((Key key, Graph graph) -> graph.vertices().size())
    .build(key -> createExpensiveGraph(key));

我們可以使用Caffeine.maximumSize(long)方法來指定緩存的最大容量。當緩存超出這個容量的時候,會使用Window TinyLfu策略來刪除緩存。我們也可以使用權重的策略來進行驅逐,可以使用Caffeine.weigher(Weigher) 函數來指定權重,使用Caffeine.maximumWeight(long) 函數來指定緩存最大權重值。

讓我們看看如何計算緩存中的對象。當緩存初始化時,其大小等於零:

LoadingCache<String, DataObject> cache = Caffeine.newBuilder()
                      .maximumSize(1)
                      .build(k -> DataObject.get("Data for " + k));    
assertEquals(0, cache.estimatedSize()); 

當我們添加一個值時,大小明顯增加:

cache.get("A");    
assertEquals(1, cache.estimatedSize()); 

我們可以將第二個值添加到緩存中,這導致第一個值被刪除:

cache.get("B"); 
assertEquals(1, cache.estimatedSize()); 

注意:maximumWeight與maximumSize不可以同時使用。

7.2 基於時間(Time-based)

// 基於固定的到期策略進行退出
LoadingCache<Key, Graph> graphs = Caffeine.newBuilder()
      .expireAfterAccess(5, TimeUnit.MINUTES)
      .build(key -> createExpensiveGraph(key));
LoadingCache<Key, Graph> graphs = Caffeine.newBuilder()
      .expireAfterWrite(10, TimeUnit.MINUTES)
      .build(key -> createExpensiveGraph(key));

// 要初始化自定義策略,我們需要實現 Expiry 接口
LoadingCache<Key, Graph> graphs = Caffeine.newBuilder()
      .expireAfter(new Expiry<Key, Graph>() {
          @Override
          public long expireAfterCreate(Key key, Graph graph, long currentTime) {
            // Use wall clock time, rather than nanotime, if from an external resource
            long seconds = graph.creationDate().plusHours(5)
                   .minus(System.currentTimeMillis(), MILLIS)
                   .toEpochSecond();
            return TimeUnit.SECONDS.toNanos(seconds);
         }

          @Override
          public long expireAfterUpdate(Key key, Graph graph, 
            long currentTime, long currentDuration) {
            return currentDuration;
          }

          @Override
          public long expireAfterRead(Key key, Graph graph,
             long currentTime, long currentDuration) {
             return currentDuration;
          }
      })
      .build(key -> createExpensiveGraph(key));

7.3 基於引用(reference-based)

// 當key和value都沒有引用時驅逐緩存
LoadingCache<Key, Graph> graphs = Caffeine.newBuilder()
                                          .weakKeys()
                                          .weakValues()
                                          .build(key -> createExpensiveGraph(key));

// 當垃圾收集器需要釋放內存時驅逐
LoadingCache<Key, Graph> graphs = Caffeine.newBuilder()
                                          .softValues()
                                          .build(key -> createExpensiveGraph(key));

我們可以將緩存的驅逐配置成基於垃圾回收器。當沒有任何對對象的強引用時,使用 WeakRefence 可以啟用對象的垃圾收回收。SoftReference 允許對象根據 JVM 的全局最近最少使用(Least-Recently-Used)的策略進行垃圾回收。

注意:AsyncLoadingCache不支持弱引用和軟引用。

8. 移除監聽器(Removal)

如果我們需要在緩存被移除的時候,得到通知產生回調,並做一些額外處理工作。這個時候RemovalListener就派上用場了。

8.1 概念

驅逐(eviction):由於滿足了某種驅逐策略,后台自動進行的刪除操作
無效(invalidation):表示由調用方手動刪除緩存
移除(removal):監聽驅逐或無效操作的監聽器
手動刪除緩存:在任何時候,您都可能明確地使緩存無效,而不用等待緩存被驅逐。

// individual key
cache.invalidate(key)
// bulk keys
cache.invalidateAll(keys)
// all keys
cache.invalidateAll()

8.2 Removal 監聽器

Cache<Key, Graph> graphs = Caffeine.newBuilder()
    .removalListener((Key key, Graph graph, RemovalCause cause) ->
        System.out.printf("Key %s was removed (%s)%n", key, cause))
    .build();

您可以通過Caffeine.removalListener(RemovalListener) 為緩存指定一個刪除偵聽器,以便在刪除數據時執行某些操作。 RemovalListener可以獲取到key、value和RemovalCause(刪除的原因)。

刪除偵聽器的里面的操作是使用Executor來異步執行的。默認執行程序是ForkJoinPool.commonPool(),可以通過Caffeine.executor(Executor)覆蓋。當操作必須與刪除同步執行時,請改為使用CacheWrite,CacheWrite將在下面說明。

注意:由RemovalListener拋出的任何異常都會被記錄(使用Logger)並不會拋出。

9、統計(Statistics)

Cache<Key, Graph> graphs = Caffeine.newBuilder()
      .maximumSize(10_000)
      .recordStats()
      .build();

使用Caffeine.recordStats(),您可以打開統計信息收集。Cache.stats() 方法返回提供統計信息的CacheStats,如:

  • hitRate():返回命中與請求的比率
  • hitCount(): 返回命中緩存的總數
  • evictionCount():緩存逐出的數量
  • averageLoadPenalty():加載新值所花費的平均時間

原文鏈接:https://www.jianshu.com/p/15d0a9ce37dd
來源:簡書


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM