地表最帥緩存Caffeine

簡介

緩存是程序員們繞不開的話題,像是常用的本地緩存Guava,分佈式緩存Redis等,是提供高性能服務的基礎。今天敬姐帶大家一起認識一個更高效的本地緩存——Caffeine

img

Caffeine Cache使用了基於內存的存儲策略,並且支持高併發、低延遲,同時還提供了緩存過期、定時刷新、緩存大小控制等功能。Caffeine是一個Java高性能的本地緩存庫。據其官方說明,因使用 Window TinyLfu 回收策略,其緩存命中率已經接近最優值。此處應有掌聲👏🏻

它是Guava Cache的升級版本, 但是比Guava Cache更快,更穩定。Caffeine Cache最適合做數據量不大,但是讀寫頻繁的應用場景。結合Redis等可以實現應用中的多級緩存策略。
!

還是老套路,先寫代碼示例,對Caffeine的帥有個膚淺的認識,後面再去研究他的內在機制

Caffeine緩存類型

新建項目,添加caffeine的maven引用

<!-- https://mvnrepository.com/artifact/com.github.ben-manes.caffeine/caffeine -->
<dependency>
    <groupId>com.github.ben-manes.caffeine</groupId>
    <artifactId>caffeine</artifactId>
    <version>3.1.7</version>
</dependency>

Caffeine四種緩存類型:Cache, LoadingCache, AsyncCache, AsyncLoadingCache

1. Cache

在獲取緩存值時,如果想要在緩存值不存在時,原子地將值寫入緩存,則可以調用get(key, k -> value)方法,該方法將避免寫入競爭。
在多線程情況下,當使用get(key, k -> value)時,如果有另一個線程同時調用本方法進行競爭,則後一線程會被阻塞,直到前一線程更新緩存完成;而若另一線程調用getIfPresent()方法,則會立即返回null,不會被阻塞。

public class CacheDemo {
    static Cache<Integer, Article> cache = Caffeine.newBuilder()
            .maximumSize(1000)
            .expireAfterWrite(10, TimeUnit.MINUTES)
            .build();

    public static void main(String[] args) {
        //get
        System.out.println(cache.get(1, x -> new Article(x)));//Article{id=1, title='title 1'}

        //getIfPresent
        System.out.println(cache.getIfPresent(2));//null

        //put 設置緩存
        cache.put(2, new Article(2));
        System.out.println(cache.getIfPresent(2));//Article{id=2, title='title 2'}

        //invalidate 移除緩存
        cache.invalidate(2);
        System.out.println(cache.getIfPresent(2));//null
    }
}

2.LoadingCache

LoadingCache是一種自動加載的緩存。使用時需要指定CacheLoader,並實現其中的load()方法供緩存缺失時自動加載。
get()方法用來讀取緩存,和上面的Cache方式不同之處在於,當緩存不存在或者已經過期時,會自動調用CacheLoader.load()方法加載最新值。加載過程是一種同步操作,將返回值插入緩存並且返回。

/**
 * LoadingCache示例,自動加載的緩存
 *
 * @author chenjing
 */
public class LoadingCacheDemo {
    private static LoadingCache<Integer, Article> cache = Caffeine.newBuilder()
            .build(new CacheLoader<>() {
                @Override
                public @Nullable Article load(Integer id) {
                    return new Article(id);
                }
            });

    public static void main(String[] args) {
        System.out.println(cache.get(1));//Article{id=1, title='title 1'}

        //getIfPresent
        System.out.println(cache.getIfPresent(2));//null


        System.out.println(cache.getAll(List.of(10,20)));//{10=Article{id=10, title='title 10'}, 20=Article{id=20, title='title 20'}}
    }
}

3.AsyncCache

和Cache類似,但是異步執行操作,並返回保存實際值的CompletableFuture,適用於需要進行併發執行提高吞吐量的場景。

/**
 * Caffeine 異步緩存
 *
 * @author chenjing
 */
public class AsyncCacheDemo {
    static AsyncCache<Integer, Article> cache = Caffeine.newBuilder()
            .maximumSize(1000)
            .expireAfterWrite(10, TimeUnit.MINUTES)
            .buildAsync();

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        //get 返回的是CompletableFuture
        CompletableFuture<Article> future = cache.get(1, x -> new Article(x));
        System.out.println(future.get());//Article{id=1, title='title 1'}
    }
}

4.AsyncLoadingCache

異步加載緩存

/**
 * Caffeine AsyncLoadingCache 異步自動加載緩存
 * @author chenjing 
 */
public class AsyncLoadingCacheDemo {
    private static AsyncLoadingCache<Integer, Article> asyncLoadingCache =
            Caffeine.newBuilder()
                    .maximumSize(1000)
                    .expireAfterWrite(10, TimeUnit.MINUTES)
                    .buildAsync(
                            (key, executor) -> CompletableFuture.supplyAsync(() -> new Article(key), executor)
                    );

    public static void main(String[] args) {

        CompletableFuture<Article> userCompletableFuture = asyncLoadingCache.get(66);
        System.out.println(userCompletableFuture.join());//Article{id=66, title='title 66'}
    }
} 

緩存過期

  1. 基於緩存大小
    通過 maximumSize() 指定緩存大小。
/**
 * 測試Caffeine基於空間的驅逐策略
 *
 * @author chenjing
 */
public class MaxSizeExpire {
    public static void main(String[] args) {
        System.out.println("測試基於容量過期的緩存");
        Integer size = 10;
        Cache<Integer, Article> cache = Caffeine.newBuilder()
                .maximumSize(size)
                .recordStats()
                .build();
        cache.put(1, new Article(1));
        System.out.println("放入一條數據,獲取看看");
        System.out.println(cache.getIfPresent(1));//Article{id=1, title='title 1'}

        System.out.println("放入" + size + "條數據");
        for (int i = 2; i <= size + 5; i++) {
            cache.put(i, new Article(i));
        }

        System.out.println("再打印第一條數據看看");
        System.out.println(cache.getIfPresent(1));//Article{id=1, title='title 1'}

        //打印一下緩存狀態
        System.out.println(cache.stats());//CacheStats{hitCount=2, missCount=0, loadSuccessCount=0, loadFailureCount=0, totalLoadTime=0, evictionCount=5, evictionWeight=5}
    }
}

  1. 基於緩存寫入時間
    一般最近一段時間緩存的數據纔是有效的,緩存很久之前的業務數據是沒有意義的。常見的一種場景就是,緩存寫入之後N分鐘之後自動失效。
/**
 * Caffeine expireAfterWrite
 * @author chenjing
 */
public class ExpireAfterWriteDemo {
    public static void main(String[] args) throws InterruptedException {
        System.out.println("測試基於時間過期的緩存");
        Integer seconds = 5;
        Integer size = 5;
        Cache<Integer, Article> cache = Caffeine.newBuilder()
                //基於時間過期
                .expireAfterWrite(seconds, TimeUnit.SECONDS)
                //監控緩存移除
                .removalListener((Integer key, Article value, RemovalCause cause) ->
                        System.out.printf("移除key %s,原因是 %s %n", key, cause))
                .build();

        System.out.println("放入" + size + "條數據");
        for (int i = 1; i <= size; i++) {
            cache.put(i, new Article(i));
            System.out.println(cache.getIfPresent(i));
        }

        System.out.println("sleep 10 seconds");
        Thread.sleep(10000);

        for (int i = 1; i <= size; i++) {
            cache.put(i, new Article(i));
            System.out.println(cache.getIfPresent(i));
        }
    }
}

執行結果:

測試基於時間過期的緩存
放入5條數據
Article{id=1, title='title 1'}
Article{id=2, title='title 2'}
Article{id=3, title='title 3'}
Article{id=4, title='title 4'}
Article{id=5, title='title 5'}
sleep 10 seconds
移除key 1,原因是 EXPIRED
Article{id=1, title='title 1'}
Article{id=2, title='title 2'}
移除key 2,原因是 EXPIRED
移除key 3,原因是 EXPIRED
Article{id=3, title='title 3'}
移除key 4,原因是 EXPIRED
移除key 5,原因是 EXPIRED
Article{id=4, title='title 4'}
Article{id=5, title='title 5'}

緩存刷新

refreshAfterWrite()和expireAfterWrite()不同,在刷新的時候如果查詢緩存元素,其舊值將仍被返回,直到該元素的刷新完畢後結束後纔會返回刷新後的新值。這裏的刷新操作默認也是由線程池ForkJoinPool.commonPool()異步執行的,我們也可以通過Caffeine.executor()重寫來指定自定義的線程池。

public class RefreshAfterWriteDemo {
    public static void main(String[] args) throws InterruptedException {
        LoadingCache<Integer, Article> cache = Caffeine.newBuilder()
                .refreshAfterWrite(2, TimeUnit.SECONDS)
                .build(new CacheLoader<Integer, Article>() {
                    @Override
                    public @Nullable Article load(Integer integer) {
                        return new Article(integer);
                    }

                    @Override
                    public @Nullable Article reload(Integer key, Article oldValue) {
                        return new Article(key + 100);
                    }
                });

        cache.put(1, new Article(1));
        for (int i = 0; i < 3; i++) {
            System.out.println(cache.get(1));
            Thread.sleep(3000);
        }
    }
}

緩存監控

創建Caffeine時設置removalListener,可以監聽緩存地清除或更新監聽。

 Cache<Integer, Article> cache = Caffeine.newBuilder()
                //基於時間過期
                .expireAfterWrite(seconds, TimeUnit.SECONDS)
                //監控緩存移除
                .removalListener((Integer key, Article value, RemovalCause cause) ->
                        System.out.printf("移除key %s,原因是 %s %n", key, cause))
                .build();

當緩存中的數據發送更新,或者被清除時,就會觸發監聽器,在監聽器裏可以自定義一些處理手段,比如打印出哪個數據被清除,原因是什麼。這個觸發和監聽的過程是異步的,就是有可能數據都被刪除一小會兒了,監聽器才監聽到。


本人公衆號[ 敬YES ]同步更新,歡迎大家關注~

img

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章