Caffeine
說起Guava Cache,很多人都不會陌生,它是Google Guava工具包中的一個非常方便易用的本地化緩存實現,基于LRU算法實現,支持多種緩存過期策略。由于Guava的大量使用,Guava Cache也得到了大量的應用。但是,Guava Cache的性能一定是最好的嗎?也許,曾經,它的性能是非常不錯的。但所謂長江后浪推前浪,總會有更加優秀的技術出現。今天,我就來介紹一個比Guava Cache性能更高的緩存框架:Caffeine。
Tips: Spring5(SpringBoot2)開始用Caffeine取代guava.詳見官方信息SPR-13797 jira.spring.io/browse/SPR-…
什么時候用
- 愿意消耗一些內存空間來提升速度
- 預料到某些鍵會被多次查詢
- 緩存中存放的數據總量不會超出內存容量
性能
由圖可以看出,Caffeine不論讀還是寫的效率都遠高于其他緩存。
這里只列出部分性能比較,詳細請看官方官方 github.com/ben-manes/c…
依賴
我們需要在 pom.xml 中添加 caffeine 依賴:
版本問題參考mvnrepository.com/artifact/co…
<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>() {@Overridepublic 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>() {@Overridepublic 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>() {@Overridepublic 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還可以獲取如下數據。
參考
oopsguy.com/2017/10/25/… juejin.im/post/5b8df6… www.jianshu.com/p/9a80c662d… www.sohu.com/a/235729991… www.cnblogs.com/yueshutong/… blog.csdn.net/qq_38974634… blog.csdn.net/qq_32867467… blog.csdn.net/grafx/artic… ifeve.com/google-guav…