🌟 引言
在軟件開發過程中,緩存是提升系統性能的常用手段。對于基礎場景,直接使用 Java集合框架(如Map/Set/List)即可滿足需求。然而,當面對更復雜的緩存場景時:
- 需要支持多種過期策略(基于時間、訪問頻率等)
- 要求自動淘汰機制
- 需要線程安全等高級特性
自行實現這些功能往往復雜度較高。本文將介紹 Java 生態中成熟的兩大主流本地緩存解決方案:Caffeine(新一代緩存之王)和Guava Cache(經典緩存方案)。
📊 核心維度對比
評估維度 | Caffeine | Guava Cache |
---|---|---|
性能 | ? 讀寫吞吐量高5-10倍 | 🐢 中等性能 |
內存效率 | 🧠 更低內存占用(優化數據結構) | 📦 較高內存消耗 |
并發能力 | 🚀 無鎖算法,百萬級QPS | 🔒 分段鎖,十萬級QPS |
淘汰算法 | 🎯 TinyLFU + LRU 自適應 | ? 標準LRU |
監控統計 | 📈 內置詳細指標 | 📊 基礎統計 |
JDK兼容性 | Java 8+ | Java 6+ |
社區活躍度 | 🌟 持續更新(2023年仍有新版本) | 🛑 維護模式(僅修復bug) |
🚀 Caffeine
Caffeine 是一個性能ISS(In-Space Sizing)的緩存框架,它使用無鎖算法和分段鎖機制,以更優的方式優化了緩存淘汰算法。Caffeine 的設計目標為極致性能,并針對一些常見的場景進行了優化。
🌟 特性
- 無鎖算法和分段鎖機制,以更優的方式優化了緩存淘汰算法。
- 高命中率,通過優化淘汰算法,Caffeine 顯著提高緩存命中率。
- 更低內存開銷,Caffeine 使用更小的內存結構,從而減少內存消耗。
- 線程安全,Caffeine 支持并發操作,保證線程安全。
🌟 如何使用
<dependency><groupId>com.github.ben-manes.caffeine</groupId><artifactId>caffeine</artifactId><version>3.2.0</version>
</dependency>
import com.github.benmanes.caffeine.cache.*;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.CompletableFuture;public class CaffeineDemo {public static void main(String[] args) {basicUsageDemo();loadingCacheDemo();asyncLoadingCacheDemo();evictionDemo();statisticsDemo();}/*** 基礎緩存操作示例*/public static void basicUsageDemo() {System.out.println("\n=== 1. 基礎緩存操作 ===");Cache<String, String> cache = Caffeine.newBuilder().expireAfterWrite(5, TimeUnit.SECONDS) // 寫入5秒后過期.maximumSize(100) // 最大100個條目.build();// 手動寫入cache.put("key1", "value1");// 獲取值(不存在返回null)String value = cache.getIfPresent("key1");System.out.println("獲取key1: " + value); // 輸出: value1// 獲取或計算(線程安全)String value2 = cache.get("key2", k -> "computed-" + k);System.out.println("獲取key2: " + value2); // 輸出: computed-key2}/*** 自動加載緩存示例*/public static void loadingCacheDemo() {System.out.println("\n=== 2. 自動加載緩存 ===");LoadingCache<String, String> cache = Caffeine.newBuilder().expireAfterAccess(3, TimeUnit.SECONDS) // 3秒未訪問則過期.maximumSize(10).build(key -> {// 模擬從數據庫加載System.out.println("正在加載: " + key);return "db-value-" + key;});// 自動觸發加載函數System.out.println(cache.get("user1001")); // 輸出: db-value-user1001System.out.println(cache.get("user1001")); // 第二次直接從緩存獲取}/*** 異步加載緩存示例*/public static void asyncLoadingCacheDemo() {System.out.println("\n=== 3. 異步加載緩存 ===");AsyncLoadingCache<String, String> cache = Caffeine.newBuilder().expireAfterWrite(10, TimeUnit.SECONDS).maximumSize(1000).buildAsync(key -> {// 模擬異步加載return CompletableFuture.supplyAsync(() -> {System.out.println("異步加載: " + key);return "async-value-" + key;});});// 異步獲取cache.get("id123").thenAccept(value -> {System.out.println("異步獲取結果: " + value); // 輸出: async-value-id123});}/*** 淘汰策略示例*/public static void evictionDemo() {System.out.println("\n=== 4. 淘汰策略 ===");Cache<String, String> cache = Caffeine.newBuilder().maximumSize(3) // 測試用的小容量.removalListener((key, value, cause) -> System.out.printf("淘汰事件: key=%s, 原因=%s\n", key, cause)).build();cache.put("k1", "v1");cache.put("k2", "v2");cache.put("k3", "v3");cache.put("k4", "v4"); // 觸發淘汰(LRU)System.out.println("當前大小: " + cache.estimatedSize()); // 輸出: 3}/*** 統計功能示例*/public static void statisticsDemo() {System.out.println("\n=== 5. 統計功能 ===");Cache<String, String> cache = Caffeine.newBuilder().maximumSize(100).recordStats() // 開啟統計.build();cache.put("k1", "v1");cache.getIfPresent("k1");cache.getIfPresent("missingKey");CacheStats stats = cache.stats();System.out.println("命中率: " + stats.hitRate()); // 輸出: 0.5System.out.println("命中數: " + stats.hitCount()); // 輸出: 1System.out.println("未命中數: " + stats.missCount()); // 輸出: 1}
}
🚀 Guava Cache
Guava Cache 是 Google 官方提供的一個緩存框架,它提供了許多高級特性,如自動加載、統計、序列化、并發控制等。與 Caffeine 不同,Guava Cache 的設計目標為簡單易用,并支持更多的高級特性。
🌟 特性
- 自動加載、統計、序列化、并發控制等高級特性。
- 更高的并發控制,Guava Cache 使用更復雜的并發控制機制,以更優的方式解決并發問題。
🌟 如何使用
<dependency><groupId>com.google.guava</groupId><artifactId>guava</artifactId><version>33.4.8-jre</version>
</dependency>
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import com.google.common.cache.*;
import io.vavr.collection.List;
public class GuavaCacheDemo {public static void main(String[] args) throws ExecutionException {basicUsageDemo();loadingCacheDemo();cacheRemovalListenerDemo();cacheStatisticsDemo();advancedEvictionDemo();}/*** 基礎緩存操作示例*/public static void basicUsageDemo() {System.out.println("\n=== 1. 基礎緩存操作 ===");Cache<String, String> cache = CacheBuilder.newBuilder().expireAfterWrite(5, TimeUnit.SECONDS) // 寫入5秒后過期.maximumSize(100) // 最大100個條目.concurrencyLevel(4) // 并發級別.build();// 手動寫入cache.put("key1", "value1");// 獲取值(不存在返回null)String value = cache.getIfPresent("key1");System.out.println("獲取key1: " + value); // 輸出: value1// 嘗試獲取不存在的keyString value2 = cache.getIfPresent("key2");System.out.println("獲取不存在的key2: " + value2); // 輸出: null}/*** 自動加載緩存示例*/public static void loadingCacheDemo() throws ExecutionException {System.out.println("\n=== 2. 自動加載緩存 ===");LoadingCache<String, String> cache = CacheBuilder.newBuilder().expireAfterAccess(3, TimeUnit.SECONDS) // 3秒未訪問則過期.maximumSize(10).build(new CacheLoader<String, String>() {@Overridepublic String load(String key) {// 模擬從數據庫加載System.out.println("正在加載: " + key);return "db-value-" + key;}});// 自動觸發加載函數System.out.println(cache.get("user1001")); // 輸出: db-value-user1001System.out.println(cache.get("user1001")); // 第二次直接從緩存獲取// 批量獲取System.out.println(cache.getAll(List.of("user1002", "user1003")));}/*** 緩存淘汰監聽器示例*/public static void cacheRemovalListenerDemo() {System.out.println("\n=== 3. 淘汰監聽器 ===");RemovalListener<String, String> listener = notification -> {System.out.printf("淘汰事件: key=%s, value=%s, 原因=%s\n", notification.getKey(), notification.getValue(),notification.getCause());};Cache<String, String> cache = CacheBuilder.newBuilder().maximumSize(3) // 測試用的小容量.removalListener(listener).build();cache.put("k1", "v1");cache.put("k2", "v2");cache.put("k3", "v3");cache.put("k4", "v4"); // 觸發淘汰(LRU)cache.invalidate("k2"); // 手動觸發淘汰}/*** 緩存統計示例*/public static void cacheStatisticsDemo() {System.out.println("\n=== 4. 緩存統計 ===");Cache<String, String> cache = CacheBuilder.newBuilder().maximumSize(100).recordStats() // 開啟統計.build();cache.put("k1", "v1");cache.getIfPresent("k1");cache.getIfPresent("missingKey");CacheStats stats = cache.stats();System.out.println("命中率: " + stats.hitRate()); // 輸出: 0.5System.out.println("命中數: " + stats.hitCount()); // 輸出: 1System.out.println("未命中數: " + stats.missCount()); // 輸出: 1System.out.println("加載成功數: " + stats.loadSuccessCount());}/*** 高級淘汰策略示例*/public static void advancedEvictionDemo() {System.out.println("\n=== 5. 高級淘汰策略 ===");Cache<String, String> cache = CacheBuilder.newBuilder()// 基于權重的淘汰(假設不同value占用不同空間).maximumWeight(1000).weigher((String key, String value) -> value.length())// 弱引用key和value(適合緩存大對象).weakKeys().weakValues()// 定期維護(減少并發開銷).concurrencyLevel(8).build();cache.put("long", "這是一個很長的字符串值");cache.put("short", "小");System.out.println("當前大小: " + cache.size());}
}
🎉 結論
對于大多數現代 Java 應用,Caffeine 無疑是更優選擇,其卓越的性能表現和更低的內存開銷使其成為新項目的首選。而 Guava Cache 則更適合已有 Guava 生態的遺留系統,或者需要特定功能(如 CacheLoader 深度集成)的場景。
終極建議: 新項目直接采用 Caffeine,老項目若無性能瓶頸可繼續使用 Guava Cache,在遇到性能問題時再考慮遷移。兩者 API 相似,遷移成本較低。
🚀 公眾號【會飛的架構師】關注并發送:面試。領取私人技術資料