本地緩存Caffeine


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

什么時候用

  1. 願意消耗一些內存空間來提升速度
  2. 預料到某些鍵會被多次查詢
  3. 緩存中存放的數據總量不會超出內存容量

性能

由圖可以看出,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提供三類驅逐策略:

  1. 基於大小(size-based)
  2. 基於時間(time-based)
  3. 基於引用(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/


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM