Guava Cacheについて

2021/10/21に公開

Cache機能

Guavaでは簡単にインメモリキャッシュを利用できる。
スレッドセーフなのでマルチスレッドでも問題なし。

キャッシュの登録

  • 自動的にキャッシュに登録
    LoadingCacheインターフェースでキャッシュを操作していく。
    Guava Cacheのドキュメントを見ていくとこちらが一般的なように見えるが、自分としてはどのようなユースケースでこれを利用していくのかあまり想像できていない。ランダム値とかをvalueとして入れるみたいな時は使えそうだけど。。。
    詳しい人誰か教えて(苦笑
    LoadingCache<String, String> loadingCache = CacheBuilder.newBuilder()
      .build(new CacheLoader<String, String>() {
        @Override
        public String load(String key) throws Exception {
          return key.toUpperCase();
        }
      });
    
    //cacheに存在しないため、ここでvalueが自動生成
    String value = loadingCache.get("key");
    //cacheから値を取得
    value = loadingCache.get("key");
    
    この例ではkeyを大文字にしたものが自動的に登録される。
    コメントに書いてあるとおりファーストアクセスではデータが存在しないため、CacheLoaderのloadメソッドに記述した計算をし、結果をキャッシュに登録する。
    loadメソッドはkeyが引数となり、別のデータを付与できないため、keyから計算される値か、外からのデータが不要な値しか登録できない(ここがそんなに用途がないと思える部分)。
  • 手動でキャッシュに挿入
    Cacheインターフェースでキャッシュを操作していく。
    ただ、CacheインターフェースはLoadingCacheインターフェースの親なのでLoadingCacheでも以下の操作は可能。
    自分はこちらの方を多用するが、一般的なのかは不明。
    Cache<String, String> cache = CacheBuilder.newBuilder().build();
    
    //キャッシュの登録
    cache.put("key", "cacheData");
    //cacheからvalueを取得
    String value = cache.getIfPresent("key");
    
    シンプルにキャッシュの登録、取得ができる。
    また、一気に複数の値も登録できる。
    cache.putAll(ImmutableMap.of("key1", "data1", "key2", "data2", "key3", "data3"));
    

取得について

取得するメソッドは3つ存在する。

  • get
    LoadingCacheでkeyに対するvalueを取得する。
    load時にエラーが発生すると、ExecutionExceptionがthrowされる
  • getUnchecked
    LoadingCacheでkeyに対するvalueを取得する。
    getとの違いはExecutionExceptionがthrowされないこと
    CacheLoaderをlambdaを利用して生成もでき、その場合Exceptionは吐かれないのでgetメソッドを使う必要がないという感じ(使えないわけではない)。
    //lambdaを利用したインスタンス生成
    CacheLoader.from((key) -> key.toUpperCase())
    
  • getIfPresent
    Cacheでkeyに対するvalueを取得する。
    紐づくvalueがあればその値を返却するし、なければnullを返却する

削除について

あまり利用しないと思うが、明示的に削除も可能

//一つのエントリーを削除
cache.invalidate("key");

//複数のエントリーを削除
cache.invalidateAll(Arrays.asList("key1", "key2", "key3"));

//全てのエントリーを削除
cache.invalidateAll();

nullの扱い

CacheLoaderパターンで、nullをloadしようとするとExceptionがthrowされてしまう。
あまりないのかもしれないが、nullをloadする必要がある場合はOptionalを活用する。

Eviction

Guava Cacheでは大きく分けて3つのEvictionを設定できる。
CacheBuilderにEviction設定を付与することで利用できる。

  • 保持数による破棄
    登録されたエントリー数が指定した数値を超えた場合に古いものから破棄される。
    Cache<String, String> cache = CacheBuilder.newBuilder().maximumSize(10).build();
    
  • 重みによる破棄
    重みという表現が正しいのか不明だが、メソッドを見る限りWeightということなので重みとしておく。
    任意の何かを超えた場合に古いものから破棄される。
    任意の何かというのはWeigherクラスで定義する。
    //この例はキャッシュに登録されたvalueのバイト数が10バイトを超えた場合に古いものから破棄
    Cache<String, String> cache = CacheBuilder.newBuilder()
      .maximumWeight(10)
      .weigher(new Weigher<String, String>() {
        @Override
        public int weigh(String key, String value) {
          return value.getBytes().length();
        }
      }).build();
    
  • 時間による破棄
    判断基準となる時間が2つ存在し、それぞれその時間から有効期限が過ぎたものから破棄される。
    //最後にアクセスしたタイミング
    Cache<String, String> cache = CacheBuilder.newBuilder()
      .expireAfterAccess(3, TimeUnit.HOURS)
      .build();
    
    //作成したタイミング
    Cache<String, String> cache = CacheBuilder.newBuilder()
      .expireAfterWrite(1, TimeUnit.DAYS)
      .build();
    

GCに関連した設定

キャッシュデータを強参照じゃなくすことができる。
これによりGCされた時に破棄されやすくできる。
正直そこまで厳密にGC制御しなきゃいけないアプリ作ったことはないので、あまり使う機会はなさそう。

//keyをWeakReferenceにすることで強参照が存在しなくなり、GCが発生したタイミングでkeyも破棄される
Cache<String, String> cache = CacheBuilder.newBuilder().weakKeys().build();

//valueをWeakReferenceにすることで強参照が存在しなくなり、GCが発生したタイミングでvalueも破棄される
Cache<String, String> cache = CacheBuilder.newBuilder().weakValues().build();

//valueをSoftReferenceにすることで弱参照よりは強い参照となり、メモリに余裕があればGCが発生しても破棄されない可能性がある
Cache<String, String> cache = CacheBuilder.newBuilder().softValues().build();

キャッシュの更新

LoadingCacheパターンの場合のリフレッシュ方法は手動更新と自動更新の2種類存在する。

  • 手動でキャッシュを更新
    refreshメソッドを使うことで更新できる。
    loadingCache.refresh("key");
    
  • 自動でキャッシュを更新
    refreshAfterWriteを事前に設定しておくことで自動更新が可能。 また、Evictionのexpire系に似ているが、あちらはデータが破棄されるが、こちらは古いデータを保持し続けている。
    LoadingCache<String, String> loadingCache = CacheBuilder.newBuilder()
      .refreshAfterWrite(1, TimeUnit.MINUTES)
      .build(new CacheLoader<String, String>() {
        @Override
        public String load(String key) throws Exception {
          return key.toUpperCase();
        }
      });
    

削除時の追加処理

キャッシュからデータが削除されたときに、何らかのアクションを起こす必要がある場合に、RemovalListenerを利用して追加処理を行うことができる。
以下の例はEvictionで破棄された時にコンソールにキーを出力する処理。

Cache<String, String> cache = CacheBuilder.newBuilder()
  .maximumSize(10)
  .removalListener(new RemovalListener<String, String>() {
    @Override
    public void onRemoval(RemovalNotification<String, String> notification){
        if (notification.wasEvicted()) {
          System.out.println(notification.getKey());
        }
    }
  }).build();

削除された原因はgetCauseメソッドで取得でき、

  • COLLECTED(GCにて破棄)
  • EXPIRED(有効期限切れ)
  • EXPLICIT(削除した時)
  • REPLACED(更新した時)
  • SIZE(サイズ超過)

の5種類存在する。

パフォーマンス計測

各種統計データを簡単に取得できる。
recodeStatsを設定しておき、statsメソッドで統計データを取得する。

Cache<String, String> cache = CacheBuilder.newBuilder().recordStats().build();

//CacheStatusに情報が格納されている
CacheStats cacheStats = cache.stats();

取れるデータとしては以下。
それなりに使えるデータが多いと思うがパフォーマンス問題がどれほど影響するのかな?

  • 平均ロード時間
  • Eviction数
  • ヒット数
  • ヒットレート
  • ロード数
  • ロード時のエラー発生回数
  • ロード時のエラーレート
  • ロードに成功した回数
  • ミスヒット数
  • ミスヒットレート
  • リクエスト数
  • 総ロード時間

まとめ

  • かなり簡単にインメモリキャッシュを実装できる。
  • 取得方法にパターンがある。
  • Evictionも充実しており、組み合わせることも可能なので柔軟に設計できる。
  • 削除時に通知等の付随処理をすることが可能。
  • キャッシュに関するデータ集計がしやすい(性能劣化の可能性があるので使い所が難しそうだが。。。)。

Discussion