1. 程式人生 > 其它 >Guava 記憶體快取的使用

Guava 記憶體快取的使用

一、概述

guava⽬前有三種重新整理本地快取的機制:

  • expireAfterAccess:當快取項在指定的時間段內沒有被讀或寫就會被回收。
  • expireAfterWrite:當快取項在指定的時間段內沒有更新就會被回收。-- 常用
  • refreshAfterWrite:當快取項上一次更新操作之後的多久會被重新整理。 -- 常用

二、原理

expireAfterWrite 為了避免快取雪崩,guava 會限制只有一個載入操作時進行加鎖,其他請求必須阻塞等待這個載入操作完成。而且,在載入完成之後,其他請求的執行緒會逐一獲得鎖,去判斷是否已被載入完成,每個執行緒必須輪流地走一個“獲得鎖,獲得值,釋放鎖”的過程,這樣效能會有一些損耗。

refreshAfterWrite 當快取項上一次更新操作之後的多久會被重新整理。在 refresh 的過程中,guava 會限制只有一個載入操作時進行加鎖,而其他查詢先返回舊值,這樣能有效減少等待和鎖爭用,所以 refreshAfterWrite 會比 expireAfterWrite 效能好。

Load 加鎖是從從 expire 到 load 到新值為⽌,⽽ refresh->reload 的過程,⼀旦 get 發現需要 refresh,會先判斷是否有 loading,再去獲得鎖,然後釋放鎖之後再去reload,阻塞的範圍只是 insertLoadingValueReference 的⼀個⼩物件的 new 和 set 操作,⼏乎可以忽略不計。

三、實踐

首先了解⼀個機制,guava 不會⾃動清除清除資料,只有在訪問時候再去判斷 expire。

設定合理的 expireAfterWrite 和 refreshAfterWrite 時間來保證快取不會被瞬間擊垮。根據合理的場景設定合理的引數。比如 expireAfterWrite=60 和 refreshAfterWrite=30,保證每隔 30 秒重新整理⼀次資料。但是超過 60 秒⼀直沒有訪問量,突然間訪問,還是會觸發 load 操作。

expireAfterWrite 是為了保證在很久沒有訪問量,⾸次訪問不再訪問舊值。⽐如已經間隔⼀天,首次訪問希望獲取最新值,而不是先獲取舊值再去重新整理新值,這種就可⽤通過設定expireAfterWrite來保證。

其實如果極端情況下,即新舊值基本不會變更的,直接不採⽤ expireAfterWrite,⽽直接採⽤ refreshAfterWrite 來執⾏ load 也是可以的,效能會更優。

public class CacheBuilderTest {

    private static AtomicInteger atomicInteger = new AtomicInteger();

    public static void main(String[] args) throws InterruptedException {
        LoadingCache<String, String> loadingCache = CacheBuilder.newBuilder()
                .expireAfterWrite(60, TimeUnit.SECONDS)
                .refreshAfterWrite(30, TimeUnit.SECONDS)
                .maximumSize(10)
                .removalListener(new RemovalListener() {
                    @Override
                    public void onRemoval(RemovalNotification notification) {
                        System.out.println("key:" + notification.getKey() + ", remove!");
                    }
                })
                .recordStats()
                .build(new CacheLoader<String, String>() {

                    @Override
                    public String load(String key) throws Exception {
                        System.out.println("key:" + key + ", load");
                        return "Hello Guava Cache " + key + "load";
                    }
                    
                    @Override
                    public ListenableFuture<String> reload(String key, String oldValue) throws Exception {
                        ListenableFutureTask<String> listenableFutureTask = ListenableFutureTask.create(() -> {
                            System.out.println("key:" + key + ", reload");
                            return "Hello Guava Cache " + key + "reload" + atomicInteger.incrementAndGet();
                        });
                        CompletableFuture.runAsync(listenableFutureTask);
                        return listenableFutureTask;
                    }
                });

        while (true) {
            String lucky = loadingCache.getUnchecked("lucky");
            System.out.println(lucky);
            CacheStats stats = loadingCache.stats();
            System.out.println("快取命中率:" + stats.hitCount());
            System.out.println("載入新值的平均時間,單位為毫秒:" + stats.averageLoadPenalty() / 10000);
            System.out.println("快取項被回收的總數,不包括顯式清除:" + stats.evictionCount());
            Thread.sleep(65000);
            System.out.println("---------------------------------------------------------");
        }
    }
}