Caffeine

Javaキャッシュローカルキャッシュ高性能Spring BootWindow TinyLFU

GitHub概要

ben-manes/caffeine

A high performance caching library for Java

スター17,137
ウォッチ363
フォーク1,666
作成日:2014年12月13日
言語:Java
ライセンス:Apache License 2.0

トピックス

なし

スター履歴

ben-manes/caffeine Star History
データ取得日時: 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);
    }
}