1、介紹
在本文中,我們來看看Caffeine — 一個高性能的 Java 緩存庫。
緩存和 Map 之間的一個根本區別在于緩存可以回收存儲的 item。
回收策略為在指定時間刪除哪些對象。此策略直接影響緩存的命中率 — 緩存庫的一個重要特征。
Caffeine 因使用 Window TinyLfu 回收策略,提供了一個近乎最佳的命中率。
2、依賴
我們需要在 pom.xml 中添加 caffeine 依賴:
1
2
3
4
5
|
< dependency > < groupId >com.github.ben-manes.caffeine</ groupId > < artifactId >caffeine</ artifactId > < version >2.5.5</ version > </ dependency > |
您可以在Maven Central 上找到最新版本的 caffeine。
3、填充緩存
讓我們來了解一下 Caffeine 的三種緩存填充策略:手動、同步加載和異步加載。
首先,我們為要緩存中存儲的值類型寫一個類:
1
2
3
4
5
6
7
8
9
10
11
|
class DataObject { private final String data; private static int objectCounter = 0 ; // standard constructors/getters public static DataObject get(String data) { objectCounter++; return new DataObject(data); } } |
3.1、手動填充
在此策略中,我們手動將值放入緩存之后再檢索。
讓我們初始化緩存:
1
2
3
4
|
Cache<String, DataObject> cache = Caffeine.newBuilder() .expireAfterWrite( 1 , TimeUnit.MINUTES) .maximumSize( 100 ) .build(); |
現在,我們可以使用 getIfPresent 方法從緩存中獲取一些值。 如果緩存中不存在此值,則此方法將返回 null:
1
2
3
4
|
String key = "A" ; DataObject dataObject = cache.getIfPresent(key); assertNull(dataObject); |
我們可以使用 put 方法手動填充緩存:
1
2
3
4
|
cache.put(key, dataObject); dataObject = cache.getIfPresent(key); assertNotNull(dataObject); |
我們也可以使用 get 方法獲取值,該方法將一個參數為 key 的 Function 作為參數傳入。如果緩存中不存在該鍵,則該函數將用于提供回退值,該值在計算后插入緩存中:
1
2
3
4
5
|
dataObject = cache .get(key, k -> DataObject.get( "Data for A" )); assertNotNull(dataObject); assertEquals( "Data for A" , dataObject.getData()); |
get 方法可以原子方式執行計算。這意味著您只進行一次計算 — 即使多個線程同時請求該值。這就是為什么使用 get 優于 getIfPresent。
有時我們需要手動使一些緩存的值失效:
1
2
3
4
|
cache.invalidate(key); dataObject = cache.getIfPresent(key); assertNull(dataObject); |
3.2、同步加載
這種加載緩存的方法使用了與用于初始化值的 Function 相似的手動策略的 get 方法。讓我們看看如何使用它。
首先,我們需要初始化緩存:
1
2
3
4
|
LoadingCache<String, DataObject> cache = Caffeine.newBuilder() .maximumSize( 100 ) .expireAfterWrite( 1 , TimeUnit.MINUTES) .build(k -> DataObject.get( "Data for " + k)); |
現在我們可以使用 get 方法檢索值:
1
2
3
4
|
DataObject dataObject = cache.get(key); assertNotNull(dataObject); assertEquals( "Data for " + key, dataObject.getData()); |
我們也可以使用 getAll 方法獲取一組值:
1
2
3
4
|
Map<String, DataObject> dataObjectMap = cache.getAll(Arrays.asList( "A" , "B" , "C" )); assertEquals( 3 , dataObjectMap.size()); |
從傳遞給 build 方法的底層后端初始化函數檢索值。 這使得可以使用緩存作為訪問值的主要門面(Facade)。
3.3、異步加載
此策略的作用與之前相同,但是以異步方式執行操作,并返回一個包含值的 CompletableFuture:
1
2
3
4
|
AsyncLoadingCache<String, DataObject> cache = Caffeine.newBuilder() .maximumSize( 100 ) .expireAfterWrite( 1 , TimeUnit.MINUTES) .buildAsync(k -> DataObject.get( "Data for " + k)); |
我們可以以相同的方式使用 get 和 getAll 方法,同時考慮到他們返回的是 CompletableFuture:
1
2
3
4
5
6
7
8
9
|
String key = "A" ; cache.get(key).thenAccept(dataObject -> { assertNotNull(dataObject); assertEquals( "Data for " + key, dataObject.getData()); }); cache.getAll(Arrays.asList( "A" , "B" , "C" )) .thenAccept(dataObjectMap -> assertEquals( 3 , dataObjectMap.size())); |
CompletableFuture 有許多有用的 API,您可以在此文中獲取更多內容。
4、值回收
Caffeine 有三個值回收策略:基于大小,基于時間和參考。
4.1、基于大小回收
這種回收方式假定當超過配置的緩存大小限制時會發生回收。 獲取大小有兩種方法:緩存中計數對象,或獲取權重。
讓我們看看如何計算緩存中的對象。當緩存初始化時,其大小等于零:
1
2
3
4
5
|
LoadingCache<String, DataObject> cache = Caffeine.newBuilder() .maximumSize( 1 ) .build(k -> DataObject.get( "Data for " + k)); assertEquals( 0 , cache.estimatedSize()); |
當我們添加一個值時,大小明顯增加:
1
2
3
|
cache.get( "A" ); assertEquals( 1 , cache.estimatedSize()); |
我們可以將第二個值添加到緩存中,這導致第一個值被刪除:
1
2
3
4
|
cache.get( "B" ); cache.cleanUp(); assertEquals( 1 , cache.estimatedSize()); |
值得一提的是,在獲取緩存大小之前,我們調用了 cleanUp 方法。 這是因為緩存回收被異步執行,這種方法有助于等待回收的完成。
我們還可以傳遞一個 weigher Function 來獲取緩存的大小:
1
2
3
4
5
6
7
8
9
10
11
12
|
LoadingCache<String, DataObject> cache = Caffeine.newBuilder() .maximumWeight( 10 ) .weigher((k,v) -> 5 ) .build(k -> DataObject.get( "Data for " + k)); assertEquals( 0 , cache.estimatedSize()); cache.get( "A" ); assertEquals( 1 , cache.estimatedSize()); cache.get( "B" ); assertEquals( 2 , cache.estimatedSize()); |
當 weight 超過 10 時,值將從緩存中刪除:
1
2
3
4
|
cache.get( "C" ); cache.cleanUp(); assertEquals( 2 , cache.estimatedSize()); |
4.2、基于時間回收
這種回收策略是基于條目的到期時間,有三種類型:
- 訪問后到期 — 從上次讀或寫發生后,條目即過期。
- 寫入后到期 — 從上次寫入發生之后,條目即過期
- 自定義策略 — 到期時間由 Expiry 實現獨自計算
讓我們使用 expireAfterAccess 方法配置訪問后過期策略:
1
2
3
|
LoadingCache<String, DataObject> cache = Caffeine.newBuilder() .expireAfterAccess( 5 , TimeUnit.MINUTES) .build(k -> DataObject.get( "Data for " + k)); |
要配置寫入后到期策略,我們使用 expireAfterWrite 方法:
1
2
3
4
5
|
cache = Caffeine.newBuilder() .expireAfterWrite( 10 , TimeUnit.SECONDS) .weakKeys() .weakValues() .build(k -> DataObject.get( "Data for " + k)); |
要初始化自定義策略,我們需要實現 Expiry 接口:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
cache = Caffeine.newBuilder().expireAfter( new Expiry<String, DataObject>() { @Override public long expireAfterCreate( String key, DataObject value, long currentTime) { return value.getData().length() * 1000 ; } @Override public long expireAfterUpdate( String key, DataObject value, long currentTime, long currentDuration) { return currentDuration; } @Override public long expireAfterRead( String key, DataObject value, long currentTime, long currentDuration) { return currentDuration; } }).build(k -> DataObject.get( "Data for " + k)); |
4.3、基于引用回收
我們可以將緩存配置為啟用緩存鍵值的垃圾回收。為此,我們將 key 和 value 配置為 弱引用,并且我們可以僅配置軟引用以進行垃圾回收。
當沒有任何對對象的強引用時,使用 WeakRefence 可以啟用對象的垃圾收回收。SoftReference 允許對象根據 JVM 的全局最近最少使用(Least-Recently-Used)的策略進行垃圾回收。有關 Java 引用的更多詳細信息,請參見此處。
我們應該使用 Caffeine.weakKeys()、Caffeine.weakValues() 和 Caffeine.softValues() 來啟用每個選項:
1
2
3
4
5
6
7
8
9
10
|
LoadingCache<String, DataObject> cache = Caffeine.newBuilder() .expireAfterWrite( 10 , TimeUnit.SECONDS) .weakKeys() .weakValues() .build(k -> DataObject.get( "Data for " + k)); cache = Caffeine.newBuilder() .expireAfterWrite( 10 , TimeUnit.SECONDS) .softValues() .build(k -> DataObject.get( "Data for " + k)); |
5、刷新
可以將緩存配置為在定義的時間段后自動刷新條目。讓我們看看如何使用 refreshAfterWrite 方法:
1
2
3
|
Caffeine.newBuilder() .refreshAfterWrite( 1 , TimeUnit.MINUTES) .build(k -> DataObject.get( "Data for " + k)); |
這里我們應該要明白 expireAfter 和 refreshAfter 之間的區別。 當請求過期條目時,執行將發生阻塞,直到 build Function 計算出新值為止。
但是,如果條目可以刷新,則緩存將返回一個舊值,并異步重新加載該值。
6、統計
Caffeine 有一種記錄緩存使用情況的統計方式:
1
2
3
4
5
6
7
8
9
|
LoadingCache<String, DataObject> cache = Caffeine.newBuilder() .maximumSize( 100 ) .recordStats() .build(k -> DataObject.get( "Data for " + k)); cache.get( "A" ); cache.get( "A" ); assertEquals( 1 , cache.stats().hitCount()); assertEquals( 1 , cache.stats().missCount()); |
我們也可能會傳入 recordStats supplier,創建一個 StatsCounter 的實現。每次與統計相關的更改將推送此對象。
7、結論
在本文中,我們熟悉了 Java 的 Caffeine 緩存庫。 我們看到了如何配置和填充緩存,以及如何根據我們的需要選擇適當的到期或刷新策略。
文中示例的源代碼可以在 Github 上找到。
以上就是本文的全部內容,希望對大家的學習有所幫助,也希望大家多多支持服務器之家。
原文鏈接:http://www.cnblogs.com/oopsguy/p/7731659.html