Guava Cacheの基本構造と実践的活用

1. キャッシュの基本特性

Guava Cacheはプロセス内ローカルキャッシュであり、ネットワーク通信を必要とせず高速なアクセスが可能ですが、JVMメモリ制限の影響を受けます。主な利用シーンは以下の通りです:

  • 高頻度アクセスが求められる
  • データ変更頻度が低い
  • メモリ使用量が少ないデータ
  • 全データの取得が必要
  • リアルタイム性が必須でない

2. 基本的な実装例

2.1 ライブラリ依存関係

<dependency>
    <groupId>com.google.guava</groupId>
    <artifactId>guava</artifactId>
    <version>31.1-jre</version>
</dependency>

2.2 キャッシュインスタンスの生成

CacheBuilder<String, String> builder = CacheBuilder.newBuilder()
    .initialCapacity(8)
    .concurrencyLevel(5)
    .maximumSize(15000)
    .expireAfterWrite(30L, TimeUnit.MINUTES)
    .build(new CacheLoader<String, String>() {
        @Override
        public String load(String key) {
            return fetchDataFromDB(key);
        }
    });

2.3 データ取得処理

// キャッシュ値を取得(存在しない場合ロード処理が自動実行)
String result = cache.get(key);

キーが存在しないか有効期限切れの場合、loadメソッドが呼び出されます。複数スレッドが同時にアクセスした場合、最初のスレッドがロードを実行し、他のスレッドは待機します。

3. リフレッシュ機能の実装

3.1 リフレッシュ設定例

ListeningExecutorService executor = MoreExecutors.listeningDecorator(Executors.newFixedThreadPool(2));
Cache cache = CacheBuilder.newBuilder()
    .expireAfterWrite(30L, TimeUnit.MINUTES)
    .refreshAfterWrite(15L, TimeUnit.MINUTES)
    .build(new CacheLoader<String, String>() {
        @Override
        public String load(String key) {
            return fetchFromDB(key);
        }
        @Override
        public ListenableFuture<String> reload(String key, String oldValue) {
            return executor.submit(() -> {
                Thread.sleep(500);
                return fetchFromDB(key);
            });
        }
    });

3.2 時間設定の動作メカニズム

  • expireAfterWrite:設定時間経過後、キャッシュが削除されloadが実行
  • refreshAfterWrite:設定時間経過後、reloadが非同期実行され、現時点の値が返却
  • 両方設定時は、expireAfterWriteが優先

4. コアメカニズムの解説

4.1 キャッシュ構造

内部実装ではCacheSegment配列を管理し、各セグメントはAtomicReferenceArrayでハッシュテーブルを保持します。

class CacheSegment<K, V> extends ReentrantLock {
    AtomicReferenceArray<CacheEntry<K, V>> table;
}

4.2 過去データの処理

有効期限の判定はisExpiredメソッドで行われます:

boolean isExpired(ReferenceEntry entry, long now) {
    if (expiresAfterWrite() && (now - entry.getWriteTime() >= expireAfterWriteNanos)) {
        return true;
    }
    return false;
}

5. リスク対策

5.1 キャッシュパンク対策

複数スレッドによる同時アクセスを防ぐため、expireAfterWriterefreshAfterWriteを組み合わせて利用します。有効期限切れ時に1つのスレッドのみがロード処理を実行し、他のスレッドは待機します。

5.2 リフレッシュ時のデータ不整合

reloadは非同期実行されるため、返却値がnullの場合、古い値が返却されます。このため、リフレッシュ中は古いデータが一時的に使用される可能性があります。

タグ: guava-cache concurrent-cache expiration-policy Memory-Management

7月21日 03:21 投稿