天天看點

java 緩存_Java本地緩存架構系列-Caffeine-1. 簡介與使用

Caffeine 是一個基于Java 8的高性能本地緩存架構,其結構和 Guava Cache 基本一樣,api也一樣,基本上很容易就能替換。 Caffeine 實際上就是在 Guava Cache 的基礎上,利用了一些 Java 8 的新特性,提高了某些場景下的性能效率。

這一章節我們會從 Caffeine 的使用引入,并提出一些問題,之後分析其源代碼解決這些問題來讓我們更好的去了解 Caffeine 的原理,更好的使用與優化,并且會對于我們之後的編碼有所裨益。

我們來看一下 Caffeine 的基本使用,首先是建立:

限制緩存大小

Caffeine 有兩種方式限制緩存大小。

兩種配置互斥,不能同時配置 1. 建立一個限制容量 Cache
Cache<String, Object> cache = Caffeine
                            .newBuilder()
                            //設定緩存的 Entries 個數最多不超過1000個
                            .maximumSize(1000)
                            .build();
           

需要注意的是,實際實作上為了性能考慮,這個限制并不會很死闆:

  • 在緩存元素個數快要達到最大限制的時候,過期政策就開始執行了,是以在達到最大容量前也許某些不太可能再次通路的 Entry 就被過期掉了。
  • 有時候因為過期 Entry 任務還沒執行完,更多的 Entry 被放入緩存,導緻緩存的 Entry 個數短暫超過了這個限制

配置了 maximumSize 就不能配置下面的 maximumWeight 和 weigher

2. 建立一個自定義權重限制容量的 Cache
Cache<String, List<Object>> stringListCache = Caffeine.newBuilder()
    //最大weight值,當所有entry的weight和快達到這個限制的時候會發生緩存過期,剔除一些緩存
    .maximumWeight(1000)
    //每個 Entry 的 weight 值
    .weigher(new Weigher<String, List<Object>>() {
        @Override
        public @NonNegative int weigh(@NonNull String key, @NonNull List<Object> value) {
            return value.size();
        }
    })
    .build();
           

當你的緩存的 Key 或者 Value 比較大的時候,想靈活地控制緩存大小,可以使用這種方式。上面我們的 key 是一個 list,以 list 的大小作為 Entry 的大小。 當把 Weigher 實作為隻傳回1,maximumWeight 其實和 maximumSize 是等效的。 同樣的,為了性能考慮,這個限制也不會很死闆。

在這裡,我們提出第一個問題:

Entry是怎麼儲存,怎麼過期的呢? 3. 指定初始大小
Cache<String, Object> cache = Caffeine.newBuilder()
    //指定初始大小
    .initialCapacity(1000)
    .build();
           

HashMap

類似,通過指定一個初始大小,減少擴容帶來的性能損耗。這個值也不宜過大,浪費記憶體。

在這裡,我們提出第二個問題:

這個初始大小,影響那些存儲參數呢? 4. 指定Key, Value為非強引用類型
Cache<String, Object> cache = Caffeine.newBuilder()
    // 設定 key 為 WeakReference
    .weakKeys()
    .build();
cache = Caffeine.newBuilder()
    // 設定 key 為 WeakReference
    .weakKeys()
    // 設定 value 為 WeakReference
    .weakValues()
    .build();
cache = Caffeine.newBuilder()
    // 設定 key 為 WeakReference
    .weakKeys()
    // 設定 value 為 SofReference
    .softValues()
    .build();
           

對于 Java 中的 StrongReference,WeakReference,SoftReference,可以參考我的另外一篇文章:JDK核心JAVA源碼解析(3) - 引用相關 在這裡簡單歸納下:

  1. StrongReference :強引用就是指在程式代碼之中普遍存在的,一般的new一個對象并指派給一個對象變量,就是一個強引用;隻要某個對象有強引用與之關聯,JVM必定不會回收這個對象,即使在記憶體不足的情況下,JVM甯願抛出OutOfMemory錯誤也不會回收這種對象。
  2. SoftReference :軟引用是用來描述一些有用但并不是必需的對象,在Java中用java.lang.ref.SoftReference類來表示。。對于軟引用關聯着的對象,在系統将要發生記憶體溢出異常之前,将會把這些對象列進回收範圍之中進行第二次回收。如果這次回收還沒有足夠的記憶體,才會抛出記憶體溢出異常。
  3. WeakReference :用來描述非必須的對象,但是它的強度比軟引用更弱一些,被弱引用關聯的對象隻能生存到下一次垃圾收集發生之前。當垃圾收集器工作時,無論目前記憶體是否足夠,都會回收掉隻被弱引用關聯的對象。在java中,用java.lang.ref.WeakReference類來表示

Caffeine 中的 Key,可以是 WeakReference,但是目前不能指定為 SoftReference,是以我們在這裡提出第三個問題,

為什麼 Key 不能指定為 SoftReference,SoftReference 為何被差別對待

設定 Key 和 Value 的 Reference 類型,也是一種限制大小的方式,但是限制比較多:

  • 使用 weakKeys 就不能使用 Writer (這裡提出第四個問題, 為什麼 weakKeys 和 Writer 不能同時使用
  • 使用 weakValues 或者 softValues 就不能使用異步緩存 buildAsync(這裡提出第五個問題, 為什麼使用 weakValues 或者 softValues 就不能使用異步緩存

一般通過 maximumSize 還有 maximumWeight 就能滿足我們的需求。

設定過期時間相關

1. 自定義過期
Cache<String, Order> cache = Caffeine.newBuilder()
    .expireAfter(new Expiry<String, Order>() {
        @Override
        //設定 Entry 建立後的過期時間
        //這裡設定為 60s 後過期
        public long expireAfterCreate(@NonNull String key, @NonNull Order value, long currentTime) {
            return 1000 * 1000 * 1000 * 60;
        }

        @Override
        //設定 Entry 更新後的過期時間
        //這裡傳回 currentDuration 表示永遠不過期
        public long expireAfterUpdate(@NonNull String key, @NonNull Order value, long currentTime, @NonNegative long currentDuration) {
            return currentDuration;
        }

        @Override
        //設定 Entry 讀取後的過期時間
        //這裡設定為 Order 的 createTime 的 60s 後過期
        public long expireAfterRead(@NonNull String key, @NonNull Order value, long currentTime, @NonNegative long currentDuration) {
            return 1000 * 1000 * 1000 * 60 - (System.currentTimeMillis() - value.createTime()) * 1000;
        }
    })
    .build();
           

通過實作 Expiry 接口,設定過期政策。這個接口主要包括三個值:

  • Entry 建立後的過期時間:參數為 Entry 的 Key 還有 Value,以及 Entry 建立時間。需要傳回的是這個 Entry 的生育過期時間,機關是 nanoSeconds
  • Entry 更新後的過期時間:參數為 Entry 的 Key 還有 Value,以及目前時間(并不是系統目前時間,而是 Ticker 裡面的目前時間,如果需要擷取系統目前時間需要自己手動擷取)和目前剩餘的過期時間。需要傳回的是這個 Entry 的剩餘過期時間,機關是 nanoSeconds。如果永遠不過期,可以傳回 currentDuration 表示剩餘時間永遠不變,永遠不過期。
  • Entry 讀取後的過期時間:參數為 Entry 的 Key 還有 Value,以及目前時間(并不是系統目前時間,而是 Ticker 裡面的目前時間,如果需要擷取系統目前時間需要自己手動擷取)和目前剩餘的過期時間。需要傳回的是這個 Entry 的剩餘時間,機關是 nanoSeconds。如果永遠不過期,可以傳回 currentDuration 表示剩餘時間永遠不變,永遠不過期。

這個配置與接下來的 expireAfterWrite 和 expireAfterAccess 互斥。不能同時配置

** 2. 設定寫入以及更新後過期**

Cache<String, Object> cache = Caffeine.newBuilder()
    //寫入或者更新1分鐘後,緩存過期并失效
    .expireAfterWrite(1, TimeUnit.MINUTES)
    .build();
           

這個配置與上面的 expireAfter 互斥,不能同時配置

** 3. 設定操作後過期**

Cache<String, Object> cache = Caffeine.newBuilder()
    //寫入或者更新或者讀取1分鐘後,緩存過期并失效
    .expireAfterAccess(1, TimeUnit.MINUTES)
    .build();
           

這個配置與上面的 expireAfter 互斥,不能同時配置

LoadingCache 相關

** 1. 生成LoadingCache **

Cache<String, Object> cache = Caffeine.newBuilder()
    //使用 CacheLoader 初始化
    .build(key -> {
        return loadFromDB(key);
    });
           

當 Key 不存在或者已過期時,會調用 CacheLoader 重新加載這個 Key。那麼,這裡要提出下面這些問題:

  1. Key 是否可以為 Null,為什麼
  2. 調用 CacheLoader 的時候,如果有異常會怎樣
2. 設定定時重新加載時間
Cache<String, Object> cache = Caffeine.newBuilder()
    //設定在寫入或者更新之後1分鐘後,調用 CacheLoader 重新加載
    .refreshAfterWrite(1, TimeUnit.MINUTES)
    //使用 CacheLoader 初始化
    .build(key -> {
        return loadFromDB(key);
    });
           

注意設定了這個配置,就隻能通過

build(CacheLoader)

來生成 LoadingCache,不能生成普通的 Cache 了

額外配置

1. 統計記錄相關
Cache<String, Object> cache = Caffeine.newBuilder()
    //打開資料采集
    .recordStats().build();
Cache<String, Object> cache = Caffeine.newBuilder()
    //自定義資料采集器
    .recordStats(() -> new StatsCounter() {
        @Override
        public void recordHits(@NonNegative int count) {
            
        }
    
        @Override
        public void recordMisses(@NonNegative int count) {
    
        }
    
        @Override
        public void recordLoadSuccess(@NonNegative long loadTime) {
    
        }
    
        @Override
        public void recordLoadFailure(@NonNegative long loadTime) {
    
        }
    
        @Override
        public void recordEviction() {
    
        }
    
        @Override
        public void recordEviction(@NonNegative int weight) {
            
        }
    
        @Override
        public void recordEviction(@NonNegative int weight, RemovalCause cause) {
    
        }
    
        @Override
        public @NonNull CacheStats snapshot() {
            return null;
        }
}).build();
           

這裡我們提出兩個問題:

  1. 預設的資料采集是否會影響性能
  2. 資料采集都會采集哪些資料
2. 某個 Entry 過期被移除後的回調
Cache<String, Object> cache = Caffeine
    .newBuilder()
    .removalListener((key, value, cause) -> {
        log.info("{}, {}, {}", key, value, cause);
    })
    .build();
           

回調裡面有三個參數,包括 Entry 的 Key, Entry 的 Value 以及移除原因 cause。這個原因是一個枚舉類型:

public enum RemovalCause {
    EXPLICIT {
        @Override public boolean wasEvicted() {
          return false;
        }
    },
    REPLACED {
        @Override public boolean wasEvicted() {
          return false;
        }
    },
    COLLECTED {
        @Override
        public boolean wasEvicted() {
            return true;
        }
    },
    EXPIRED {
        @Override
        public boolean wasEvicted() {
            return true;
        }
    },
    SIZE {
        @Override
        public boolean wasEvicted() {
            return true;
        }
    };
}
           

這裡再提出一個問題:

失效原因究竟對應哪些 API 的操作導緻的失效? 3. 緩存主動更新其他存儲或者資源

我們還可以通過設定 Writer,将對于緩存的更新,作用于其他存儲,例如資料庫:

Cache<String, Object> cache = Caffeine.newBuilder()
    .writer(new CacheWriter<String, Object>() {
        @Override
        public void write(@NonNull String key, @NonNull Object value) {
            //緩存更新時(包括建立和修改,不包括load),回調這裡
            //資料庫更新
            db.upsert(key, value);
        }

        @Override
        public void delete(@NonNull String key, @Nullable Object value, @NonNull RemovalCause cause) {
            //緩存失效時(包括任何原因的失效),回調這裡
            //資料庫更新
            db.markAsDeleted(key, value);
        }
    })
    .build();
           

那麼就引出了如下幾個問題:

  • 如果回調發生異常,會怎麼處理?
  • 具體哪些 API 會引發 write,哪些會引發 delete

異步緩存

1. 生成異步緩存
AsyncCache<String, Object> cache = Caffeine.newBuilder()
    //生成異步緩存
    .buildAsync();
           

這種緩存,擷取的 Value 都是一個

CompletableFuture

**2. 生成異步 LoadingCache **

AsyncCache<String, Object> cache = Caffeine.newBuilder()
    //生成異步緩存
    .buildAsync(key -> {
        return loadFromDB(key);
    });
           
3. 設定異步任務線程池
AsyncCache<String, Object> cache = Caffeine.newBuilder()
    .executor(new ForkJoinPool(10))
    //生成異步緩存
    .buildAsync();
           

這裡我們提出如下問題:

  1. 異步緩存裡面,哪些操作是異步的?
  2. 這些異步任務,執行的線程池預設是哪個?
  3. 異步任務有異常,如何處理?

到這裡我們基本把建立說完了,接下來看一下使用這些緩存:

Cache<String, String> syncCache = Caffeine.newBuilder().build();
//加入緩存
syncCache.put(key, value);
//批量加入
syncCache.putAll(keyValueMap);
//讀取緩存,如果不存在,則執行後面的mappingFunction讀取并放入緩存
syncCache.get(key, k -> {
    return readFromOther(k);
});
//批量讀取
syncCache.getAll(keys, ks -> {
   return readFromOther(k);
});
//擷取緩存配置資訊,以及其他次元的資訊
Policy<String, String> policy = syncCache.policy();
//擷取統計資訊,前提是必須打開統計
CacheStats stats = syncCache.stats();
//擷取某個key,如果不存在則傳回null
syncCache.getIfPresent(key);
//将map轉換為map,對map的修改會影響緩存
ConcurrentMap<@NonNull String, @NonNull String> map = syncCache.asMap();
//讓某個key生效
syncCache.invalidate(key);
//讓所有key失效
syncCache.invalidateAll();
//批量失效
syncCache.invalidateAll(keys);
//估計大小
@NonNegative long estimatedSize = syncCache.estimatedSize();
//等待過期清理任務完成,讓緩存處于一個穩定狀态
syncCache.cleanUp();
           

這裡隻提了同步緩存,異步緩存的 API 類似,隻是取值變成了

CompletableFuture

包裝的

接下來的章節,我們會深入研究 Caffeine 的源代碼和實作原理及思想

繼續閱讀