作者:京東工業(yè) 孫磊
一、認識Caffeine
1、Caffeine是什么?
Caffeine是一個基于Java8開發(fā)的提供了近乎最佳命中率的高性能的緩存庫, 也是SpringBoot內(nèi)置的本地緩存實現(xiàn)。
2、Caffeine提供了靈活的構(gòu)造器去創(chuàng)建一個擁有下列特性的緩存:
?自動加載條目到緩存中,可選異步方式
?可以基于大小剔除
?可以設(shè)置過期時間,時間可以從上次訪問或上次寫入開始計算
?異步刷新
?keys自動包裝在弱引用中
?values自動包裝在弱引用或軟引用中
?條目剔除通知
?緩存訪問統(tǒng)計
3、核心類和參數(shù)
核心工具類:Caffeine是創(chuàng)建高性能緩存的基類。
核心參數(shù):
maximumSize
:緩存最大值
maximumWeight
:緩存最大權(quán)重,權(quán)重和最大值不能同時設(shè)置
initialCapacity
:緩存初始容量
expireAfterWriteNanos
:在寫入多少納秒沒更新后過期
expireAfterAccessNanos
:在訪問多少納秒沒更新后過期
refreshAfterWriteNanos
:寫入多少納秒沒更新后更新
二、數(shù)據(jù)加載
Caffeine提供了四種緩存添加策略
1、手動加載
public static void demo() {
Cache cache =
Caffeine.newBuilder()
.expireAfterAccess(Duration.ofMinutes(1))
.maximumSize(100)
.recordStats()
.build();
// 插入數(shù)據(jù)
cache.put("a", "a");
// 查詢某個key,如果沒有返回空
String a = cache.getIfPresent("a");
System.out.println(a);
// 查找緩存,如果緩存不存在則生成緩存元素, 如果無法生成則返回null
String b = cache.get("b", k -> {
System.out.println("begin query ..." + Thread.currentThread().getName());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
}
System.out.println("end query ...");
return UUID.randomUUID().toString();
});
System.out.println(b);
// 移除一個緩存元素
cache.invalidate("a");
}
2、自動加載
public static void demo() {
LoadingCache loadingCache = Caffeine.newBuilder()
.maximumSize(100)
.expireAfterWrite(10, TimeUnit.MINUTES)
.build(new CacheLoader() {
@Nullable
@Override
public Object load(@NonNull Object key) throws Exception {
return createExpensiveValue();
}
@Override
public @NonNull Map loadAll(@NonNull Iterable keys) throws Exception {
if (keys == null) {
return Collections.emptyMap();
}
Map map = new HashMap?>();
for (Object key : keys) {
map.put((String) key, createExpensiveValue());
}
return map;
}
});
// 查找緩存,如果緩存不存在則生成緩存元素, 如果無法生成則返回null
String a = loadingCache.get("a");
System.out.println(a);
// 批量查找緩存,如果緩存不存在則生成緩存元素
Set keys = new HashSet?>();
keys.add("a");
keys.add("b");
Map allValues = loadingCache.getAll(keys);
System.out.println(allValues);
}
private static String createExpensiveValue() {
{
System.out.println("begin query ..." + Thread.currentThread().getName());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
}
System.out.println("end query ...");
return UUID.randomUUID().toString();
}
}
一個LoadingCache是Cache附加一個CacheLoader能力之后的緩存實現(xiàn)。
getAll方法中,將會對每個key調(diào)用一次CacheLoader.load來生成元素,當批量查詢效率更高的時候,你可以自定義loadAll方法實現(xiàn)。
3、手動異步加載
public static void demo() throws ExecutionException, InterruptedException {
AsyncCache asyncCache = Caffeine.newBuilder()
.maximumSize(100)
.buildAsync();
// 添加或者更新一個緩存元素
asyncCache.put("a",CompletableFuture.completedFuture("a"));
// 查找一個緩存元素, 沒有查找到的時候返回null
CompletableFuture a = asyncCache.getIfPresent("a");
System.out.println(a.get());
// 查找緩存元素,如果不存在,則異步生成
CompletableFuture completableFuture = asyncCache.get("b", k ->createExpensiveValue("b"));
System.out.println(completableFuture.get());
// 移除一個緩存元素
asyncCache.synchronous().invalidate("a");
System.out.println(asyncCache.getIfPresent("a"));
}
private static String createExpensiveValue(String key) {
{
System.out.println("begin query ..." + Thread.currentThread().getName());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
}
System.out.println("end query ...");
return UUID.randomUUID().toString();
}
}
一個`AsyncCache`是 `Cache`的一個變體,`AsyncCache`提供了在 Executor上生成緩存元素并返回 CompletableFuture的能力。這給出了在當前流行的響應(yīng)式編程模型中利用緩存的能力。
synchronous()
方法給 Cache
提供了阻塞直到異步緩存生成完畢的能力。
異步緩存默認的線程池實現(xiàn)是 ForkJoinPool.commonPool() ,你也可以通過覆蓋并實現(xiàn) `Caffeine.executor(Executor)`方法來自定義你的線程池選擇。
4、自動異步加載
public static void demo() throws ExecutionException, InterruptedException {
AsyncLoadingCache cache = Caffeine.newBuilder()
.maximumSize(10_000)
.expireAfterWrite(10, TimeUnit.MINUTES)
// 你可以選擇: 去異步的封裝一段同步操作來生成緩存元素
//.buildAsync(key -> createExpensiveValue(key));
// 你也可以選擇: 構(gòu)建一個異步緩存元素操作并返回一個future
.buildAsync((key, executor) ->createExpensiveValueAsync(key, executor));
// 查找緩存元素,如果其不存在,將會異步進行生成
CompletableFuture a = cache.get("a");
System.out.println(a.get());
// 批量查找緩存元素,如果其不存在,將會異步進行生成
Set keys = new HashSet?>();
keys.add("a");
keys.add("b");
CompletableFuture> values = cache.getAll(keys);
System.out.println(values.get());
}
private static String createExpensiveValue(String key) {
{
System.out.println("begin query ..." + Thread.currentThread().getName());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
}
System.out.println("end query ...");
return UUID.randomUUID().toString();
}
}
private static CompletableFuture createExpensiveValueAsync(String key, Executor executor) {
{
System.out.println("begin query ..." + Thread.currentThread().getName());
try {
Thread.sleep(1000);
executor.execute(()-> System.out.println("async create value...."));
} catch (InterruptedException e) {
}
System.out.println("end query ...");
return CompletableFuture.completedFuture(UUID.randomUUID().toString());
}
}
一個 AsyncLoadingCache
是一個 AsyncCache
加上 AsyncCacheLoader
能力的實現(xiàn)。
在需要同步的方式去生成緩存元素的時候,CacheLoader
是合適的選擇。而在異步生成緩存的場景下, AsyncCacheLoader
則是更合適的選擇并且它會返回一個 CompletableFuture。
三、驅(qū)除策略
Caffeine 提供了三種驅(qū)逐策略,分別是基于容量,基于時間和基于引用三種類型;還提供了手動移除方法和監(jiān)聽器。
1、基于容量
// 基于緩存容量大小,緩存中個數(shù)進行驅(qū)逐
Cache cache =
Caffeine.newBuilder()
.maximumSize(100)
.recordStats()
.build();
// 基于緩存的權(quán)重進行驅(qū)逐
AsyncCache asyncCache = Caffeine.newBuilder()
.maximumWeight(10)
.buildAsync();
2、基于時間
// 基于固定時間
Cache cache =
Caffeine.newBuilder()
//距離上次訪問后一分鐘刪除
.expireAfterAccess(Duration.ofMinutes(1))
.recordStats()
.build();
Cache cache =
Caffeine.newBuilder()
// 距離上次寫入一分鐘后刪除
.expireAfterWrite(Duration.ofMinutes(1))
.recordStats()
.build();
// 基于不同的過期驅(qū)逐策略
Cache expire =
Caffeine.newBuilder()
.expireAfter(new Expiry() {
@Override
public long expireAfterCreate(@NonNull String key, @NonNull String value, long currentTime) {
return LocalDateTime.now().plusMinutes(5).getSecond();
}
@Override
public long expireAfterUpdate(@NonNull String key, @NonNull String value, long currentTime, @NonNegative long currentDuration) {
return currentDuration;
}
@Override
public long expireAfterRead(@NonNull String key, @NonNull String value, long currentTime, @NonNegative long currentDuration) {
return currentDuration;
}
})
.recordStats()
.build();
Caffeine提供了三種方法進行基于時間的驅(qū)逐:
?expireAfterAccess(long, TimeUnit):
一個值在最近一次訪問后,一段時間沒訪問時被淘汰。
?expireAfterWrite(long, TimeUnit):
一個值在初次創(chuàng)建或最近一次更新后,一段時間后被淘汰。
?expireAfter(Expiry):
一個值將會在指定的時間后被認定為過期項。
3、基于引用
java對象引用匯總表:
?
?
?
// 當key和緩存元素都不再存在其他強引用的時候驅(qū)逐
LoadingCache weak = Caffeine.newBuilder()
.weakKeys()
.weakValues()
.build(k ->createExpensiveValue());
// 當進行GC的時候進行驅(qū)逐
LoadingCache soft = Caffeine.newBuilder()
.softValues()
.build(k ->createExpensiveValue());
weakKeys:使用弱引用存儲key時,當沒有其他的強引用時,則會被垃圾回收器回收。
weakValues:使用弱引用存儲value時,當沒有其他的強引用時,則會被垃圾回收器回收。
softValues:使用軟引用存儲key時,當沒有其他的強引用時,內(nèi)存不足時會被回收。
4、手動移除
Cache cache =
Caffeine.newBuilder()
.expireAfterWrite(Duration.ofMinutes(1))
.recordStats()
.build();
// 單個刪除
cache.invalidate("a");
// 批量刪除
Set keys = new HashSet?>();
keys.add("a");
keys.add("b");
cache.invalidateAll(keys);
// 失效所有key
cache.invalidateAll();
任何時候都可以手動刪除,不用等到驅(qū)逐策略生效。
5、移除監(jiān)聽器
Cache cache =
Caffeine.newBuilder()
.expireAfterWrite(Duration.ofMinutes(1))
.recordStats()
.evictionListener(new RemovalListener() {
@Override
public void onRemoval(@Nullable Object key, @Nullable Object value, @NonNull RemovalCause cause) {
System.out.println("element evict cause" + cause.name());
}
})
.removalListener(new RemovalListener() {
@Override
public void onRemoval(@Nullable Object key, @Nullable Object value, @NonNull RemovalCause cause) {
System.out.println("element removed cause" + cause.name());
}
}).build();
你可以為你的緩存通過Caffeine.removalListener(RemovalListener)
方法定義一個移除監(jiān)聽器在一個元素被移除的時候進行相應(yīng)的操作。這些操作是使用 Executor異步執(zhí)行的,其中默認的 Executor 實現(xiàn)是 ForkJoinPool.commonPool()并且可以通過覆蓋Caffeine.executor(Executor)
方法自定義線程池的實現(xiàn)。
注意:Caffeine.evictionListener(RemovalListener)
。這個監(jiān)聽器將在 RemovalCause.wasEvicted()
為 true 的時候被觸發(fā)。
6、驅(qū)逐原因匯總
EXPLICIT:如果原因是這個,那么意味著數(shù)據(jù)被我們手動的remove掉了 REPLACED:就是替換了,也就是put數(shù)據(jù)的時候舊的數(shù)據(jù)被覆蓋導(dǎo)致的移除 COLLECTED:這個有歧義點,其實就是收集,也就是垃圾回收導(dǎo)致的,一般是用弱引用或者軟引用會導(dǎo)致這個情況 EXPIRED:數(shù)據(jù)過期,無需解釋的原因。 SIZE:個數(shù)超過限制導(dǎo)致的移除
四、緩存統(tǒng)計
Caffeine
通過使用Caffeine.recordStats()
方法可以打開數(shù)據(jù)收集功能,可以幫助優(yōu)化緩存使用。
// 緩存訪問統(tǒng)計
CacheStats stats = cache.stats();
System.out.println("stats.hitCount():"+stats.hitCount());//命中次數(shù)
System.out.println("stats.hitRate():"+stats.hitRate());//緩存命中率
System.out.println("stats.missCount():"+stats.missCount());//未命中次數(shù)
System.out.println("stats.missRate():"+stats.missRate());//未命中率
System.out.println("stats.loadSuccessCount():"+stats.loadSuccessCount());//加載成功的次數(shù)
System.out.println("stats.loadFailureCount():"+stats.loadFailureCount());//加載失敗的次數(shù),返回null
System.out.println("stats.loadFailureRate():"+stats.loadFailureRate());//加載失敗的百分比
System.out.println("stats.totalLoadTime():"+stats.totalLoadTime());//總加載時間,單位ns
System.out.println("stats.evictionCount():"+stats.evictionCount());//驅(qū)逐次數(shù)
System.out.println("stats.evictionWeight():"+stats.evictionWeight());//驅(qū)逐的weight值總和
System.out.println("stats.requestCount():"+stats.requestCount());//請求次數(shù)
System.out.println("stats.averageLoadPenalty():"+stats.averageLoadPenalty());//單次load平均耗時
-
JAVA
+關(guān)注
關(guān)注
19文章
2960瀏覽量
104562 -
計算
+關(guān)注
關(guān)注
2文章
446瀏覽量
38739 -
緩存
+關(guān)注
關(guān)注
1文章
233瀏覽量
26649
發(fā)布評論請先 登錄
相關(guān)推薦
評論