Caffeine本地高性能緩存組件
1. 簡介
Caffeine是一個用于Java應用程序的高性能緩存框架。它提供了一個強大且易于使用的緩存庫,可以在應用程序中使用,以提高數據訪問的速度和效率。
下面是一些Caffeine緩存框架的主要特點:
-
高性能:Caffeine的設計目標之一是提供卓越的性能。它通過使用高效的數據結構和優化的算法來實現快速的緩存訪問。與其他一些常見的緩存框架相比,Caffeine在緩存訪問的速度和響應時間上表現出色。
-
內存管理:Caffeine提供了靈活的內存管理選項。它支持基于大小、基于數量和基于權重的緩存大小限制。你可以根據應用程序的需求來選擇合適的緩存大小策略,并且可以通過配置參數進行進一步的調整。
-
強大的功能:Caffeine提供了許多強大的功能來滿足各種需求。它支持異步加載和刷新緩存項,可以設置過期時間和定時刷新策略,支持緩存項的自動刪除和手動失效等。此外,Caffeine還提供了統計信息和監聽器機制,可以方便地監控和管理緩存的狀態和變化。
-
線程安全:Caffeine是線程安全的,可以在多線程環境中安全地使用。它使用了細粒度的鎖定機制來保護共享資源,確保并發訪問的正確性和一致性。
-
易于集成:Caffeine是一個獨立的Java庫,可以很容易地與現有的應用程序集成。它與標準的Java并發庫和其他第三方庫兼容,并且可以與各種框架和技術(如Spring、Hibernate等)無縫集成。
官方文檔:https://github.com/ben-manes/caffeine/wiki/Home-zh-CN
2. Quick Start
寫幾個單元測試 熟悉一下 caffeine的基本用法
2.1 添加maven依賴
java8 最高只能使用2.x的版本
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
<version>2.9.3</version>
</dependency>
2.2 添加緩存
數據準備
private final List<User> users = Lists.newArrayList(
new User(1, "zhangsan"),
new User(2, "lisi"),
new User(3, "wangwu"));
private final int userKey = 1;
private final List<Integer> userKeys = Lists.newArrayList(1, 2);
@SneakyThrows
private User getUserById(Integer id) {
TimeUnit.SECONDS.sleep(1);
return users.stream().filter(u -> Objects.equals(u.getId(), id)).findFirst().get();
}
2.2.1 手動加載
@Test
public void manual() {
Cache<Integer, User> cache = Caffeine
.newBuilder()
// 元素寫入10分鐘后過期
.expireAfterWrite(10, TimeUnit.MINUTES)
// 最大能放1w個元素
.maximumSize(10_000)
.build();
// 查找一個緩存元素, 沒有查找到的時候返回null
User user = cache.getIfPresent(userKey);
// 如果緩存不存在則執行 mappingFunction 生成緩存元素返回, 并將元素put進cache
// 類似于map的 computeIfAbsent方法
user = cache.get(userKey, k -> getUserById(userKey));
// 添加或者更新一個緩存元素
cache.put(userKey, getUserById(userKey));
// 移除一個緩存元素
cache.invalidate(userKey);
}
推薦使用
cache.get(key, k -> value)操作來在緩存中不存在該key對應的緩存元素的時候進行計算生成并直接寫入至緩存內,而當該key對應的緩存元素存在的時候將會直接返回存在的緩存值。
2.2.2 自動加載
@Test
public void loading() {
LoadingCache<Integer, User> cache = Caffeine
.newBuilder()
.maximumSize(10_000)
.expireAfterWrite(10, TimeUnit.MINUTES)
// 設置自動加載的function
.build(this::getUserById);
// 查找緩存,如果緩存不存在則自動調用getUserById生成緩存元素, 如果無法生成則返回null
User user = cache.get(userKey);
// 批量查找緩存,如果緩存不存在則生成緩存元素
Map<Integer, User> users = cache.getAll(userKeys);
}
LoadingCache是一個Cache附加上CacheLoader能力之后的緩存實現。
通過
getAll可以達到批量查找緩存的目的。 默認情況下,在getAll方法中,將會對每個不存在對應緩存的key調用一次CacheLoader.load來生成緩存元素。 在批量檢索比單個查找更有效率的場景下,你可以覆蓋并開發CacheLoader.loadAll方法來使你的緩存更有效率。
2.2.3 異步手動加載
@Test
@SneakyThrows
public void asyncManual() {
AsyncCache<Integer, User> cache = Caffeine
.newBuilder()
.expireAfterWrite(10, TimeUnit.MINUTES)
.maximumSize(10_000)
// 構建異步對象
.buildAsync();
// 查找一個緩存元素, 沒有查找到的時候返回null
CompletableFuture<User> user = cache.getIfPresent(userKey);
// 查找緩存元素,如果不存在,則異步生成
user = cache.get(userKey, k -> getUserById(userKey));
// 添加或者更新一個緩存元素
cache.put(userKey, user);
// 移除一個緩存元素
cache.synchronous().invalidate(userKey);
}
AsyncCache是Cache的一個變體,AsyncCache提供了在Executor上生成緩存元素并返回CompletableFuture的能力。這給出了在當前流行的響應式編程模型中利用緩存的能力。
synchronous()方法給Cache提供了阻塞直到異步緩存生成完畢的能力。當然,也可以使用
AsyncCache.asMap()所暴露出來的ConcurrentMap的方法對緩存進行操作。默認的線程池實現是
ForkJoinPool.commonPool(),當然你也可以通過覆蓋并實現Caffeine.executor(Executor)方法來自定義你的線程池選擇。
2.2.4 異步自動加載
@Test
@SneakyThrows
public void asyncLoading() {
AsyncLoadingCache<Integer, User> cache = Caffeine
.newBuilder()
.maximumSize(10_000)
.expireAfterWrite(10, TimeUnit.MINUTES)
// 設置自動加載的function
.buildAsync(key -> getUserById(key));
// 也可以指定加載時使用緩存對象的executor
//.buildAsync((key, executor) -> getUserById(key, executor));
// 查找緩存元素,如果其不存在,將會異步進行生成
CompletableFuture<User> user = cache.get(userKey);
// 批量查找緩存元素,如果其不存在,將會異步進行生成
CompletableFuture<Map<Integer, User>> users = cache.getAll(userKeys);
}
AsyncLoadingCache是一個AsyncCache加上AsyncCacheLoader能力的實現。
2.3 緩存驅逐
Caffeine 提供了三種驅逐策略,分別是基于容量,基于時間和基于引用三種類型。
在默認情況下,當一個緩存元素過期的時候,Caffeine不會自動立即將其清理和驅逐。而它將會在寫操作之后進行少量的維護工作,在寫操作較少的情況下,也偶爾會在讀操作之后進行。如果你的緩存吞吐量較高,那么你不用去擔心你的緩存的過期維護問題。
2.3.1 基于容量
@Test
@SneakyThrows
public void evictionWithSize() {
// 基于緩存內的元素個數進行驅逐
LoadingCache<Integer, User> cacheWithSize = Caffeine.newBuilder()
.maximumSize(1)
.build(key -> getUserById(key));
// 基于緩存內元素權重進行驅逐
LoadingCache<Integer, User> cacheWitWeight = Caffeine.newBuilder()
.maximumWeight(50)
// 權重必須大于0
.weigher((Integer key, User user) -> Math.abs(user.hashCode() % 100))
.build(key -> getUserById(key));
for (User user : users) {
cacheWithSize.put(user.getId(), user);
cacheWitWeight.put(user.getId(), user);
}
//因為是異步驅逐的 所以需要睡眠一下
TimeUnit.SECONDS.sleep(1);
log.info("cacheWithSize size:{}, element: {}", cacheWithSize.asMap().size(), cacheWithSize.asMap());
// cacheWithSize size:1, element: {3=User(id=3, name=wangwu)}
log.info("cacheWitWeight size:{} element: {}", cacheWitWeight.asMap().size(), cacheWitWeight.asMap());
// cacheWitWeight size:2 element: {2=User(id=2, name=lisi), 3=User(id=3, name=wangwu)}
}
如果你的緩存容量不希望超過某個特定的大小,那么記得使用
Caffeine.maximumSize(long)。緩存將會嘗試通過基于就近度和頻率的算法來驅逐掉不會再被使用到的元素。另一種情況,你的緩存可能中的元素可能存在不同的“權重”--打個比方,你的緩存中的元素可能有不同的內存占用--你也許需要借助
Caffeine.weigher(Weigher)方法來界定每個元素的權重并通過Caffeine.maximumWeight(long)方法來界定緩存中元素的總權重來實現上述的場景。除了“最大容量”所需要的注意事項,在基于權重驅逐的策略下,一個緩存元素的權重計算是在其創建和更新時,此后其權重值都是靜態存在的,在兩個元素之間進行權重的比較的時候,并不會根據進行相對權重的比較。
2.3.2 基于時間
@Test
public void evictionWithTime() {
// 基于固定的過期時間驅逐策略 - 訪問多久后過期
LoadingCache<Integer, User> cacheWithAccessTime = Caffeine.newBuilder()
.expireAfterAccess(5, TimeUnit.MINUTES)
.build(key -> getUserById(key));
// 基于固定的過期時間驅逐策略 - 寫入多久后過期
LoadingCache<Integer, User> cacheWithWriteTime = Caffeine.newBuilder()
.expireAfterWrite(10, TimeUnit.MINUTES)
.build(key -> getUserById(key));
// 基于不同的過期驅逐策略
LoadingCache<Integer, User> cacheWithDynamicTime = Caffeine.newBuilder()
.expireAfter(new Expiry<Integer, User>() {
// 創建多久后過期
public long expireAfterCreate(Integer key, User user, long currentTime) {
// 給一個60-120秒的隨機時間
return TimeUnit.SECONDS.toNanos(RandomUtils.nextInt(60, 120));
}
// 更新多久后過期
public long expireAfterUpdate(Integer key, User graph,
long currentTime, long currentDuration) {
return currentDuration;
}
// 訪問多久后過期
public long expireAfterRead(Integer key, User graph,
long currentTime, long currentDuration) {
return currentDuration;
}
})
.build(key -> getUserById(key));
}
Caffeine提供了三種方法進行基于時間的驅逐:
expireAfterAccess(long, TimeUnit):一個元素在上一次讀寫操作后一段時間之后,在指定的時間后沒有被再次訪問將會被認定為過期項。expireAfterWrite(long, TimeUnit):一個元素將會在其創建或者最近一次被更新之后的一段時間后被認定為過期項。expireAfter(Expiry):一個元素將會在指定的時間后被認定為過期項。在寫操作,和偶爾的讀操作中將會進行周期性的過期事件的執行。過期事件的調度和觸發將會在O(1)的時間復雜度內完成。
2.3.3 基于引用
@Test
public void evictionWithReference() {
// 當key和緩存元素都不再存在其他強引用的時候驅逐
LoadingCache<Integer, User> cacheWithWeak = Caffeine.newBuilder()
.weakKeys()
.weakValues()
.build(key -> getUserById(key));
// 當進行GC的時候進行驅逐
LoadingCache<Integer, User> cacheWithSoft = Caffeine.newBuilder()
.softValues()
.build(key -> getUserById(key));
}
Caffeine 允許你配置你的緩存去讓GC去幫助清理緩存當中的元素,其中key支持弱引用,而value則支持弱引用和軟引用。記住
AsyncCache不支持軟引用和弱引用。
Caffeine.weakKeys()在保存key的時候將會進行弱引用。這允許在GC的過程中,當key沒有被任何強引用指向的時候去將緩存元素回收。由于GC只依賴于引用相等性。這導致在這個情況下,緩存將會通過引用相等(==)而不是對象相等equals()去進行key之間的比較。
Caffeine.weakValues()在保存value的時候將會使用弱引用。這允許在GC的過程中,當value沒有被任何強引用指向的時候去將緩存元素回收。由于GC只依賴于引用相等性。這導致在這個情況下,緩存將會通過引用相等(==)而不是對象相等equals()去進行value之間的比較。
Caffeine.softValues()在保存value的時候將會使用軟引用。為了相應內存的需要,在GC過程中被軟引用的對象將會被通過LRU算法回收。由于使用軟引用可能會影響整體性能,我們還是建議通過使用基于緩存容量的驅逐策略代替軟引用的使用。同樣的,使用softValues()將會通過引用相等(==)而不是對象相等equals()去進行value之間的比較。
2.4 刪除緩存
術語:
- 驅逐(eviction) 緩存元素因為策略被移除(如2.3章節)
- 失效(invalidation) 緩存元素被手動移除
- 移除(removal) 由于驅逐或者失效而最終導致的結果
@Test
@SneakyThrows
public void removeOrEvictionRecord() {
Cache<Integer, User> cache = Caffeine.newBuilder()
.expireAfterWrite(5, TimeUnit.SECONDS)
.evictionListener((Integer key, User user, RemovalCause cause) ->
log.info("Key {} was evicted ({})", key, cause))
.removalListener((Integer key, User user, RemovalCause cause) ->
log.info("Key {} was removed ({})", key, cause))
.build();
for (User user : users) {
cache.put(user.getId(), user);
}
log.info("cache data put success");
TimeUnit.SECONDS.sleep(10);
// 失效key
cache.invalidate(userKey);
// 批量失效key
cache.invalidateAll(userKeys);
// 失效所有的key
cache.invalidateAll();
}
你可以為你的緩存通過
Caffeine.removalListener(RemovalListener)方法定義一個移除監聽器在一個元素被移除的時候進行相應的操作。這些操作是使用Executor異步執行的,其中默認的 Executor 實現是ForkJoinPool.commonPool()并且可以通過覆蓋Caffeine.executor(Executor)方法自定義線程池的實現。當移除之后的自定義操作必須要同步執行的時候,你需要使用
Caffeine.evictionListener(RemovalListener)。這個監聽器將在RemovalCause.wasEvicted()為 true 的時候被觸發。為了移除操作能夠明確生效,Cache.asMap()提供了方法來執行原子操作。記住任何在
RemovalListener中被拋出的異常將會被吞食。
2.5 刷新緩存
@Test
@SneakyThrows
public void refreshAfterWrite() {
// 同時使用 expireAfterWrite refreshAfterWrite
// 使一個元素在其被允許刷新但是沒有被主動查詢的時候,這個元素也會被視為過期(防止不活躍的數據常駐內存)
LoadingCache<Integer, User> cache = Caffeine.newBuilder()
.maximumSize(10_000)
.expireAfterWrite(Duration.ofSeconds(10))
/*
* 1. 寫入到達指定時間后刷新
* 2. 不是到達時間直接刷新 而是標記為準備刷新 數據下次訪問的時候才開始刷新
* 3. 在刷新的時候如果查詢緩存元素,那么直接返回舊值
*/
.refreshAfterWrite(Duration.ofSeconds(3))
.build(key -> getUserById(key));
}
刷新和驅逐并不相同。可以通過
LoadingCache.refresh(K)方法,異步為key對應的緩存元素刷新一個新的值。與驅逐不同的是,在刷新的時候如果查詢緩存元素,其舊值將仍被返回,直到該元素的刷新完畢后結束后才會返回刷新后的新值。與
expireAfterWrite相反,refreshAfterWrite將會使在寫操作之后的一段時間后允許key對應的緩存元素進行刷新,但是只有在這個key被真正查詢到的時候才會正式進行刷新操作。所以打個比方,你可以在同一個緩存中同時用到refreshAfterWrite和expireAfterWrite,這樣緩存元素在被允許刷新的時候不會直接刷新使得過期時間被盲目重置。當一個元素在其被允許刷新但是沒有被主動查詢的時候,這個元素也會被視為過期。
2.6 統計
@Test
public void statistics() {
Cache<Integer, User> cache = Caffeine.newBuilder()
.maximumSize(10_000)
.recordStats()
.build();
// hitRate 查詢緩存的命中率
// evictionCount 被驅逐的緩存數量
// averageLoadPenalty 新值被載入的平均耗時
log.info("cache stats:{}", cache.stats());
// cache stats:CacheStats{hitCount=0, missCount=0, loadSuccessCount=0, loadFailureCount=0, totalLoadTime=0, evictionCount=0, evictionWeight=0}
}
3. 集成SpringBoot
集成 SpringBoot 有兩種方式
-
將cache對象聲明稱
SpringBean然后使用的時候注入進來直接操作 -
結合
Spring的CacheManager將caffeine注冊到cache模塊中,然后使用spring注解進行緩存操作cache 方面的注解主要有以下 5 個:
@Cacheable【創建、查詢緩存】:觸發緩存入口(一般放在創建和獲取的方法上,@Cacheable 注解會先查詢是否已經有緩存。如果有,則直接從緩存中返回;如果沒有,則會執行方法并返回結果緩存【返回方法返回 NULL,則不進行緩存】)
@CachePut【更新緩存】:更新緩存且不影響方法執行(用于修改的方法上,該注解下的方法始終會被執行)
@CacheEvict【刪除緩存】:觸發緩存的 eviction(用于刪除的方法上)
@Caching【組合緩存配置】:將多個緩存組合在一個方法上(該注解可以允許一個方法同時設置多個注解)
@CacheConfig【類級別共享配置】:在類級別設置一些緩存相關的共同配置(與其它緩存配合使用),避免在每個緩存方法上重復配置相同的緩存屬性

浙公網安備 33010602011771號