Caffeine
說起Guava Cache,很多人都不會陌生,它是Google Guava工具包中的一個非常方便易用的本地化緩存實現,基於LRU算法實現,支持多種緩存過期策略。由於Guava的大量使用,Guava Cache也得到了大量的應用。但是,Guava Cache的性能一定是最好的嗎?也許,曾經,它的性能是非常不錯的。但所謂長江后浪推前浪,總會有更加優秀的技術出現。今天,我就來介紹一個比Guava Cache性能更高的緩存框架:Caffeine。
Tips: Spring5(SpringBoot2)開始用Caffeine取代guava.詳見官方信息SPR-13797
https://jira.spring.io/browse/SPR-13797
什么時候用
- 願意消耗一些內存空間來提升速度
- 預料到某些鍵會被多次查詢
- 緩存中存放的數據總量不會超出內存容量
性能
由圖可以看出,Caffeine不論讀還是寫的效率都遠高於其他緩存。
這里只列出部分性能比較,詳細請看官方官方 https://github.com/ben-manes/caffeine/wiki/Benchmarks
依賴
我們需要在 pom.xml 中添加 caffeine 依賴:
版本問題參考https://mvnrepository.com/artifact/com.github.ben-manes.caffeine/caffeine
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
<version>2.7.0</version>
</dependency>
新建對象
// 1、最簡單
Cache<String, Object> cache = Caffeine.newBuilder()
.build();
// 2、真實使用過程中我們需要自己配置參數。這里只列舉部分,具體請看下面列表
Cache<String, Object> cache = Caffeine.newBuilder()
.initialCapacity(2)//初始大小
.maximumSize(2)//最大數量
.expireAfterWrite(3, TimeUnit.SECONDS)//過期時間
.build();
參數含義
- initialCapacity: 初始的緩存空間大小
- maximumSize: 緩存的最大數量
- maximumWeight: 緩存的最大權重
- expireAfterAccess: 最后一次讀或寫操作后經過指定時間過期
- expireAfterWrite: 最后一次寫操作后經過指定時間過期
- refreshAfterWrite: 創建緩存或者最近一次更新緩存后經過指定時間間隔,刷新緩存
- weakKeys: 打開key的弱引用
- weakValues:打開value的弱引用
- softValues:打開value的軟引用
- recordStats:開發統計功能
注意:
expireAfterWrite和expireAfterAccess同時存在時,以expireAfterWrite為准。
maximumSize和maximumWeight不可以同時使用
異步
AsyncCache<Object, Object> asyncCache = Caffeine.newBuilder()
.buildAsync();
解釋
A semi-persistent mapping from keys to values. Cache entries are manually added using
{@link #get(Object, Function)} or {@link #put(Object, CompletableFuture)}, and are stored in the
cache until either evicted or manually invalidated.
Implementations of this interface are expected to be thread-safe, and can be safely accessed by
multiple concurrent threads.
添加數據
Caffeine 為我們提供了三種填充策略:
手動、同步和異步
手動添加
很簡單的
public static void main(String[] args) {
Cache<String, String> cache = Caffeine.newBuilder()
.build();
cache.put("hello", "world");
System.out.println(cache.getIfPresent("hello"));
}
自動添加1(自定義添加函數)
Cache<String, String> cache = Caffeine.newBuilder()
.build();
// 1.如果緩存中能查到,則直接返回
// 2.如果查不到,則從我們自定義的getValue方法獲取數據,並加入到緩存中
cache.get("hello", new Function<String, String>() {
@Override
public String apply(String k) {
return getValue(k);
}
});
System.out.println(cache.getIfPresent("hello"));
}
// 緩存中找不到,則會進入這個方法。一般是從數據庫獲取內容
private static String getValue(String k) {
return k + ":value";
// 這種寫法可以簡化成下面Lambda表達式
cache.get("hello", new Function<String, String>() {
@Override
public String apply(String k) {
return getValue(k);
}
});
// 可以簡寫為
cache.get("hello", k -> getValue(k));
自動添加2(初始添加)
和上面方法一樣,只不過這個是在新建對象的時候添加
LoadingCache<String, String> loadingCache = Caffeine.newBuilder()
.build(new CacheLoader<String, String>() {
@Override
public String load(String k) {
return getValue(k);
}
});
// 同樣可簡化為下面這樣
LoadingCache<String, String> loadingCache2 = Caffeine.newBuilder()
.build(k -> getValue(k));
過期策略
Caffeine提供三類驅逐策略:
- 基於大小(size-based)
- 基於時間(time-based)
- 基於引用(reference-based)
1、大小
Cache<String, String> cache = Caffeine.newBuilder()
.maximumSize(3)
.build();
cache.put("key1", "value1");
cache.put("key2", "value2");
cache.put("key3", "value3");
cache.put("key4", "value4");
cache.put("key5", "value5");
cache.cleanUp();
System.out.println(cache.getIfPresent("key1"));
System.out.println(cache.getIfPresent("key2"));
System.out.println(cache.getIfPresent("key3"));
System.out.println(cache.getIfPresent("key4"));
System.out.println(cache.getIfPresent("key5"));
輸出結果
null
value2
null
value4
value5
1、淘汰2個
2、淘汰並不是按照先后順序,內部有自己的算法
2、時間
Caffeine提供了三種定時驅逐策略:
- expireAfterAccess(long, TimeUnit):在最后一次訪問或者寫入后開始計時,在指定的時間后過期。假如一直有請求訪問該key,那么這個緩存將一直不會過期。
- expireAfterWrite(long, TimeUnit): 在最后一次寫入緩存后開始計時,在指定的時間后過期。
- expireAfter(Expiry): 自定義策略,過期時間由Expiry實現獨自計算。
緩存的刪除策略使用的是惰性刪除和定時刪除。這兩個刪除策略的時間復雜度都是O(1)。
expireAfterWrite
Cache<String, String> cache = Caffeine.newBuilder()
.expireAfterWrite(3, TimeUnit.SECONDS)
.build();
cache.put("key1", "value1");
cache.put("key2", "value2");
cache.put("key3", "value3");
cache.put("key4", "value4");
cache.put("key5", "value5");
System.out.println(cache.getIfPresent("key1"));
System.out.println(cache.getIfPresent("key2"));
Thread.sleep(3*1000);
System.out.println(cache.getIfPresent("key3"));
System.out.println(cache.getIfPresent("key4"));
System.out.println(cache.getIfPresent("key5"));
結果
value1
value2
null
null
null
例子2
Cache<String, String> cache = Caffeine.newBuilder()
.expireAfterWrite(3, TimeUnit.SECONDS)
.build();
cache.put("key1", "value1");
Thread.sleep(1*1000);
System.out.println(cache.getIfPresent("key1"));
Thread.sleep(1*1000);
System.out.println(cache.getIfPresent("key1"));
Thread.sleep(1*1000);
System.out.println(cache.getIfPresent("key1"));
結果
value1
value1
null
expireAfterAccess
Access就是讀和寫
Cache<String, String> cache = Caffeine.newBuilder()
.expireAfterAccess(3, TimeUnit.SECONDS)
.build();
cache.put("key1", "value1");
Thread.sleep(1*1000);
System.out.println(cache.getIfPresent("key1"));
Thread.sleep(1*1000);
System.out.println(cache.getIfPresent("key1"));
Thread.sleep(1*1000);
System.out.println(cache.getIfPresent("key1"));
Thread.sleep(3*1000);
System.out.println(cache.getIfPresent("key1"));
結果
value1
value1
value1
null
讀和寫都沒有的情況下,3秒后才過期
也可以同時用expireAfterAccess和expireAfterWrite方法指定過期時間,這時只要對象滿足兩者中的一個條件就會被自動過期刪除。
expireAfter 和 refreshAfter 之間的區別
- expireAfter 條件觸發后,新的值更新完成前,所有請求都會被阻塞,更新完成后其他請求才能訪問這個值。這樣能確保獲取到的都是最新的值,但是有性能損失。
- refreshAfter 條件觸發后,新的值更新完成前也可以訪問,不會被阻塞,只是獲取的是舊的數據。更新結束后,獲取的才是新的數據。有可能獲取到臟數據。
3、引用
- Caffeine.weakKeys() 使用弱引用存儲key。如果沒有其他地方對該key有強引用,那么該緩存就會被垃圾回收器回收。
- Caffeine.weakValues() 使用弱引用存儲value。如果沒有其他地方對該value有強引用,那么該緩存就會被垃圾回收器回收。
- Caffeine.softValues() 使用軟引用存儲value。
Cache<String, Object> cache = Caffeine.newBuilder()
.weakValues()
.build();
Object value1 = new Object();
Object value2 = new Object();
cache.put("key1", value1);
cache.put("key2", value2);
value2 = new Object(); // 原對象不再有強引用
System.gc();
System.out.println(cache.getIfPresent("key1"));
System.out.println(cache.getIfPresent("key2"));
結果
java.lang.Object@7a4f0f29
null
解釋:當給value2引用賦值一個新的對象之后,就不再有任何一個強引用指向原對象。System.gc()觸發垃圾回收后,原對象就被清除了。
簡單回顧下Java中的四種引用
Java4種引用的級別由高到低依次為:強引用 > 軟引用 > 弱引用 > 虛引用
引用類型 | 被垃圾回收時間 | 用途 | 生存時間 |
---|---|---|---|
強引用 | 從來不會 | 對象的一般狀態 | JVM停止運行時終止 |
軟引用 | 在內存不足時 | 對象緩存 | 內存不足時終止 |
弱引用 | 在垃圾回收時 | 對象緩存 | GC運行后終止 |
虛引用 | Unknown | Unknown | Unknown |
顯式刪除緩存
除了通過上面的緩存淘汰策略刪除緩存,我們還可以手動的刪除
// 1、指定key刪除
cache.invalidate("key1");
// 2、批量指定key刪除
List<String> list = new ArrayList<>();
list.add("key1");
list.add("key2");
cache.invalidateAll(list);//批量清除list中全部key對應的記錄
// 3、刪除全部
cache.invalidateAll();
淘汰、移除監聽器
可以為緩存對象添加一個移除監聽器,這樣當有記錄被刪除時可以感知到這個事件。
Cache<String, String> cache = Caffeine.newBuilder()
.expireAfterAccess(3, TimeUnit.SECONDS)
.removalListener(new RemovalListener<Object, Object>() {
@Override
public void onRemoval(@Nullable Object key, @Nullable Object value, @NonNull RemovalCause cause) {
System.out.println("key:" + key + ",value:" + value + ",刪除原因:" + cause);
}
})
.expireAfterWrite(1, TimeUnit.SECONDS)
.build();
cache.put("key1", "value1");
cache.put("key2", "value2");
cache.invalidate("key1");
Thread.sleep(2 * 1000);
cache.cleanUp();
結果
key:key1,value:value1,刪除原因:EXPLICIT
key:key2,value:value2,刪除原因:EXPIRED
統計
Cache<String, String> cache = Caffeine.newBuilder()
.maximumSize(3)
.recordStats()
.build();
cache.put("key1", "value1");
cache.put("key2", "value2");
cache.put("key3", "value3");
cache.put("key4", "value4");
cache.getIfPresent("key1");
cache.getIfPresent("key2");
cache.getIfPresent("key3");
cache.getIfPresent("key4");
cache.getIfPresent("key5");
cache.getIfPresent("key6");
System.out.println(cache.stats());
結果
CacheStats{hitCount=4, missCount=2, loadSuccessCount=0, loadFailureCount=0, totalLoadTime=0, evictionCount=0, evictionWeight=0}
除了結果輸出的內容,CacheStats還可以獲取如下數據。
參考
http://oopsguy.com/2017/10/25/java-caching-caffeine/
https://juejin.im/post/5b8df63c6fb9a019e04ebaf4
https://www.jianshu.com/p/9a80c662dac4
https://www.sohu.com/a/235729991_100109711
https://www.cnblogs.com/yueshutong/p/9381540.html
https://blog.csdn.net/qq_38974634/article/details/80650810
https://blog.csdn.net/qq_32867467/article/details/82944506
https://blog.csdn.net/grafx/article/details/80462628
http://ifeve.com/google-guava-cachesexplained/