Caffeine
Java用高性能アプリケーション内キャッシュライブラリ。Guava Cacheの後継として、最適化されたアルゴリズムと豊富な機能を提供。
Caffeine
CaffeineはJava用の高性能アプリケーション内キャッシュライブラリです。Guava Cacheの後継として設計され、最適化されたアルゴリズムと豊富な機能を提供し、Spring Boot 2.0以降の標準キャッシュプロバイダーとして採用されています。
概要
CaffeineはJava開発者に最も推奨されるローカルキャッシュソリューションとして位置づけられています。Google Guava Cacheの経験を活かして設計された、Java 8以降に特化した現代的なキャッシュライブラリです。優れたヒット率と低レイテンシを実現し、Spring Boot環境での統合性に優れることから、エンタープライズJavaアプリケーションでの採用が急速に拡大しています。
詳細
主な特徴
- 高性能アルゴリズム: Window TinyLFU アルゴリズムによる優れたキャッシュヒット率
- サイズベース退避: エントリ数や重み関数に基づく柔軟なキャッシュサイズ管理
- 時間ベース有効期限: 書き込み後・アクセス後の時間ベース自動削除
- 自動リフレッシュ: バックグラウンドでの非同期データ更新機能
- Spring統合: Spring Boot/Spring Frameworkとのシームレスな統合
- 統計情報: 詳細なキャッシュパフォーマンス統計の取得
アーキテクチャ
Caffeineの核となる設計原則:
- 非ブロッキング: 読み取り操作での非同期処理によるスループット向上
- 近最適: W-TinyLFU アルゴリズムによるGuava比較で優れた退避判定
- Java 8+最適化: StreamAPI、Optional、CompletableFutureの活用
- メモリ効率: オフヒープ不要のJVMヒープ最適化設計
パフォーマンス特性
- Guava Cacheと比較して最大3倍の書き込み性能向上
- 読み取り専用ワークロードでの優れたスループット
- ガベージコレクション負荷の軽減
- 同期・非同期両方の操作モードサポート
メリット・デメリット
メリット
- 優秀なパフォーマンス: 市場最高クラスのJavaローカルキャッシュ性能
- Spring Boot標準: 2.0以降のデフォルトキャッシュプロバイダーとして簡単統合
- 豊富な機能: サイズ・時間・手動退避、統計、リフレッシュ等の包括的機能
- 優れたヒット率: Window TinyLFUアルゴリズムによる効率的なキャッシュ管理
- 活発な開発: 継続的な性能向上とバグ修正
- 軽量依存: 最小限の外部依存関係
デメリット
- ローカル限定: 分散キャッシュではなくJVMローカルのみ
- Java 8+必須: 古いJavaバージョンでは利用不可
- メモリ制約: JVMヒープサイズに依存したキャッシュ容量
- 設定複雑性: 高度な設定には詳細な理解が必要
- 永続化なし: アプリケーション再起動でキャッシュデータ消失
- 分散環境: クラスター環境での一貫性保証なし
参考ページ
書き方の例
依存関係の追加とセットアップ
<!-- Maven dependency -->
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
<version>3.1.8</version>
</dependency>
<!-- Spring Boot Cache Starter (Caffeineを自動検出) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
// Gradle dependency
implementation 'com.github.ben-manes.caffeine:caffeine:3.1.8'
// Spring Boot Cache
implementation 'org.springframework.boot:spring-boot-starter-cache'
基本的なキャッシュ操作
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import java.time.Duration;
public class CaffeineBasicExample {
public static void main(String[] args) {
// 基本的なキャッシュの作成
Cache<String, String> cache = Caffeine.newBuilder()
.maximumSize(1000) // 最大1000エントリ
.expireAfterWrite(Duration.ofMinutes(10)) // 書き込み後10分で削除
.build();
// データの挿入
cache.put("key1", "value1");
cache.put("key2", "value2");
// データの取得
String value1 = cache.getIfPresent("key1");
System.out.println("key1: " + value1);
// キーが存在しない場合の値生成
String value3 = cache.get("key3", key -> {
// データベースやAPIから取得する処理をシミュレート
return "generated_value_for_" + key;
});
System.out.println("key3: " + value3);
// バッチ操作
Map<String, String> data = Map.of(
"batch1", "bvalue1",
"batch2", "bvalue2",
"batch3", "bvalue3"
);
cache.putAll(data);
// キャッシュ統計の確認
System.out.println("Cache size: " + cache.estimatedSize());
// 手動削除
cache.invalidate("key1");
cache.invalidateAll();
}
}
LoadingCache(自動ロード)の使用
import com.github.benmanes.caffeine.cache.LoadingCache;
import com.github.benmanes.caffeine.cache.CacheLoader;
public class CaffeineLoadingExample {
// データソースのシミュレート
private static final Map<String, String> DATABASE = Map.of(
"user:1", "田中太郎",
"user:2", "佐藤花子",
"user:3", "鈴木一郎"
);
public static void main(String[] args) {
// LoadingCacheの作成
LoadingCache<String, String> userCache = Caffeine.newBuilder()
.maximumSize(500)
.expireAfterAccess(Duration.ofMinutes(5)) // アクセス後5分で削除
.refreshAfterWrite(Duration.ofMinutes(1)) // 書き込み後1分でリフレッシュ
.build(new CacheLoader<String, String>() {
@Override
public String load(String key) throws Exception {
// データベースからの読み込みをシミュレート
System.out.println("Loading from database: " + key);
Thread.sleep(100); // データベースアクセスの遅延をシミュレート
return DATABASE.getOrDefault(key, "Unknown User");
}
@Override
public String reload(String key, String oldValue) throws Exception {
// 非同期でのリフレッシュ処理
System.out.println("Refreshing: " + key);
return load(key);
}
});
// データの取得(自動でCacheLoaderが呼ばれる)
String user1 = userCache.get("user:1");
String user2 = userCache.get("user:2");
String unknown = userCache.get("user:999");
System.out.println("User 1: " + user1);
System.out.println("User 2: " + user2);
System.out.println("Unknown: " + unknown);
// バッチロード
Map<String, String> users = userCache.getAll(
Arrays.asList("user:1", "user:2", "user:3")
);
users.forEach((k, v) -> System.out.println(k + " -> " + v));
}
}
AsyncCache(非同期キャッシュ)
import com.github.benmanes.caffeine.cache.AsyncCache;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class CaffeineAsyncExample {
private static final ExecutorService executor = Executors.newFixedThreadPool(4);
public static void main(String[] args) {
// 非同期キャッシュの作成
AsyncCache<String, String> asyncCache = Caffeine.newBuilder()
.maximumSize(1000)
.expireAfterWrite(Duration.ofMinutes(10))
.executor(executor) // カスタムエグゼキューター
.buildAsync();
// 非同期での値設定
CompletableFuture<Void> putFuture = asyncCache.put("async_key",
CompletableFuture.supplyAsync(() -> {
// 重い処理をシミュレート
try {
Thread.sleep(500);
return "async_value_generated_at_" + System.currentTimeMillis();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}, executor));
// 非同期での値取得
CompletableFuture<String> getFuture = asyncCache.get("async_key",
(key, executor) -> CompletableFuture.supplyAsync(() -> {
System.out.println("Generating value for: " + key);
return "default_value_for_" + key;
}, executor));
// 結果の取得
getFuture.thenAccept(value -> {
System.out.println("Async result: " + value);
}).join();
// 複数の非同期操作
CompletableFuture<Map<String, String>> batchFuture = asyncCache.getAll(
Arrays.asList("key1", "key2", "key3"),
(keys, executor) -> {
Map<String, CompletableFuture<String>> futures = keys.stream()
.collect(Collectors.toMap(
key -> key,
key -> CompletableFuture.supplyAsync(() ->
"batch_value_" + key, executor)
));
return futures;
}
);
batchFuture.thenAccept(results -> {
results.forEach((k, v) -> System.out.println("Batch: " + k + " -> " + v));
}).join();
executor.shutdown();
}
}
Spring Boot統合
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.stereotype.Service;
@SpringBootApplication
@EnableCaching
public class CaffeineSpringBootApplication {
public static void main(String[] args) {
SpringApplication.run(CaffeineSpringBootApplication.class, args);
}
}
@Service
public class UserService {
@Cacheable(value = "users", key = "#userId")
public User findUser(Long userId) {
// データベースアクセスをシミュレート
System.out.println("データベースからユーザーを取得: " + userId);
simulateSlowDatabase();
return new User(userId, "User " + userId, "user" + userId + "@example.com");
}
@CacheEvict(value = "users", key = "#userId")
public void updateUser(Long userId, User user) {
System.out.println("ユーザーを更新: " + userId);
// キャッシュから削除される
}
@CacheEvict(value = "users", allEntries = true)
public void clearUserCache() {
System.out.println("全ユーザーキャッシュをクリア");
}
private void simulateSlowDatabase() {
try {
Thread.sleep(200);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
// application.yml設定
spring:
cache:
type: caffeine
caffeine:
spec: maximumSize=1000,expireAfterAccess=300s,recordStats
cache-names:
- users
- products
- orders
# カスタム設定(application.properties)
# spring.cache.caffeine.spec=maximumSize=1000,expireAfterWrite=600s
カスタムキャッシュ設定
import com.github.benmanes.caffeine.cache.RemovalListener;
import com.github.benmanes.caffeine.cache.Weigher;
import com.github.benmanes.caffeine.cache.stats.CacheStats;
public class CaffeineAdvancedExample {
public static void main(String[] args) {
// 高度なキャッシュ設定
Cache<String, String> advancedCache = Caffeine.newBuilder()
.maximumWeight(1000) // 重み合計の制限
.weigher(new Weigher<String, String>() {
@Override
public int weigh(String key, String value) {
return key.length() + value.length();
}
})
.expireAfterAccess(Duration.ofMinutes(5))
.expireAfterWrite(Duration.ofMinutes(10))
.refreshAfterWrite(Duration.ofMinutes(1))
.recordStats() // 統計情報を記録
.removalListener(new RemovalListener<String, String>() {
@Override
public void onRemoval(String key, String value, RemovalCause cause) {
System.out.println("削除: " + key + " -> " + value + " (" + cause + ")");
}
})
.build();
// データの追加とアクセス
advancedCache.put("short", "val");
advancedCache.put("medium_key", "medium_value");
advancedCache.put("very_long_key_name", "very_long_value_content_here");
// 統計情報の取得
CacheStats stats = advancedCache.stats();
System.out.println("Request count: " + stats.requestCount());
System.out.println("Hit count: " + stats.hitCount());
System.out.println("Miss count: " + stats.missCount());
System.out.println("Hit rate: " + stats.hitRate());
System.out.println("Average load penalty: " + stats.averageLoadPenalty());
System.out.println("Eviction count: " + stats.evictionCount());
// キャッシュのクリーンアップ
advancedCache.cleanUp();
// 重み確認のため追加データ投入
for (int i = 0; i < 100; i++) {
advancedCache.put("key" + i, "value" + i);
}
System.out.println("Final cache size: " + advancedCache.estimatedSize());
}
}
パフォーマンステストと比較
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ThreadLocalRandom;
public class CaffeinePerformanceTest {
private static final int OPERATIONS = 1_000_000;
private static final int THREADS = 4;
public static void main(String[] args) throws InterruptedException {
testCaffeine();
testConcurrentHashMap();
}
private static void testCaffeine() throws InterruptedException {
Cache<Integer, String> cache = Caffeine.newBuilder()
.maximumSize(10000)
.recordStats()
.build();
long startTime = System.nanoTime();
Thread[] threads = new Thread[THREADS];
for (int t = 0; t < THREADS; t++) {
threads[t] = new Thread(() -> {
for (int i = 0; i < OPERATIONS / THREADS; i++) {
int key = ThreadLocalRandom.current().nextInt(1000);
cache.get(key, k -> "value" + k);
}
});
}
for (Thread thread : threads) {
thread.start();
}
for (Thread thread : threads) {
thread.join();
}
long duration = System.nanoTime() - startTime;
CacheStats stats = cache.stats();
System.out.println("=== Caffeine Performance ===");
System.out.println("Duration: " + duration / 1_000_000 + " ms");
System.out.println("Hit rate: " + String.format("%.2f%%", stats.hitRate() * 100));
System.out.println("Average load time: " + stats.averageLoadPenalty() + " ns");
}
private static void testConcurrentHashMap() throws InterruptedException {
ConcurrentHashMap<Integer, String> map = new ConcurrentHashMap<>();
long startTime = System.nanoTime();
Thread[] threads = new Thread[THREADS];
for (int t = 0; t < THREADS; t++) {
threads[t] = new Thread(() -> {
for (int i = 0; i < OPERATIONS / THREADS; i++) {
int key = ThreadLocalRandom.current().nextInt(1000);
map.computeIfAbsent(key, k -> "value" + k);
}
});
}
for (Thread thread : threads) {
thread.start();
}
for (Thread thread : threads) {
thread.join();
}
long duration = System.nanoTime() - startTime;
System.out.println("=== ConcurrentHashMap Performance ===");
System.out.println("Duration: " + duration / 1_000_000 + " ms");
System.out.println("Final size: " + map.size());
}
}
Caffeineは、そのシンプルで直感的なAPIと優れたパフォーマンスにより、Javaアプリケーションでのローカルキャッシュニーズに対する理想的なソリューションとして確立されています。