Caffeine

Java用高性能アプリケーション内キャッシュライブラリ。Guava Cacheの後継として、最適化されたアルゴリズムと豊富な機能を提供。

キャッシュライブラリJavaSpring Bootアプリケーション内キャッシュ高性能Guava後継

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アプリケーションでのローカルキャッシュニーズに対する理想的なソリューションとして確立されています。