Java 緩存框架 Caffeine 應(yīng)用場(chǎng)景解析
一、Caffeine 簡介
1. 框架概述
Caffeine是由Google工程師Ben Manes開發(fā)的一款Java本地緩存框架,其初始版本發(fā)布于2014年。該框架的設(shè)計(jì)靈感來源于Guava Cache,但在性能和功能方面進(jìn)行了革命性的優(yōu)化。Caffeine基于"W-TinyLFU"(Window-Tiny Least Frequently Used)算法實(shí)現(xiàn),這是一種改進(jìn)的LFU緩存淘汰算法,結(jié)合了LFU的高命中率優(yōu)勢(shì)和LRU的時(shí)效性特點(diǎn)。
1.1 Caffeine的核心優(yōu)勢(shì)
1.1.1 超高性能
Caffeine在性能方面實(shí)現(xiàn)了質(zhì)的飛躍:
- 基準(zhǔn)測(cè)試顯示,相比Guava Cache,Caffeine的讀性能提升約8-12倍,寫性能提升約5-10倍
- 支持每秒數(shù)百萬次(典型值300-500萬QPS)的緩存操作
- 采用無鎖并發(fā)設(shè)計(jì),大幅減少線程競(jìng)爭(zhēng)(如使用并發(fā)哈希表和非阻塞隊(duì)列)
- 對(duì)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ù)場(chǎng)景 - 自定義過期:通過
expireAfter方法實(shí)現(xiàn)基于業(yè)務(wù)邏輯的復(fù)雜過期判斷
1.1.3 異步支持
Caffeine提供完整的異步緩存(AsyncCache)支持:
- 異步加載機(jī)制:通過
AsyncLoadingCache實(shí)現(xiàn)非阻塞式數(shù)據(jù)加載 - 支持CompletableFuture:可與Java8的異步編程模型完美結(jié)合
- 典型應(yīng)用場(chǎng)景:高并發(fā)環(huán)境下的微服務(wù)接口緩存
1.1.4 豐富的監(jiān)聽器
Caffeine提供完善的監(jiān)控支持:
- 移除監(jiān)聽器(
RemovalListener):可監(jiān)聽緩存項(xiàng)的驅(qū)逐、失效或手動(dòng)移除 - 統(tǒng)計(jì)功能:通過
recordStats()開啟命中率統(tǒng)計(jì) - 典型配置:
cache.recordStats(); CacheStats stats = cache.stats(); double hitRate = stats.hitRate();
1.1.5 內(nèi)存安全
Caffeine提供多種內(nèi)存保護(hù)機(jī)制:
- 基于容量:通過
maximumSize限制緩存項(xiàng)數(shù)量 - 基于時(shí)間:通過上述過期策略控制
- 基于引用:支持弱引用鍵/值(
weakKeys/weakValues)和軟引用值(softValues) - 權(quán)重控制:通過
weigher和maximumWeight實(shí)現(xiàn)基于對(duì)象大小的精確控制
典型內(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)異步緩存加載時(shí)添加 -->
<dependency>
<groupId>java.net.http</groupId>
<artifactId>http-client</artifactId>
<version>11.0.1</version> <!-- 最低要求JDK11 -->
<scope>runtime</scope> <!-- 通常只需運(yùn)行時(shí)依賴 -->
</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è)計(jì),主要分為基礎(chǔ)緩存接口和功能擴(kuò)展接口兩大類:
1.基礎(chǔ)緩存接口層次結(jié)構(gòu)
Cache (基本功能)
├── LoadingCache (同步加載)
└── AsyncCache (異步基礎(chǔ))
└── AsyncLoadingCache (異步加載)2.關(guān)鍵組件詳細(xì)說明(含典型應(yīng)用場(chǎng)景)
| 組件 | 作用說明 | 典型使用場(chǎng)景 | 示例代碼片段 |
|---|---|---|---|
Cache<K,V> | 手動(dòng)管理緩存,需顯式處理緩存未命中 | 簡單緩存場(chǎng)景,數(shù)據(jù)源訪問成本較低 | cache.get(key, k -> fetchFromDB(k)) |
LoadingCache<K,V> | 自動(dòng)加載緩存,內(nèi)置CacheLoader | 高頻訪問且加載邏輯固定的場(chǎng)景 | LoadingCache.from(this::loadFromAPI) |
AsyncCache<K,V> | 返回CompletableFuture的異步接口 | 配合非阻塞IO或遠(yuǎn)程調(diào)用 | cache.get(key).thenAccept(value -> ...) |
AsyncLoadingCache<K,V> | 異步自動(dòng)加載緩存 | 微服務(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)計(jì) | listener((key,value,reason) -> logRemoval()) |
Expiry<K,V> | 細(xì)粒度過期控制 | 動(dòng)態(tài)TTL場(chǎng)景(如會(huì)話緩存) | expireAfter((key,value,currentTime) -> customTTL) |
3.高級(jí)特性支持
- 權(quán)重計(jì)算:通過
weigher接口實(shí)現(xiàn)基于緩存對(duì)象大小的淘汰策略 - 刷新機(jī)制:
refreshAfterWrite配合CacheLoader.reload實(shí)現(xiàn)后臺(tái)刷新 - 統(tǒng)計(jì)監(jiān)控:
recordStats()啟用命中率等統(tǒng)計(jì)指標(biāo) - 線程模型:默認(rèn)使用ForkJoinPool.commonPool(),可通過
executor自定義
4.最佳實(shí)踐提示:
- 對(duì)于長時(shí)間加載操作,優(yōu)先選擇AsyncLoadingCache避免阻塞
- 移除監(jiān)聽器不要執(zhí)行耗時(shí)操作,否則會(huì)影響緩存性能
- 在Spring環(huán)境中建議通過@Bean配置全局緩存管理器
- 生產(chǎn)環(huán)境務(wù)必啟用統(tǒng)計(jì)功能(recordStats)進(jìn)行監(jiān)控
三、Caffeine 核心用法
Caffeine 的使用流程遵循 "構(gòu)建器模式配置 → 創(chuàng)建緩存實(shí)例 → 讀寫緩存" 的邏輯,下面分場(chǎng)景講解具體用法。
3.1 基礎(chǔ)緩存(Cache):手動(dòng)控制讀寫
Cache 是最基礎(chǔ)的緩存類型,需手動(dòng)處理緩存未命中(未命中時(shí)返回 null),適合緩存邏輯簡單的場(chǎng)景。
3.1.1 創(chuàng)建 Cache 實(shí)例
通過 Caffeine.newBuilder() 配置緩存規(guī)則,常見配置包括:
- 容量控制:
maximumSize(long):設(shè)置緩存最大容量(條目數(shù)),超過后按 W-TinyLFU 算法淘汰。maximumWeight(long)+weigher(Weigher):基于權(quán)重控制緩存大小,適合不同條目占用不同內(nèi)存的場(chǎng)景。
- 過期策略:
expireAfterWrite(Duration):寫入后過期(如 10 分鐘未更新則過期),適合數(shù)據(jù)變更頻繁的場(chǎng)景。expireAfterAccess(Duration):訪問后過期(如 5 分鐘未訪問則過期),適合熱點(diǎn)數(shù)據(jù)緩存。expireAfter(Expiry):自定義過期時(shí)間計(jì)算邏輯,可實(shí)現(xiàn)基于業(yè)務(wù)規(guī)則的過期。
- 監(jiān)聽器:
removalListener(RemovalListener):設(shè)置緩存移除監(jiān)聽器,可記錄日志或觸發(fā)后續(xù)操作。
- 其他特性:
weakKeys()/weakValues():使用弱引用,允許被垃圾回收。softValues():使用軟引用,在內(nèi)存不足時(shí)被回收。recordStats():啟用統(tǒng)計(jì)信息收集。
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)先級(jí)低于expireAfterWrite)
.removalListener((key, value, cause) -> { // 緩存移除監(jiān)聽器
System.out.printf("緩存移除:key=%s, value=%s, 原因=%s%n",
key, value, cause.toString());
// 原因可能是:EXPLICIT(手動(dòng)刪除)、REPLACED(值被替換)、
// COLLECTED(垃圾回收)、EXPIRED(過期)、SIZE(超過容量限制)
})
.recordStats() // 啟用統(tǒng)計(jì)
.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. 讀取并計(jì)算(未命中時(shí)執(zhí)行函數(shù)邏輯,但不自動(dòng)存入緩存)
String user3 = userCache.get("user:1003", key -> {
// 模擬從數(shù)據(jù)庫查詢數(shù)據(jù)(僅當(dāng)緩存未命中時(shí)執(zhí)行)
System.out.println("緩存未命中,查詢DB:" + key);
return "王五"; // 此結(jié)果不會(huì)自動(dòng)存入緩存
});
System.out.println("讀取user:1003:" + user3); // 輸出:王五
// 5. 緩存維護(hù)操作
userCache.invalidate("user:1002"); // 單個(gè)刪除
userCache.invalidateAll(List.of("user:1001", "user:1003")); // 批量刪除
userCache.cleanUp(); // 手動(dòng)觸發(fā)清理過期條目
userCache.invalidateAll(); // 清空所有緩存
// 6. 查看統(tǒng)計(jì)信息(需先啟用recordStats)
System.out.println("命中率:" + userCache.stats().hitRate());
}
}3.1.2 應(yīng)用場(chǎng)景示例
- 簡單KV緩存:
- 緩存用戶Session信息
- 緩存系統(tǒng)配置項(xiàng)
- 臨時(shí)數(shù)據(jù)存儲(chǔ)(如驗(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;
}
}多級(jí)緩存:
// 作為本地緩存與Redis組成二級(jí)緩存
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):自動(dòng)加載未命中數(shù)據(jù)
LoadingCache 是 Cache 的子類,通過實(shí)現(xiàn) CacheLoader 接口,實(shí)現(xiàn) "緩存未命中時(shí)自動(dòng)加載數(shù)據(jù)并存入緩存",適合緩存數(shù)據(jù)需從數(shù)據(jù)源(如 DB、Redis)加載的場(chǎng)景。
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:定義緩存未命中時(shí)的加載邏輯
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ù)(緩存未命中時(shí)自動(dòng)執(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. 讀取緩存(未命中時(shí)自動(dòng)調(diào)用load()加載并存入緩存)
String product1 = productCache.get("prod:101"); // 首次:加載并返回
System.out.println("讀取prod:101:" + product1); // 輸出:商品-101
// 3. 批量讀取(getAll())
Map<String, String> products = productCache.getAll(List.of("prod:102", "prod:103"));
System.out.println("批量讀取結(jié)果:" + products);
// 4. 主動(dòng)刷新(異步)
productCache.refresh("prod:101"); // 后臺(tái)刷新,舊值仍可用
// 5. 統(tǒng)計(jì)信息
System.out.println("加載次數(shù):" + productCache.stats().loadCount());
}
}3.2.2 關(guān)鍵特性:刷新(Refresh)與過期(Expire)的區(qū)別
| 特性 | 刷新(Refresh) | 過期(Expire) |
|---|---|---|
| 觸發(fā)時(shí)機(jī) | 刷新時(shí)間到后 | 過期時(shí)間到后 |
| 讀取行為 | 異步刷新,立即返回舊值 | 同步重新加載,可能阻塞請(qǐng)求 |
| 適用場(chǎng)景 | 數(shù)據(jù)允許短暫不一致(如商品詳情) | 數(shù)據(jù)強(qiáng)一致要求(如訂單狀態(tài)) |
| 實(shí)現(xiàn)方式 | 需配置refreshAfterWrite | 配置expireAfterWrite/AfterAccess |
典型使用模式:
// 商品詳情緩存:10分鐘強(qiáng)制過期,5分鐘自動(dòng)刷新
LoadingCache<String, Product> productCache = Caffeine.newBuilder()
.maximumSize(10000)
.expireAfterWrite(10, TimeUnit.MINUTES) // 強(qiáng)制過期時(shí)間
.refreshAfterWrite(5, TimeUnit.MINUTES) // 自動(dòng)刷新時(shí)間
.build(this::loadProductFromDB);
3.3 異步緩存(AsyncCache/AsyncLoadingCache):非阻塞讀寫
在高并發(fā)場(chǎng)景下,同步緩存的 load() 可能會(huì)阻塞線程,而 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)境建議使用有界隊(duì)列和拒絕策略)
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 -> {
// 模擬耗時(shí)操作(如RPC調(diào)用,耗時(shí)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. 同步獲取(僅測(cè)試用,實(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, // 空閑線程存活時(shí)間
new LinkedBlockingQueue<>(1000), // 有界隊(duì)列
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("平均加載時(shí)間:" + stats.averageLoadPenalty() + "ns");
四、Caffeine 高級(jí)特性
4.1 緩存統(tǒng)計(jì)(Cache Statistics)
緩存統(tǒng)計(jì)功能是優(yōu)化緩存性能的重要工具。通過開啟緩存統(tǒng)計(jì),可以實(shí)時(shí)監(jiān)控以下關(guān)鍵指標(biāo):
- 命中率(Hit Rate):反映緩存有效性,計(jì)算公式為:
命中次數(shù)/(命中次數(shù)+未命中次數(shù)) - 加載耗時(shí)(Load Penalty):統(tǒng)計(jì)從數(shù)據(jù)源加載數(shù)據(jù)的平均耗時(shí)
- 移除次數(shù)(Eviction Count):因容量或過期策略導(dǎo)致的緩存移除次數(shù)
- 加載失敗率(Load Failure Rate):數(shù)據(jù)源加載失敗的比例
典型應(yīng)用場(chǎng)景:
- 評(píng)估緩存配置是否合理
- 識(shí)別熱點(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)計(jì)功能
.build(key -> {
// 模擬耗時(shí)加載
try {
Thread.sleep(100);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
return "統(tǒng)計(jì)測(cè)試:" + key;
});
// 模擬讀寫操作
statsCache.get("key1"); // 第一次加載(未命中)
statsCache.get("key1"); // 命中已有緩存
statsCache.get("key2"); // 新鍵加載
statsCache.invalidate("key1"); // 手動(dòng)失效
// 獲取統(tǒng)計(jì)結(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("平均加載耗時(shí)(ns):" + stats.averageLoadPenalty()); // 約100ms
System.out.println("加載失敗率:" + stats.loadFailureRate()); // 0.0
}
}4.2 自定義過期策略(Expiry)
標(biāo)準(zhǔn)的TTL(Time-To-Live)過期策略對(duì)所有緩存條目采用統(tǒng)一設(shè)置,而自定義過期策略允許基于業(yè)務(wù)特性實(shí)現(xiàn)精細(xì)化控制。
常見應(yīng)用場(chǎng)景:
- 不同優(yōu)先級(jí)數(shù)據(jù)設(shè)置不同有效期(如熱點(diǎn)數(shù)據(jù)短時(shí)效,冷數(shù)據(jù)長時(shí)效)
- 讀寫操作影響過期時(shí)間(如讀操作續(xù)期)
- 動(dòng)態(tài)調(diào)整過期時(shí)間(如根據(jù)數(shù)據(jù)價(jià)值計(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)建時(shí)過期策略
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) {
// 更新策略:保持原有過期時(shí)間(默認(rèn))
return currentDuration;
// 或者重置為創(chuàng)建時(shí)間:return expireAfterCreate(key, value, currentTime);
}
@Override
public long expireAfterRead(String key, String value,
long currentTime, long currentDuration) {
// 讀取時(shí)策略:熱門數(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行為 | 適用場(chǎng)景 | Caffeine配置 |
|---|---|---|---|
| 強(qiáng)引用 | 永不回收 | 默認(rèn)方式 | - |
| 軟引用 | 內(nèi)存不足時(shí)回收 | 緩存大對(duì)象 | .softValues() |
| 弱引用 | 下次GC時(shí)回收 | 臨時(shí)性緩存 | .weakKeys()/.weakValues() |
注意事項(xiàng):
- 使用
weakKeys()時(shí),key比較基于==而非equals() softValues()可能導(dǎo)致GC壓力增大- 引用回收與顯式失效策略共同作用
// 弱引用Key+Value的緩存(適合臨時(shí)性數(shù)據(jù))
Cache<String, byte[]> weakCache = Caffeine.newBuilder()
.weakKeys() // Key無強(qiáng)引用時(shí)回收
.weakValues() // Value無強(qiáng)引用時(shí)回收
.maximumSize(10_000) // 仍保持容量限制
.build();
// 軟引用Value的緩存(適合大對(duì)象)
Cache<String, byte[]> softCache = Caffeine.newBuilder()
.softValues() // 內(nèi)存不足時(shí)回收Value
.expireAfterWrite(1, TimeUnit.HOURS) // 配合顯式過期
.build();
// 典型使用場(chǎng)景
void processLargeData(String dataId) {
byte[] data = softCache.get(dataId, id -> {
// 從數(shù)據(jù)庫加載大對(duì)象(如圖片、文件等)
return loadLargeDataFromDB(id);
});
// 使用數(shù)據(jù)...
}五、Caffeine 注意事項(xiàng)
在實(shí)際開發(fā)中,若使用不當(dāng),Caffeine 可能出現(xiàn)緩存穿透、內(nèi)存溢出、線程阻塞等問題,以下是核心注意事項(xiàng):
5.1 區(qū)分 "刷新(Refresh)" 與 "過期(Expire)"
刷新(refreshAfterWrite):
- 工作機(jī)制:當(dāng)緩存條目超過指定時(shí)間未被寫入時(shí),下次讀取會(huì)觸發(fā)異步刷新,但在此期間仍會(huì)返回舊值
- 適用場(chǎng)景:對(duì)數(shù)據(jù)一致性要求不高,可接受短暫延遲的場(chǎng)景
- 商品詳情頁的評(píng)論數(shù)統(tǒng)計(jì)
- 新聞資訊的閱讀量統(tǒng)計(jì)
- 排行榜數(shù)據(jù)的更新
過期(expireAfterWrite/expireAfterAccess):
- expireAfterWrite:從寫入開始計(jì)時(shí)
- expireAfterAccess:從最后一次訪問開始計(jì)時(shí)
- 工作機(jī)制:過期后緩存條目立即失效,讀取時(shí)會(huì)同步阻塞直到重新加載完成
- 適用場(chǎng)景:對(duì)數(shù)據(jù)一致性要求高的核心業(yè)務(wù)
- 用戶賬戶余額
- 訂單支付狀態(tài)
- 庫存數(shù)量
?? 典型誤用場(chǎng)景:
將用戶余額這類強(qiáng)一致性數(shù)據(jù)配置為refreshAfterWrite(5s).可能導(dǎo)致:
- 用戶A看到余額100元
- 用戶B完成扣款50元
- 5秒內(nèi)用戶A仍看到100元(舊值)
- 直到下次讀取才刷新為50元
5.2 避免緩存穿透:空值緩存與布隆過濾器
緩存穿透的典型特征:
- 查詢一個(gè)必然不存在的數(shù)據(jù)(如不存在的用戶ID)
- 每次請(qǐng)求都穿透到數(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:布隆過濾器(適合千萬級(jí)key場(chǎng)景)
// 初始化布隆過濾器
BloomFilter<String> bloomFilter = BloomFilter.create(
Funnels.stringFunnel(Charset.defaultCharset()),
1000000, // 預(yù)期元素?cái)?shù)量
0.01 // 誤判率
);
// 查詢流程
if (!bloomFilter.mightContain(key)) {
return null; // 肯定不存在
} else {
return cache.get(key); // 可能存在
}5.3 緩存鍵(Key)必須重寫 hashCode() 和 equals()
常見錯(cuò)誤案例:
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工具類自動(dòng)生成
- 保證不可變(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)存溢出:合理配置容量與過期時(shí)間
典型配置示例:
Caffeine.newBuilder()
.maximumSize(10_000) // 基于條目數(shù)限制
.expireAfterWrite(30, TimeUnit.MINUTES) // 寫入后30分鐘過期
.expireAfterAccess(10, TimeUnit.MINUTES) // 10分鐘無訪問過期
.weigher((String key, String value) -> value.length()) // 按value大小計(jì)算權(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);
// 返回降級(jí)值
return "DEFAULT_VALUE";
// 或者拋出特定異常
// throw new CacheLoadException(e);
}
}
});
// 使用時(shí)的異常處理
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)勢(shì):
- 使用 Count-Min Sketch 數(shù)據(jù)結(jié)構(gòu)高效統(tǒng)計(jì)訪問頻率
- 適應(yīng)不同工作負(fù)載模式(突發(fā)性和長期性訪問)
- 在基準(zhǔn)測(cè)試中,Caffeine 的讀寫性能比 Guava Cache 高出 10-20 倍
功能特性對(duì)比
| 特性 | Caffeine | Guava Cache |
|---|---|---|
| 異步加載 | 支持 AsyncLoadingCache | 僅同步加載 |
| 過期策略 | 支持基于大小、時(shí)間、引用等多種策略 | 僅基本過期策略 |
| 自動(dòng)刷新 | 支持 refreshAfterWrite | 不支持 |
| 權(quán)重計(jì)算 | 支持自定義權(quán)重 | 支持但性能較差 |
| 監(jiān)聽器 | 支持移除監(jiān)聽器 | 支持移除監(jiān)聽器 |
| 統(tǒng)計(jì) | 提供命中率等詳細(xì)統(tǒng)計(jì) | 提供基本統(tǒng)計(jì) |
兼容性與遷移
Caffeine 在設(shè)計(jì)時(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 的分布式緩存支持與多級(jí)緩存架構(gòu)
本地緩存特性
Caffeine 作為本地緩存的核心特點(diǎn):
- 僅作用于單個(gè) JVM 進(jìn)程內(nèi)
- 不同服務(wù)器節(jié)點(diǎn)間的緩存數(shù)據(jù)不共享
- 適用于高頻訪問、低變化率的數(shù)據(jù)
二級(jí)緩存架構(gòu)實(shí)現(xiàn)
典型的生產(chǎn)級(jí)緩存架構(gòu)組合:
- 第一層:Caffeine 本地緩存(納秒級(jí)響應(yīng))
- 設(shè)置合理的過期時(shí)間(如30秒)
- 適合極端熱點(diǎn)數(shù)據(jù)
- 第二層:Redis 分布式緩存(毫秒級(jí)響應(yīng))
- 設(shè)置較長的過期時(shí)間(如5分鐘)
- 使用Redis集群保證高可用
- 數(shù)據(jù)源:數(shù)據(jù)庫/服務(wù)(秒級(jí)響應(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鎖定同一對(duì)象 - 采用雙重檢查鎖定模式減少鎖競(jìng)爭(zhēng)
- 設(shè)置合理的鎖等待超時(shí)時(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)模式:
主動(dòng)更新:后臺(tái)線程定期(如每分鐘)刷新熱點(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);
被動(dòng)更新:獲取數(shù)據(jù)時(shí)異步刷新
LoadingCache<String, Object> cache = Caffeine.newBuilder()
.maximumSize(10_000)
.refreshAfterWrite(1, TimeUnit.MINUTES)
.build(key -> queryDataSource(key));
其他防護(hù)策略
- 布隆過濾器:前置過濾不存在的key請(qǐng)求
- 緩存預(yù)熱:系統(tǒng)啟動(dòng)時(shí)加載熱點(diǎn)數(shù)據(jù)
- 隨機(jī)過期時(shí)間:對(duì)相同類型key設(shè)置不同的過期時(shí)間偏移量
int baseExpire = 3600; // 基礎(chǔ)1小時(shí) int randomOffset = ThreadLocalRandom.current().nextInt(600); // 0-10分鐘隨機(jī) cache.put(key, value, baseExpire + randomOffset, TimeUnit.SECONDS);
相關(guān)文章
SpringBoot中@EnableAutoConfiguration注解的實(shí)現(xiàn)
Spring Boot@EnableAutoConfiguration是一個(gè)強(qiáng)大的工具,可以簡化配置過程,從而實(shí)現(xiàn)快速開發(fā),本文主要介紹了SpringBoot中@EnableAutoConfiguration注解的實(shí)現(xiàn),感興趣的可以了解一下2024-01-01
詳解Spring Boot讀取配置文件與配置文件優(yōu)先級(jí)
這篇文章主要介紹了詳解Spring Boot讀取配置文件與配置文件優(yōu)先級(jí),小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2018-08-08
Java構(gòu)造器(構(gòu)造方法)與方法區(qū)別說明
這篇文章主要介紹了Java構(gòu)造器(構(gòu)造方法)與方法區(qū)別說明,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過來看看吧2020-09-09
SpringBoot 整合 Lettuce Redis的實(shí)現(xiàn)方法
這篇文章主要介紹了SpringBoot 整合 Lettuce Redis的實(shí)現(xiàn)方法,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2019-07-07

