Java 緩存框架 Caffeine 應(yīng)用場景解析
一、Caffeine 簡介
1. 框架概述
Caffeine是由Google工程師Ben Manes開發(fā)的一款Java本地緩存框架,其初始版本發(fā)布于2014年。該框架的設(shè)計靈感來源于Guava Cache,但在性能和功能方面進(jìn)行了革命性的優(yōu)化。Caffeine基于"W-TinyLFU"(Window-Tiny Least Frequently Used)算法實(shí)現(xiàn),這是一種改進(jìn)的LFU緩存淘汰算法,結(jié)合了LFU的高命中率優(yōu)勢和LRU的時效性特點(diǎn)。
1.1 Caffeine的核心優(yōu)勢
1.1.1 超高性能
Caffeine在性能方面實(shí)現(xiàn)了質(zhì)的飛躍:
- 基準(zhǔn)測試顯示,相比Guava Cache,Caffeine的讀性能提升約8-12倍,寫性能提升約5-10倍
- 支持每秒數(shù)百萬次(典型值300-500萬QPS)的緩存操作
- 采用無鎖并發(fā)設(shè)計,大幅減少線程競爭(如使用并發(fā)哈希表和非阻塞隊列)
- 對JVM的內(nèi)存模型進(jìn)行了深度優(yōu)化,減少緩存行偽共享問題
1.1.2 靈活的過期策略
Caffeine提供三種核心過期策略:
- 寫入后過期:通過
expireAfterWrite設(shè)置,例如:Caffeine.newBuilder().expireAfterWrite(10, TimeUnit.MINUTES).build();
- 訪問后過期:通過
expireAfterAccess設(shè)置,適合熱點(diǎn)數(shù)據(jù)場景 - 自定義過期:通過
expireAfter方法實(shí)現(xiàn)基于業(yè)務(wù)邏輯的復(fù)雜過期判斷
1.1.3 異步支持
Caffeine提供完整的異步緩存(AsyncCache)支持:
- 異步加載機(jī)制:通過
AsyncLoadingCache實(shí)現(xiàn)非阻塞式數(shù)據(jù)加載 - 支持CompletableFuture:可與Java8的異步編程模型完美結(jié)合
- 典型應(yīng)用場景:高并發(fā)環(huán)境下的微服務(wù)接口緩存
1.1.4 豐富的監(jiān)聽器
Caffeine提供完善的監(jiān)控支持:
- 移除監(jiān)聽器(
RemovalListener):可監(jiān)聽緩存項的驅(qū)逐、失效或手動移除 - 統(tǒng)計功能:通過
recordStats()開啟命中率統(tǒng)計 - 典型配置:
cache.recordStats(); CacheStats stats = cache.stats(); double hitRate = stats.hitRate();
1.1.5 內(nèi)存安全
Caffeine提供多種內(nèi)存保護(hù)機(jī)制:
- 基于容量:通過
maximumSize限制緩存項數(shù)量 - 基于時間:通過上述過期策略控制
- 基于引用:支持弱引用鍵/值(
weakKeys/weakValues)和軟引用值(softValues) - 權(quán)重控制:通過
weigher和maximumWeight實(shí)現(xiàn)基于對象大小的精確控制
典型內(nèi)存安全配置示例:
Caffeine.newBuilder()
.maximumSize(10_000)
.weigher((String key, String value) -> value.length())
.maximumWeight(50_000_000) // ~50MB
.build();二、Caffeine 基礎(chǔ)
在使用 Caffeine 前,需先引入依賴,并了解其核心組件的作用。
2.1 依賴引入(Maven/Gradle)
Caffeine 的最新版本可在 Maven 中央倉庫查詢(https://mvnrepository.com/artifact/com.github.ben-manes.caffeine/caffeine)
Maven 配置示例(含注釋說明)
<!-- Caffeine核心依賴(必選) -->
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
<version>3.2.7</version> <!-- 截至2024年1月最新穩(wěn)定版 -->
<!-- 建議通過dependencyManagement統(tǒng)一管理版本 -->
</dependency>
<!-- 異步支持依賴(可選) -->
<!-- 當(dāng)需要配合Java11+的HttpClient實(shí)現(xiàn)異步緩存加載時添加 -->
<dependency>
<groupId>java.net.http</groupId>
<artifactId>http-client</artifactId>
<version>11.0.1</version> <!-- 最低要求JDK11 -->
<scope>runtime</scope> <!-- 通常只需運(yùn)行時依賴 -->
</dependency>Gradle 配置示例(Kotlin DSL)
dependencies {
// 核心實(shí)現(xiàn)(必選)
implementation("com.github.ben-manes.caffeine:caffeine:3.2.7")
// 異步支持(可選)
runtimeOnly("java.net.http:http-client:11.0.1") {
because("For async cache loading with HTTP requests")
}
}2.2 核心組件解析
Caffeine 的核心組件采用分層設(shè)計,主要分為基礎(chǔ)緩存接口和功能擴(kuò)展接口兩大類:
1.基礎(chǔ)緩存接口層次結(jié)構(gòu)
Cache (基本功能)
├── LoadingCache (同步加載)
└── AsyncCache (異步基礎(chǔ))
└── AsyncLoadingCache (異步加載)2.關(guān)鍵組件詳細(xì)說明(含典型應(yīng)用場景)
| 組件 | 作用說明 | 典型使用場景 | 示例代碼片段 |
|---|---|---|---|
Cache<K,V> | 手動管理緩存,需顯式處理緩存未命中 | 簡單緩存場景,數(shù)據(jù)源訪問成本較低 | cache.get(key, k -> fetchFromDB(k)) |
LoadingCache<K,V> | 自動加載緩存,內(nèi)置CacheLoader | 高頻訪問且加載邏輯固定的場景 | LoadingCache.from(this::loadFromAPI) |
AsyncCache<K,V> | 返回CompletableFuture的異步接口 | 配合非阻塞IO或遠(yuǎn)程調(diào)用 | cache.get(key).thenAccept(value -> ...) |
AsyncLoadingCache<K,V> | 異步自動加載緩存 | 微服務(wù)間數(shù)據(jù)緩存 | AsyncLoadingCache.from(this::asyncLoad) |
CacheLoader<K,V> | 定義加載邏輯的函數(shù)式接口 | 統(tǒng)一數(shù)據(jù)加載策略 | new CacheLoader<>() { @Override public V load(K key)... } |
RemovalListener<K,V> | 移除事件監(jiān)聽器 | 緩存一致性維護(hù)、監(jiān)控統(tǒng)計 | listener((key,value,reason) -> logRemoval()) |
Expiry<K,V> | 細(xì)粒度過期控制 | 動態(tài)TTL場景(如會話緩存) | expireAfter((key,value,currentTime) -> customTTL) |
3.高級特性支持
- 權(quán)重計算:通過
weigher接口實(shí)現(xiàn)基于緩存對象大小的淘汰策略 - 刷新機(jī)制:
refreshAfterWrite配合CacheLoader.reload實(shí)現(xiàn)后臺刷新 - 統(tǒng)計監(jiān)控:
recordStats()啟用命中率等統(tǒng)計指標(biāo) - 線程模型:默認(rèn)使用ForkJoinPool.commonPool(),可通過
executor自定義
4.最佳實(shí)踐提示:
- 對于長時間加載操作,優(yōu)先選擇AsyncLoadingCache避免阻塞
- 移除監(jiān)聽器不要執(zhí)行耗時操作,否則會影響緩存性能
- 在Spring環(huán)境中建議通過@Bean配置全局緩存管理器
- 生產(chǎn)環(huán)境務(wù)必啟用統(tǒng)計功能(recordStats)進(jìn)行監(jiān)控
三、Caffeine 核心用法
Caffeine 的使用流程遵循 "構(gòu)建器模式配置 → 創(chuàng)建緩存實(shí)例 → 讀寫緩存" 的邏輯,下面分場景講解具體用法。
3.1 基礎(chǔ)緩存(Cache):手動控制讀寫
Cache 是最基礎(chǔ)的緩存類型,需手動處理緩存未命中(未命中時返回 null),適合緩存邏輯簡單的場景。
3.1.1 創(chuàng)建 Cache 實(shí)例
通過 Caffeine.newBuilder() 配置緩存規(guī)則,常見配置包括:
- 容量控制:
maximumSize(long):設(shè)置緩存最大容量(條目數(shù)),超過后按 W-TinyLFU 算法淘汰。maximumWeight(long)+weigher(Weigher):基于權(quán)重控制緩存大小,適合不同條目占用不同內(nèi)存的場景。
- 過期策略:
expireAfterWrite(Duration):寫入后過期(如 10 分鐘未更新則過期),適合數(shù)據(jù)變更頻繁的場景。expireAfterAccess(Duration):訪問后過期(如 5 分鐘未訪問則過期),適合熱點(diǎn)數(shù)據(jù)緩存。expireAfter(Expiry):自定義過期時間計算邏輯,可實(shí)現(xiàn)基于業(yè)務(wù)規(guī)則的過期。
- 監(jiān)聽器:
removalListener(RemovalListener):設(shè)置緩存移除監(jiān)聽器,可記錄日志或觸發(fā)后續(xù)操作。
- 其他特性:
weakKeys()/weakValues():使用弱引用,允許被垃圾回收。softValues():使用軟引用,在內(nèi)存不足時被回收。recordStats():啟用統(tǒng)計信息收集。
import com.github.ben-manes.caffeine.cache.Caffeine;
import com.github.ben-manes.caffeine.cache.Cache;
import java.util.concurrent.TimeUnit;
public class CaffeineBasicDemo {
public static void main(String[] args) {
// 1. 配置并創(chuàng)建Cache實(shí)例(帶詳細(xì)注釋)
Cache<String, String> userCache = Caffeine.newBuilder()
.maximumSize(1000) // 最大容量1000條
.expireAfterWrite(10, TimeUnit.MINUTES) // 寫入后10分鐘過期
.expireAfterAccess(5, TimeUnit.MINUTES) // 訪問后5分鐘過期(優(yōu)先級低于expireAfterWrite)
.removalListener((key, value, cause) -> { // 緩存移除監(jiān)聽器
System.out.printf("緩存移除:key=%s, value=%s, 原因=%s%n",
key, value, cause.toString());
// 原因可能是:EXPLICIT(手動刪除)、REPLACED(值被替換)、
// COLLECTED(垃圾回收)、EXPIRED(過期)、SIZE(超過容量限制)
})
.recordStats() // 啟用統(tǒng)計
.build(); // 構(gòu)建Cache實(shí)例
// 2. 寫入緩存(多種方式)
userCache.put("user:1001", "張三"); // 常規(guī)put
userCache.asMap().putIfAbsent("user:1002", "李四"); // 線程安全寫入
// 3. 讀取緩存(未命中返回null)
String user1 = userCache.getIfPresent("user:1001");
System.out.println("讀取user:1001:" + user1); // 輸出:張三
// 4. 讀取并計算(未命中時執(zhí)行函數(shù)邏輯,但不自動存入緩存)
String user3 = userCache.get("user:1003", key -> {
// 模擬從數(shù)據(jù)庫查詢數(shù)據(jù)(僅當(dāng)緩存未命中時執(zhí)行)
System.out.println("緩存未命中,查詢DB:" + key);
return "王五"; // 此結(jié)果不會自動存入緩存
});
System.out.println("讀取user:1003:" + user3); // 輸出:王五
// 5. 緩存維護(hù)操作
userCache.invalidate("user:1002"); // 單個刪除
userCache.invalidateAll(List.of("user:1001", "user:1003")); // 批量刪除
userCache.cleanUp(); // 手動觸發(fā)清理過期條目
userCache.invalidateAll(); // 清空所有緩存
// 6. 查看統(tǒng)計信息(需先啟用recordStats)
System.out.println("命中率:" + userCache.stats().hitRate());
}
}3.1.2 應(yīng)用場景示例
- 簡單KV緩存:
- 緩存用戶Session信息
- 緩存系統(tǒng)配置項
- 臨時數(shù)據(jù)存儲(如驗(yàn)證碼)
- 配合Spring Cache:
@Configuration
@EnableCaching
public class CacheConfig {
@Bean
public CacheManager cacheManager() {
CaffeineCacheManager manager = new CaffeineCacheManager();
manager.setCaffeine(Caffeine.newBuilder()
.maximumSize(1000)
.expireAfterWrite(10, TimeUnit.MINUTES));
return manager;
}
}多級緩存:
// 作為本地緩存與Redis組成二級緩存
public class MultiLevelCache {
private final Cache<String, Object> localCache;
private final RedisTemplate<String, Object> redisTemplate;
public Object get(String key) {
Object value = localCache.getIfPresent(key);
if (value == null) {
value = redisTemplate.opsForValue().get(key);
if (value != null) {
localCache.put(key, value);
}
}
return value;
}
}3.2 加載緩存(LoadingCache):自動加載未命中數(shù)據(jù)
LoadingCache 是 Cache 的子類,通過實(shí)現(xiàn) CacheLoader 接口,實(shí)現(xiàn) "緩存未命中時自動加載數(shù)據(jù)并存入緩存",適合緩存數(shù)據(jù)需從數(shù)據(jù)源(如 DB、Redis)加載的場景。
3.2.1 創(chuàng)建 LoadingCache 實(shí)例
import com.github.ben-manes.caffeine.cache.Caffeine;
import com.github.ben-manes.caffeine.cache.LoadingCache;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.List;
public class CaffeineLoadingDemo {
public static void main(String[] args) throws ExecutionException {
// 1. 實(shí)現(xiàn)CacheLoader:定義緩存未命中時的加載邏輯
LoadingCache<String, String> productCache = Caffeine.newBuilder()
.maximumSize(500)
.expireAfterWrite(30, TimeUnit.MINUTES)
.refreshAfterWrite(10, TimeUnit.MINUTES) // 10分鐘后刷新(不阻塞讀?。?
.recordStats()
.build(new CacheLoader<String, String>() {
@Override
public String load(String key) throws Exception {
// 模擬從數(shù)據(jù)庫加載數(shù)據(jù)(緩存未命中時自動執(zhí)行)
System.out.println("緩存未命中,從DB加載:" + key);
if (key.startsWith("prod:")) {
return "商品-" + key.substring(5); // 如key=prod:101 → 商品-101
}
throw new IllegalArgumentException("Invalid key format");
}
// 可選:實(shí)現(xiàn)批量加載(提升getAll性能)
@Override
public Map<String, String> loadAll(Iterable<? extends String> keys) {
System.out.println("批量加載keys:" + keys);
// 實(shí)際應(yīng)從DB批量查詢
Map<String, String> result = new HashMap<>();
for (String key : keys) {
result.put(key, "商品-" + key.substring(5));
}
return result;
}
});
// 2. 讀取緩存(未命中時自動調(diào)用load()加載并存入緩存)
String product1 = productCache.get("prod:101"); // 首次:加載并返回
System.out.println("讀取prod:101:" + product1); // 輸出:商品-101
// 3. 批量讀?。╣etAll())
Map<String, String> products = productCache.getAll(List.of("prod:102", "prod:103"));
System.out.println("批量讀取結(jié)果:" + products);
// 4. 主動刷新(異步)
productCache.refresh("prod:101"); // 后臺刷新,舊值仍可用
// 5. 統(tǒng)計信息
System.out.println("加載次數(shù):" + productCache.stats().loadCount());
}
}3.2.2 關(guān)鍵特性:刷新(Refresh)與過期(Expire)的區(qū)別
| 特性 | 刷新(Refresh) | 過期(Expire) |
|---|---|---|
| 觸發(fā)時機(jī) | 刷新時間到后 | 過期時間到后 |
| 讀取行為 | 異步刷新,立即返回舊值 | 同步重新加載,可能阻塞請求 |
| 適用場景 | 數(shù)據(jù)允許短暫不一致(如商品詳情) | 數(shù)據(jù)強(qiáng)一致要求(如訂單狀態(tài)) |
| 實(shí)現(xiàn)方式 | 需配置refreshAfterWrite | 配置expireAfterWrite/AfterAccess |
典型使用模式:
// 商品詳情緩存:10分鐘強(qiáng)制過期,5分鐘自動刷新
LoadingCache<String, Product> productCache = Caffeine.newBuilder()
.maximumSize(10000)
.expireAfterWrite(10, TimeUnit.MINUTES) // 強(qiáng)制過期時間
.refreshAfterWrite(5, TimeUnit.MINUTES) // 自動刷新時間
.build(this::loadProductFromDB);
3.3 異步緩存(AsyncCache/AsyncLoadingCache):非阻塞讀寫
在高并發(fā)場景下,同步緩存的 load() 可能會阻塞線程,而 AsyncCache 通過返回 CompletableFuture 實(shí)現(xiàn)非阻塞操作,所有 IO 操作均在異步線程池中執(zhí)行。
3.3.1 創(chuàng)建 AsyncLoadingCache 實(shí)例
import com.github.ben-manes.caffeine.cache.AsyncLoadingCache;
import com.github.ben-manes.caffeine.cache.Caffeine;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
public class CaffeineAsyncDemo {
public static void main(String[] args) throws Exception {
// 1. 自定義線程池(生產(chǎn)環(huán)境建議使用有界隊列和拒絕策略)
Executor executor = Executors.newFixedThreadPool(5, r -> {
Thread thread = new Thread(r);
thread.setName("caffeine-async-" + thread.getId());
return thread;
});
// 2. 創(chuàng)建AsyncLoadingCache實(shí)例
AsyncLoadingCache<String, String> orderCache = Caffeine.newBuilder()
.maximumSize(1000)
.expireAfterWrite(15, TimeUnit.MINUTES)
.executor(executor) // 指定異步線程池
.buildAsync(key -> {
// 模擬耗時操作(如RPC調(diào)用,耗時200ms)
TimeUnit.MILLISECONDS.sleep(200);
System.out.println(Thread.currentThread().getName() + " 加載訂單:" + key);
return "訂單-" + key.substring(6); // 如key=order:2024 → 訂單-2024
});
// 3. 異步讀取(推薦方式)
CompletableFuture<String> future = orderCache.get("order:2024");
future.thenApplyAsync(order -> {
System.out.println("處理訂單數(shù)據(jù):" + order);
return order.toUpperCase();
}, executor); // 使用相同線程池處理結(jié)果
// 4. 批量讀?。ǚ祷豈ap<Key, CompletableFuture>)
Map<String, CompletableFuture<String>> futures =
orderCache.getAll(List.of("order:2025", "order:2026"));
// 5. 同步獲?。▋H測試用,實(shí)際應(yīng)避免)
String order = orderCache.get("order:2027").get();
System.out.println("同步獲取結(jié)果:" + order);
}
}3.3.2 最佳實(shí)踐
線程池配置:
// 更完善的線程池配置
ThreadPoolExecutor executor = new ThreadPoolExecutor(
5, // 核心線程數(shù)
10, // 最大線程數(shù)
60, TimeUnit.SECONDS, // 空閑線程存活時間
new LinkedBlockingQueue<>(1000), // 有界隊列
new ThreadFactoryBuilder().setNameFormat("cache-loader-%d").build(),
new ThreadPoolExecutor.CallerRunsPolicy() // 拒絕策略
);
異常處理:
orderCache.get("badKey").exceptionally(ex -> {
System.err.println("加載失敗: " + ex.getMessage());
return "defaultValue";
});
結(jié)合Spring使用:
@Cacheable(value = "orders", cacheManager = "asyncCacheManager")
public CompletableFuture<Order> getOrderAsync(String orderId) {
return CompletableFuture.supplyAsync(() -> orderService.loadOrder(orderId));
}
性能監(jiān)控:
CacheStats stats = orderCache.synchronous().stats();
System.out.println("平均加載時間:" + stats.averageLoadPenalty() + "ns");
四、Caffeine 高級特性
4.1 緩存統(tǒng)計(Cache Statistics)
緩存統(tǒng)計功能是優(yōu)化緩存性能的重要工具。通過開啟緩存統(tǒng)計,可以實(shí)時監(jiān)控以下關(guān)鍵指標(biāo):
- 命中率(Hit Rate):反映緩存有效性,計算公式為:
命中次數(shù)/(命中次數(shù)+未命中次數(shù)) - 加載耗時(Load Penalty):統(tǒng)計從數(shù)據(jù)源加載數(shù)據(jù)的平均耗時
- 移除次數(shù)(Eviction Count):因容量或過期策略導(dǎo)致的緩存移除次數(shù)
- 加載失敗率(Load Failure Rate):數(shù)據(jù)源加載失敗的比例
典型應(yīng)用場景:
- 評估緩存配置是否合理
- 識別熱點(diǎn)數(shù)據(jù)
- 監(jiān)控緩存性能瓶頸
import com.github.ben-manes.caffeine.cache.CacheStats;
public class CaffeineStatsDemo {
public static void main(String[] args) {
LoadingCache<String, String> statsCache = Caffeine.newBuilder()
.maximumSize(100)
.recordStats() // 必須顯式開啟統(tǒng)計功能
.build(key -> {
// 模擬耗時加載
try {
Thread.sleep(100);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
return "統(tǒng)計測試:" + key;
});
// 模擬讀寫操作
statsCache.get("key1"); // 第一次加載(未命中)
statsCache.get("key1"); // 命中已有緩存
statsCache.get("key2"); // 新鍵加載
statsCache.invalidate("key1"); // 手動失效
// 獲取統(tǒng)計結(jié)果
CacheStats stats = statsCache.stats();
System.out.println("緩存命中率:" + stats.hitRate()); // 50%(1次命中/2次查詢)
System.out.println("加載成功次數(shù):" + stats.loadSuccessCount()); // 2次加載
System.out.println("移除次數(shù):" + stats.evictionCount()); // 0(未達(dá)到容量上限)
System.out.println("平均加載耗時(ns):" + stats.averageLoadPenalty()); // 約100ms
System.out.println("加載失敗率:" + stats.loadFailureRate()); // 0.0
}
}4.2 自定義過期策略(Expiry)
標(biāo)準(zhǔn)的TTL(Time-To-Live)過期策略對所有緩存條目采用統(tǒng)一設(shè)置,而自定義過期策略允許基于業(yè)務(wù)特性實(shí)現(xiàn)精細(xì)化控制。
常見應(yīng)用場景:
- 不同優(yōu)先級數(shù)據(jù)設(shè)置不同有效期(如熱點(diǎn)數(shù)據(jù)短時效,冷數(shù)據(jù)長時效)
- 讀寫操作影響過期時間(如讀操作續(xù)期)
- 動態(tài)調(diào)整過期時間(如根據(jù)數(shù)據(jù)價值計算)
import com.github.ben-manes.caffeine.cache.Caffeine;
import com.github.ben-manes.caffeine.cache.Expiry;
import com.github.ben-manes.caffeine.cache.LoadingCache;
import java.util.concurrent.TimeUnit;
class CustomExpiry implements Expiry<String, String> {
@Override
public long expireAfterCreate(String key, String value, long currentTime) {
// 創(chuàng)建時過期策略
if (key.startsWith("flash:")) { // 閃存數(shù)據(jù):30秒過期
return TimeUnit.SECONDS.toNanos(30);
} else if (key.startsWith("hot:")) { // 熱門數(shù)據(jù):5分鐘
return TimeUnit.MINUTES.toNanos(5);
} else { // 普通數(shù)據(jù):30分鐘
return TimeUnit.MINUTES.toNanos(30);
}
}
@Override
public long expireAfterUpdate(String key, String value,
long currentTime, long currentDuration) {
// 更新策略:保持原有過期時間(默認(rèn))
return currentDuration;
// 或者重置為創(chuàng)建時間:return expireAfterCreate(key, value, currentTime);
}
@Override
public long expireAfterRead(String key, String value,
long currentTime, long currentDuration) {
// 讀取時策略:熱門數(shù)據(jù)讀取后續(xù)期5分鐘
if (key.startsWith("hot:")) {
return TimeUnit.MINUTES.toNanos(5);
}
return currentDuration;
}
}
public class CaffeineCustomExpiryDemo {
public static void main(String[] args) {
LoadingCache<String, String> customExpiryCache = Caffeine.newBuilder()
.expireAfter(new CustomExpiry())
.build(key -> "自定義過期:" + key);
customExpiryCache.get("flash:news:2023"); // 30秒過期
customExpiryCache.get("hot:product:101"); // 5分鐘且讀取續(xù)期
customExpiryCache.get("normal:user:201"); // 30分鐘過期
}
}4.3 弱引用與軟引用:避免內(nèi)存溢出
Java引用類型與緩存回收策略:
| 引用類型 | GC行為 | 適用場景 | Caffeine配置 |
|---|---|---|---|
| 強(qiáng)引用 | 永不回收 | 默認(rèn)方式 | - |
| 軟引用 | 內(nèi)存不足時回收 | 緩存大對象 | .softValues() |
| 弱引用 | 下次GC時回收 | 臨時性緩存 | .weakKeys()/.weakValues() |
注意事項:
- 使用
weakKeys()時,key比較基于==而非equals() softValues()可能導(dǎo)致GC壓力增大- 引用回收與顯式失效策略共同作用
// 弱引用Key+Value的緩存(適合臨時性數(shù)據(jù))
Cache<String, byte[]> weakCache = Caffeine.newBuilder()
.weakKeys() // Key無強(qiáng)引用時回收
.weakValues() // Value無強(qiáng)引用時回收
.maximumSize(10_000) // 仍保持容量限制
.build();
// 軟引用Value的緩存(適合大對象)
Cache<String, byte[]> softCache = Caffeine.newBuilder()
.softValues() // 內(nèi)存不足時回收Value
.expireAfterWrite(1, TimeUnit.HOURS) // 配合顯式過期
.build();
// 典型使用場景
void processLargeData(String dataId) {
byte[] data = softCache.get(dataId, id -> {
// 從數(shù)據(jù)庫加載大對象(如圖片、文件等)
return loadLargeDataFromDB(id);
});
// 使用數(shù)據(jù)...
}五、Caffeine 注意事項
在實(shí)際開發(fā)中,若使用不當(dāng),Caffeine 可能出現(xiàn)緩存穿透、內(nèi)存溢出、線程阻塞等問題,以下是核心注意事項:
5.1 區(qū)分 "刷新(Refresh)" 與 "過期(Expire)"
刷新(refreshAfterWrite):
- 工作機(jī)制:當(dāng)緩存條目超過指定時間未被寫入時,下次讀取會觸發(fā)異步刷新,但在此期間仍會返回舊值
- 適用場景:對數(shù)據(jù)一致性要求不高,可接受短暫延遲的場景
- 商品詳情頁的評論數(shù)統(tǒng)計
- 新聞資訊的閱讀量統(tǒng)計
- 排行榜數(shù)據(jù)的更新
過期(expireAfterWrite/expireAfterAccess):
- expireAfterWrite:從寫入開始計時
- expireAfterAccess:從最后一次訪問開始計時
- 工作機(jī)制:過期后緩存條目立即失效,讀取時會同步阻塞直到重新加載完成
- 適用場景:對數(shù)據(jù)一致性要求高的核心業(yè)務(wù)
- 用戶賬戶余額
- 訂單支付狀態(tài)
- 庫存數(shù)量
?? 典型誤用場景:
將用戶余額這類強(qiáng)一致性數(shù)據(jù)配置為refreshAfterWrite(5s).可能導(dǎo)致:
- 用戶A看到余額100元
- 用戶B完成扣款50元
- 5秒內(nèi)用戶A仍看到100元(舊值)
- 直到下次讀取才刷新為50元
5.2 避免緩存穿透:空值緩存與布隆過濾器
緩存穿透的典型特征:
- 查詢一個必然不存在的數(shù)據(jù)(如不存在的用戶ID)
- 每次請求都穿透到數(shù)據(jù)庫
- 可能被惡意攻擊者利用,造成數(shù)據(jù)庫壓力
解決方案1:空值緩存
LoadingCache<String, String> cache = Caffeine.newBuilder()
.expireAfterWrite(1, TimeUnit.MINUTES) // 空值緩存1分鐘
.build(key -> {
String value = queryFromDB(key);
// 特殊空值標(biāo)記,避免與真實(shí)空值混淆
return value != null ? value : "NULL_VALUE";
});
解決方案2:布隆過濾器(適合千萬級key場景)
// 初始化布隆過濾器
BloomFilter<String> bloomFilter = BloomFilter.create(
Funnels.stringFunnel(Charset.defaultCharset()),
1000000, // 預(yù)期元素數(shù)量
0.01 // 誤判率
);
// 查詢流程
if (!bloomFilter.mightContain(key)) {
return null; // 肯定不存在
} else {
return cache.get(key); // 可能存在
}5.3 緩存鍵(Key)必須重寫 hashCode() 和 equals()
常見錯誤案例:
class CompositeKey {
private Long id;
private String category;
// 缺少hashCode/equals實(shí)現(xiàn)
}
// 實(shí)際使用中
CompositeKey key1 = new CompositeKey(1L, "A");
CompositeKey key2 = new CompositeKey(1L, "A");
cache.put(key1, "value");
// 將返回null,因?yàn)閗ey2被視為不同key
cache.getIfPresent(key2); 正確實(shí)現(xiàn)要點(diǎn):
- 使用Objects工具類自動生成
- 保證不可變(final字段)
- 實(shí)現(xiàn)Serializable接口(分布式緩存需要)
class CompositeKey implements Serializable {
private final Long id;
private final String category;
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof CompositeKey)) return false;
CompositeKey that = (CompositeKey) o;
return Objects.equals(id, that.id) &&
Objects.equals(category, that.category);
}
@Override
public int hashCode() {
return Objects.hash(id, category);
}
}5.4 異步緩存(AsyncCache)的線程池選擇
默認(rèn)線程池的問題:
- ForkJoinPool.commonPool()是JVM全局共享的
- 可能被CompletableFuture等其他組件占用
- 在容器環(huán)境中可能線程數(shù)不足
推薦配置:
ExecutorService executor = Executors.newFixedThreadPool(
Runtime.getRuntime().availableProcessors() * 2,
new ThreadFactoryBuilder()
.setNameFormat("caffeine-loader-%d")
.setDaemon(true)
.build()
);
AsyncLoadingCache<String, String> cache = Caffeine.newBuilder()
.executor(executor) // 指定專屬線程池
.buildAsync(key -> loadExpensiveValue(key));5.5 避免內(nèi)存溢出:合理配置容量與過期時間
典型配置示例:
Caffeine.newBuilder()
.maximumSize(10_000) // 基于條目數(shù)限制
.expireAfterWrite(30, TimeUnit.MINUTES) // 寫入后30分鐘過期
.expireAfterAccess(10, TimeUnit.MINUTES) // 10分鐘無訪問過期
.weigher((String key, String value) -> value.length()) // 按value大小計算權(quán)重
.maximumWeight(50_000_000) // 約50MB內(nèi)存限制
監(jiān)控建議:
- 通過cache.stats()獲取命中率
- 使用JMX監(jiān)控緩存大小
- 設(shè)置告警閾值(如內(nèi)存使用>80%)
5.6 CacheLoader 的異常處理
完整異常處理方案:
LoadingCache<String, String> cache = Caffeine.newBuilder()
.build(new CacheLoader<String, String>() {
@Override
public String load(String key) {
try {
return queryDB(key);
} catch (SQLException e) {
// 記錄詳細(xì)日志
log.error("DB查詢失敗, key: {}", key, e);
// 返回降級值
return "DEFAULT_VALUE";
// 或者拋出特定異常
// throw new CacheLoadException(e);
}
}
});
// 使用時的異常處理
try {
return cache.get(key);
} catch (CacheLoaderException e) {
// 處理加載失敗
return processFallback(key);
} catch (Exception e) {
// 兜底處理
return "SYSTEM_ERROR";
}六、常見問題
Q1:Caffeine 與 Guava Cache 的詳細(xì)區(qū)別
性能比較
Caffeine 采用了創(chuàng)新的 W-TinyLFU 緩存淘汰算法,該算法結(jié)合了 TinyLFU 和 LRU 的優(yōu)勢:
- 使用 Count-Min Sketch 數(shù)據(jù)結(jié)構(gòu)高效統(tǒng)計訪問頻率
- 適應(yīng)不同工作負(fù)載模式(突發(fā)性和長期性訪問)
- 在基準(zhǔn)測試中,Caffeine 的讀寫性能比 Guava Cache 高出 10-20 倍
功能特性對比
| 特性 | Caffeine | Guava Cache |
|---|---|---|
| 異步加載 | 支持 AsyncLoadingCache | 僅同步加載 |
| 過期策略 | 支持基于大小、時間、引用等多種策略 | 僅基本過期策略 |
| 自動刷新 | 支持 refreshAfterWrite | 不支持 |
| 權(quán)重計算 | 支持自定義權(quán)重 | 支持但性能較差 |
| 監(jiān)聽器 | 支持移除監(jiān)聽器 | 支持移除監(jiān)聽器 |
| 統(tǒng)計 | 提供命中率等詳細(xì)統(tǒng)計 | 提供基本統(tǒng)計 |
兼容性與遷移
Caffeine 在設(shè)計時特別考慮了與 Guava Cache 的兼容性:
- 90%以上的 API 可以直接替換
- 主要差異在于構(gòu)建方式(Caffeine.newBuilder() vs CacheBuilder.newBuilder())
- 遷移示例:
// Guava Cache
LoadingCache<Key, Value> cache = CacheBuilder.newBuilder()
.maximumSize(1000)
.build(new CacheLoader<Key, Value>() {
public Value load(Key key) {
return createValue(key);
}
});
// 遷移到 Caffeine
LoadingCache<Key, Value> cache = Caffeine.newBuilder()
.maximumSize(1000)
.build(key -> createValue(key));Q2:Caffeine 的分布式緩存支持與多級緩存架構(gòu)
本地緩存特性
Caffeine 作為本地緩存的核心特點(diǎn):
- 僅作用于單個 JVM 進(jìn)程內(nèi)
- 不同服務(wù)器節(jié)點(diǎn)間的緩存數(shù)據(jù)不共享
- 適用于高頻訪問、低變化率的數(shù)據(jù)
二級緩存架構(gòu)實(shí)現(xiàn)
典型的生產(chǎn)級緩存架構(gòu)組合:
- 第一層:Caffeine 本地緩存(納秒級響應(yīng))
- 設(shè)置合理的過期時間(如30秒)
- 適合極端熱點(diǎn)數(shù)據(jù)
- 第二層:Redis 分布式緩存(毫秒級響應(yīng))
- 設(shè)置較長的過期時間(如5分鐘)
- 使用Redis集群保證高可用
- 數(shù)據(jù)源:數(shù)據(jù)庫/服務(wù)(秒級響應(yīng))
- 最終數(shù)據(jù)一致性保障
實(shí)現(xiàn)示例
public class TwoLevelCacheService {
private final Cache<String, Object> localCache = Caffeine.newBuilder()
.maximumSize(10_000)
.expireAfterWrite(30, TimeUnit.SECONDS)
.build();
private final RedisTemplate<String, Object> redisTemplate;
public Object getData(String key) {
// 1. 嘗試從本地緩存獲取
Object value = localCache.getIfPresent(key);
if (value != null) {
return value;
}
// 2. 嘗試從Redis獲取
value = redisTemplate.opsForValue().get(key);
if (value != null) {
localCache.put(key, value);
return value;
}
// 3. 回源查詢
value = queryDatabase(key);
redisTemplate.opsForValue().set(key, value, 5, TimeUnit.MINUTES);
localCache.put(key, value);
return value;
}
}Q3:緩存擊穿解決方案的深入分析
互斥鎖方案詳解
實(shí)現(xiàn)要點(diǎn):
- 使用
key.intern()獲取字符串規(guī)范表示,確保相同key鎖定同一對象 - 采用雙重檢查鎖定模式減少鎖競爭
- 設(shè)置合理的鎖等待超時時間
增強(qiáng)版實(shí)現(xiàn):
public Object getDataWithLock(String key) {
Object value = cache.getIfPresent(key);
if (value != null) {
return value;
}
synchronized (key.intern()) {
// 雙重檢查
value = cache.getIfPresent(key);
if (value != null) {
return value;
}
try {
value = queryDataSource(key);
cache.put(key, value);
} finally {
// 釋放資源
}
}
return value;
}熱點(diǎn)Key永不過期方案
實(shí)現(xiàn)模式:
主動更新:后臺線程定期(如每分鐘)刷新熱點(diǎn)數(shù)據(jù)
ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);
scheduler.scheduleAtFixedRate(() -> {
List<String> hotKeys = getHotKeyList();
hotKeys.forEach(key -> {
Object value = queryDataSource(key);
cache.put(key, value);
});
}, 0, 1, TimeUnit.MINUTES);
被動更新:獲取數(shù)據(jù)時異步刷新
LoadingCache<String, Object> cache = Caffeine.newBuilder()
.maximumSize(10_000)
.refreshAfterWrite(1, TimeUnit.MINUTES)
.build(key -> queryDataSource(key));
其他防護(hù)策略
- 布隆過濾器:前置過濾不存在的key請求
- 緩存預(yù)熱:系統(tǒng)啟動時加載熱點(diǎn)數(shù)據(jù)
- 隨機(jī)過期時間:對相同類型key設(shè)置不同的過期時間偏移量
int baseExpire = 3600; // 基礎(chǔ)1小時 int randomOffset = ThreadLocalRandom.current().nextInt(600); // 0-10分鐘隨機(jī) cache.put(key, value, baseExpire + randomOffset, TimeUnit.SECONDS);
相關(guān)文章
解決idea爆紅 cant resolve symbol String的問題解析
連著出差幾個禮拜沒有使用idea開發(fā)工具,突然一天打開電腦發(fā)現(xiàn)idea里的代碼全部爆紅,懵逼不如所措,很多朋友建議我按住Alt+回車設(shè)置jdk就能解決,但是仍然報錯,經(jīng)過幾個小時的倒騰最終解決,遇到此問題的朋友參考下本文吧2021-06-06
form表單數(shù)據(jù)通過@RequestBody解析成java實(shí)體類的實(shí)現(xiàn)過程
解決@RequestBody請求無法接收前端數(shù)據(jù)問題:前端通過axios提交數(shù)據(jù),后端使用fastJSON解析時出現(xiàn)NULL,通過在實(shí)體類上添加@JSONField注解指定屬性對應(yīng)關(guān)系,可以成功接收數(shù)據(jù)2025-12-12
spring注入在有常量的情況下使用@AllArgsConstructor操作
這篇文章主要介紹了spring注入在有常量的情況下使用@AllArgsConstructor操作,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2021-09-09
關(guān)于IDEA使用jsp可以訪問頁面轉(zhuǎn)換為html彈出頁面為404的問題
這篇文章主要介紹了關(guān)于IDEA使用jsp可以訪問頁面轉(zhuǎn)換為html彈出頁面為404的問題及解決方法,本文給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下2020-12-12
springboot讀取自定義配置文件時出現(xiàn)亂碼解決方案
這篇文章主要介紹了springboot讀取自定義配置文件時出現(xiàn)亂碼解決方案,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2021-11-11

