Caffeine
GitHub概要
ben-manes/caffeine
A high performance caching library for Java
スター17,137
ウォッチ363
フォーク1,666
作成日:2014年12月13日
言語:Java
ライセンス:Apache License 2.0
トピックス
なし
スター履歴
データ取得日時: 2025/10/22 09:55
ライブラリ
Caffeine
概要
CaffeineはJava向け最高性能ローカルキャッシュライブラリです。Window TinyLFUアルゴリズム、非同期ローディング、自動リフレッシュ、サイズ・時間ベースエビクション機能を実装し、Spring Boot 2.x以降のデフォルトキャッシュプロバイダーとして採用されています。2025年でJavaエコシステムのローカルキャッシュ標準として完全に確立し、Guava Cacheを凌駕する性能とSpring Boot標準採用により圧倒的シェアを獲得、マイクロサービス、高負荷APIサーバーでの採用が急増し、Java開発における必須ライブラリとなっています。
詳細
Caffeine 3.2.1は2025年現在も活発に開発されており、Java 11+での最適化とGraalVM Native Imageサポートを提供。Window TinyLFU(W-TinyLFU)エビクションアルゴリズムにより、従来のLRUアルゴリズムを上回る高いヒット率を実現し、4%の改善を達成。非同期APIによりI/Oブロッキングを回避し、バルクローディング、自動リフレッシュ、統計機能を標準装備。JCache(JSR-107)完全準拠により、標準仕様での利用が可能。
主な特徴
- Window TinyLFU アルゴリズム: 最適なエビクション戦略による高いキャッシュヒット率
- 非同期ローディング: 非ブロッキングI/Oによる高スループット
- 自動リフレッシュ: バックグラウンドでの期限切れデータ更新
- サイズベースエビクション: メモリ効率的なキャッシュサイズ管理
- 時間ベースエビクション: writeAfter/accessAfterによる柔軟な期限設定
- 統計とメトリクス: パフォーマンス監視と最適化支援
メリット・デメリット
メリット
- Javaローカルキャッシュで最高クラスの性能(Guava Cacheの数倍)
- Spring Boot標準採用による豊富なエコシステム統合
- 非同期APIによる高並行性とスケーラビリティ
- Window TinyLFUによる従来比4%のヒット率向上
- メモリ効率的な設計とGCプレッシャー軽減
- 包括的な統計機能による運用可視性
デメリット
- シングルJVMでのローカルキャッシュ(分散キャッシュ不可)
- Java 11以上が必要(レガシー環境で制約)
- 高機能ゆえの設定複雑性(シンプルな用途では過剰)
- メモリ使用量がキャッシュサイズに比例
- キャッシュウォームアップ時間が必要
- デバッグ時のキャッシュ状態把握の難しさ
参考ページ
書き方の例
依存関係の追加
// Gradle でのセットアップ
implementation 'com.github.ben-manes.caffeine:caffeine:3.2.1'
// オプション拡張
implementation 'com.github.ben-manes.caffeine:guava:3.2.1' // Guava 互換性
implementation 'com.github.ben-manes.caffeine:jcache:3.2.1' // JCache サポート
<!-- Maven でのセットアップ -->
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
<version>3.2.1</version>
</dependency>
基本的なキャッシュ作成と操作
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import java.util.concurrent.TimeUnit;
public class BasicCacheExample {
public static void main(String[] args) {
// 基本的なキャッシュ作成
Cache<String, String> cache = Caffeine.newBuilder()
.maximumSize(1_000) // 最大サイズ
.expireAfterWrite(Duration.ofMinutes(5)) // 書き込み後5分で期限切れ
.build();
// キャッシュ操作
cache.put("user:1", "John Doe");
String user = cache.getIfPresent("user:1");
System.out.println("User: " + user); // "User: John Doe"
// 存在しない場合のデフォルト値
String defaultUser = cache.get("user:999", key -> "Unknown User");
System.out.println("Default: " + defaultUser); // "Default: Unknown User"
// バルク操作
Map<String, String> users = Map.of(
"user:2", "Jane Smith",
"user:3", "Bob Johnson"
);
cache.putAll(users);
// キャッシュサイズと統計
System.out.println("Cache size: " + cache.estimatedSize());
cache.invalidate("user:1"); // 個別削除
cache.invalidateAll(); // 全削除
}
}
LoadingCache(自動ローディング)
import com.github.benmanes.caffeine.cache.LoadingCache;
public class LoadingCacheExample {
// データベースアクセスのシミュレーション
private static String loadUserFromDatabase(String userId) {
// 実際のDBアクセス処理
try {
Thread.sleep(100); // DB アクセス時間をシミュレーション
return "User-" + userId + "-from-DB";
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return null;
}
}
public static void main(String[] args) {
// LoadingCache の作成
LoadingCache<String, String> userCache = Caffeine.newBuilder()
.maximumSize(10_000)
.expireAfterWrite(Duration.ofMinutes(30))
.refreshAfterWrite(Duration.ofMinutes(5)) // 5分後にバックグラウンドでリフレッシュ
.recordStats() // 統計情報記録
.build(key -> loadUserFromDatabase(key)); // ローダー関数
// 自動ローディング
String user1 = userCache.get("user:123"); // DB からロード
String user2 = userCache.get("user:123"); // キャッシュから取得
// バルクローディング
Map<String, String> users = userCache.getAll(
List.of("user:100", "user:101", "user:102")
);
// 統計情報確認
CacheStats stats = userCache.stats();
System.out.printf("Hit rate: %.2f%%\n", stats.hitRate() * 100);
System.out.printf("Miss count: %d\n", stats.missCount());
System.out.printf("Load time: %.2f ms\n", stats.averageLoadTime() / 1_000_000);
}
}
AsyncCache(非同期キャッシュ)
import com.github.benmanes.caffeine.cache.AsyncCache;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Executor;
import java.util.concurrent.ForkJoinPool;
public class AsyncCacheExample {
private static CompletableFuture<String> loadUserAsync(String userId) {
return CompletableFuture.supplyAsync(() -> {
// 非同期でのデータ取得処理
try {
Thread.sleep(50);
return "Async-User-" + userId;
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new RuntimeException(e);
}
});
}
public static void main(String[] args) throws Exception {
// AsyncCache の作成
AsyncCache<String, String> asyncCache = Caffeine.newBuilder()
.maximumSize(1_000)
.expireAfterWrite(Duration.ofMinutes(10))
.buildAsync();
// 非同期での値取得
CompletableFuture<String> future1 = asyncCache.get("user:async1",
key -> loadUserAsync(key));
CompletableFuture<String> future2 = asyncCache.get("user:async2",
(key, executor) -> loadUserAsync(key));
// 結果取得
String result1 = future1.get();
String result2 = future2.get();
System.out.println("Async Result 1: " + result1);
System.out.println("Async Result 2: " + result2);
// バルク非同期操作
CompletableFuture<Map<String, String>> bulkFuture = asyncCache.getAll(
List.of("user:bulk1", "user:bulk2"),
(keys, executor) -> {
Map<String, CompletableFuture<String>> futures = new HashMap<>();
for (String key : keys) {
futures.put(key, loadUserAsync(key));
}
return futures;
}
);
Map<String, String> bulkResults = bulkFuture.get();
System.out.println("Bulk Results: " + bulkResults);
}
}
Spring Boot での統合
// Spring Boot Configuration
@Configuration
@EnableCaching
public class CacheConfiguration {
@Bean
public CacheManager cacheManager() {
CaffeineCacheManager cacheManager = new CaffeineCacheManager();
cacheManager.setCaffeine(Caffeine.newBuilder()
.initialCapacity(100)
.maximumSize(1000)
.expireAfterAccess(Duration.ofMinutes(10))
.weakKeys()
.recordStats());
return cacheManager;
}
// 複数キャッシュの設定
@Bean
public CacheManager multipleCacheManager() {
CaffeineCacheManager manager = new CaffeineCacheManager();
// ユーザーキャッシュ
manager.registerCustomCache("users",
Caffeine.newBuilder()
.maximumSize(1000)
.expireAfterWrite(Duration.ofHours(1))
.build());
// セッションキャッシュ
manager.registerCustomCache("sessions",
Caffeine.newBuilder()
.maximumSize(10000)
.expireAfterAccess(Duration.ofMinutes(30))
.build());
return manager;
}
}
// Service クラスでの使用
@Service
public class UserService {
@Cacheable(value = "users", key = "#userId")
public User getUserById(String userId) {
// データベースからユーザー取得
return userRepository.findById(userId);
}
@CacheEvict(value = "users", key = "#user.id")
public User updateUser(User user) {
return userRepository.save(user);
}
@CacheEvict(value = "users", allEntries = true)
public void clearUserCache() {
// 全ユーザーキャッシュをクリア
}
}
カスタムエビクションポリシー
import com.github.benmanes.caffeine.cache.RemovalCause;
import com.github.benmanes.caffeine.cache.RemovalListener;
public class CustomEvictionExample {
public static void main(String[] args) {
// カスタムリムーバル リスナー
RemovalListener<String, String> removalListener =
(key, value, cause) -> {
System.out.printf("Removed: %s=%s (cause: %s)\n",
key, value, cause);
// クリーンアップ処理
if (cause == RemovalCause.EXPIRED) {
// 期限切れ時の処理
cleanupExpiredData(key, value);
} else if (cause == RemovalCause.SIZE) {
// サイズ制限による削除時の処理
logSizeBasedEviction(key);
}
};
// 重み付きエビクション
Cache<String, String> weightedCache = Caffeine.newBuilder()
.maximumWeight(1_000_000) // 最大重み
.weigher((String key, String value) -> {
// オブジェクトサイズに基づく重み計算
return key.length() + value.length();
})
.removalListener(removalListener)
.build();
// 時間ベースの複合エビクション
Cache<String, String> timeBasedCache = Caffeine.newBuilder()
.expireAfterWrite(Duration.ofHours(2)) // 書き込み後2時間
.expireAfterAccess(Duration.ofMinutes(30)) // アクセス後30分
.refreshAfterWrite(Duration.ofMinutes(15)) // 15分でリフレッシュ
.removalListener(removalListener)
.build(key -> fetchDataFromSource(key));
// 弱参照による自動ガベージコレクション連携
Cache<String, Object> weakRefCache = Caffeine.newBuilder()
.maximumSize(1000)
.weakKeys() // キーの弱参照
.weakValues() // 値の弱参照
.build();
}
private static void cleanupExpiredData(String key, String value) {
// 期限切れデータのクリーンアップ処理
System.out.println("Cleaning up expired data: " + key);
}
private static void logSizeBasedEviction(String key) {
// サイズベースエビクションのログ
System.out.println("Size-based eviction: " + key);
}
private static String fetchDataFromSource(String key) {
// データソースからの取得処理
return "Fresh data for " + key;
}
}
パフォーマンス監視と最適化
public class CacheMonitoringExample {
public static void setupCacheWithMonitoring() {
Cache<String, String> monitoredCache = Caffeine.newBuilder()
.maximumSize(1000)
.expireAfterWrite(Duration.ofMinutes(10))
.recordStats() // 統計記録を有効化
.build();
// 定期的な統計レポート
ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);
scheduler.scheduleAtFixedRate(() -> {
CacheStats stats = monitoredCache.stats();
System.out.println("=== Cache Statistics ===");
System.out.printf("Hit Rate: %.2f%%\n", stats.hitRate() * 100);
System.out.printf("Miss Rate: %.2f%%\n", stats.missRate() * 100);
System.out.printf("Hit Count: %d\n", stats.hitCount());
System.out.printf("Miss Count: %d\n", stats.missCount());
System.out.printf("Load Count: %d\n", stats.loadCount());
System.out.printf("Eviction Count: %d\n", stats.evictionCount());
System.out.printf("Average Load Time: %.2f ms\n",
stats.averageLoadTime() / 1_000_000.0);
System.out.println("========================");
}, 0, 30, TimeUnit.SECONDS);
// JVM メトリクス統合(Micrometer 使用例)
/*
MeterRegistry meterRegistry = new SimpleMeterRegistry();
CaffeineCacheMetrics.monitor(meterRegistry, monitoredCache, "userCache");
*/
}
// ベンチマークとパフォーマンステスト
public static void benchmarkCachePerformance() {
Cache<Integer, String> cache = Caffeine.newBuilder()
.maximumSize(10_000)
.build();
int iterations = 100_000;
long startTime = System.nanoTime();
// 書き込みベンチマーク
for (int i = 0; i < iterations; i++) {
cache.put(i, "Value " + i);
}
long writeTime = System.nanoTime() - startTime;
// 読み込みベンチマーク
startTime = System.nanoTime();
for (int i = 0; i < iterations; i++) {
cache.getIfPresent(i % 1000);
}
long readTime = System.nanoTime() - startTime;
System.out.printf("Write time: %.2f ms\n", writeTime / 1_000_000.0);
System.out.printf("Read time: %.2f ms\n", readTime / 1_000_000.0);
System.out.printf("Write throughput: %.0f ops/sec\n",
iterations * 1_000_000_000.0 / writeTime);
System.out.printf("Read throughput: %.0f ops/sec\n",
iterations * 1_000_000_000.0 / readTime);
}
}