簡介
緩存是程序員們繞不開的話題,像是常用的本地緩存Guava,分佈式緩存Redis等,是提供高性能服務的基礎。今天敬姐帶大家一起認識一個更高效的本地緩存——Caffeine。
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'}
}
}
緩存過期
- 基於緩存大小
通過 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}
}
}
- 基於緩存寫入時間
一般最近一段時間緩存的數據纔是有效的,緩存很久之前的業務數據是沒有意義的。常見的一種場景就是,緩存寫入之後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();
當緩存中的數據發送更新,或者被清除時,就會觸發監聽器,在監聽器裏可以自定義一些處理手段,比如打印出哪個數據被清除,原因是什麼。這個觸發和監聽的過程是異步的,就是有可能數據都被刪除一小會兒了,監聽器才監聽到。