CLOVER🍀

That was when it all began.

軽量キャッシュライブラリ、cache2kを試す

前にJavaで使えるHTTPクライアントを調べた時に、jcabi-httpというライブラリがcache2kというキャッシュライブラリに依存していることを知りました。というか、同じ人が作っているっぽいです。

cache2k
http://cache2k.org/

特徴として、

  • 速い
  • Expireのサポートあり
  • Cacheの実装を切り替えられる(LRL、Clock、Clock Pro Plus、ARC、Random)

というのが挙げられるみたいです。

ベンチマークも載せられています。速いらしい?

Benchmarks
http://cache2k.org/benchmarks.html

なお、分散キャッシュではないので、そういうのが欲しい場合は別のものを使ってねってスタンスみたいです。

ちょっと試してみます。

Maven依存関係

使い方によって、少し変わります。

通常は、こうなります。

    <dependency>
      <groupId>org.cache2k</groupId>
      <artifactId>cache2k-api</artifactId>
      <version>0.20</version>
    </dependency>
    <dependency>
      <groupId>org.cache2k</groupId>
      <artifactId>cache2k-core</artifactId>
      <version>0.20</version>
      <scope>runtime</scope>
    </dependency>

APIをcompileに、coreをruntimeにします。後述の、Cacheの実装を切り替える場合にはcoreをcompileにします。

cache2k自身の依存関係は、オプションでCommons Loggingのみです(微妙…)。

今回はapiとcoreだけ使いますが、他にEEおよびJMX関係のモジュールが存在します。基本的には統計情報取得のためっぽいです。

使ってみる

ここからのコードは、以下のimport文があることを前提にします。

import static org.assertj.core.api.Assertions.*;

import java.util.Spliterator;
import java.util.Spliterators;
import java.util.concurrent.TimeUnit;
import java.util.stream.IntStream;
import java.util.stream.StreamSupport;

import org.cache2k.Cache;
import org.cache2k.CacheBuilder;
import org.cache2k.CacheEntry;
import org.cache2k.CacheException;
import org.cache2k.CacheSource;
import org.cache2k.ClosableIterator;

import org.junit.Test;

しれっと、JUnitとAssertJがいます。

単純なパターン

まずは導入的に、最もシンプルと思われる設定で、基本的なAPIを使ってみます。

最初に、CacheBuilderを使って、キーと値のClassクラスを指定してCacheのインスタンスを作成します。

        Cache<String, String> cache =
            CacheBuilder.newCache(String.class, String.class).build();

エントリの登録は、putで。

        IntStream
            .rangeClosed(1, 5)
            .forEach(i -> cache.put("key" + i, "value" + i));

取得は、peekもしくはgetを使います。

        assertThat(cache.peek("key1"))
            .isEqualTo("value1");
        assertThat(cache.get("key2"))
            .isEqualTo("value2");

両者の違いですが、getの方は存在しないエントリに対するキーを指定すると、例外が投げられます。

        assertThat(cache.peek("key10"))
            .isNull();

        try {
            cache.get("key10");
            failBecauseExceptionWasNotThrown(CacheException.class);
        } catch (CacheException e) {
            assertThat(e).hasMessage("org.cache2k.impl.CacheUsageExcpetion: source not set");
        }

これは、Cache#getには後述するCacheSourceを呼び出すという副作用が付けられるからみたいです。今の例はCacheSourceを設定していないので、こうなりますと。

エントリの削除はremove、全削除はclear。

        cache.remove("key1");

        assertThat(cache.peek("key1"))
            .isNull();

        cache.clear();

        assertThat(cache.getTotalEntryCount())
            .isEqualTo(0);

最後は、destoryでCacheを破棄します。

        cache.destroy();

イテレーションを行うには、Cache#iteratorメソッドでClosableIteratorを取得して、CacheEntryでイテレーションします。

        Cache<String, String> cache =
            CacheBuilder.newCache(String.class, String.class).build();

        IntStream
            .rangeClosed(1, 5)
            .forEach(i -> cache.put("key" + i, "value" + i));

        int result = 0;
        try (ClosableIterator<CacheEntry<String, String>> iterator = cache.iterator()) {
            result =
                StreamSupport
                .stream(Spliterators.spliteratorUnknownSize(iterator, Spliterator.ORDERED),
                        false)
                .mapToInt(entry -> {
                    return Integer.decode(((String)entry.getValue()).substring("value".length()));
                })
                .sum();
        }

        assertThat(result)
            .isEqualTo(15);

        cache.destroy();

ClosableIteratorは、クローズする必要があります。あと、ClosableIteratorってスペルが…??

CacheSourceを使う

先ほど名前だけ出てきましたが、CacheSourceを使うとCache#getの呼び出し時にロード処理を入れることができます。

使い方は、CacheSourceインターフェースを実装したクラスを作成し、getメソッドを実装します。getメソッドの引数は、キーとなります。

        CacheSource<String, String> source = new CacheSource<String, String>() {
            @Override
            public String get(String key) throws Throwable {
                return "**" + key + "**";
            }
        };

        Cache<String, String> cache =
            CacheBuilder
            .newCache(String.class, String.class)
            .source(source)
            .build();

        assertThat(cache.get("key"))
            .isEqualTo("**key**");

        cache.destroy();

なお、Cache#peek呼び出し時にはCacheSourceは呼び出されません。

        CacheSource<String, String> source = key -> "**" + key + "**";

        Cache<String, String> cache =
            CacheBuilder
            .newCache(String.class, String.class)
            .source(source)
            .build();

        assertThat(cache.peek("key"))
            .isNull();

        cache.destroy();
Expireを設定する

CacheBuilderでCacheを構築する際に、expiryDurationを指定することで、キャッシュエントリの有効期限を設定することができます。

        Cache<String, String> cache =
            CacheBuilder
            .newCache(String.class, String.class)
            .expiryDuration(3, TimeUnit.SECONDS)
            .build();

        cache.put("key1", "value1");
        cache.put("key2", "value2");

        TimeUnit.SECONDS.sleep(2);

        cache.peek("key1");

        TimeUnit.SECONDS.sleep(2);

        assertThat(cache.peek("key1"))
            .isNull();
        assertThat(cache.peek("key2"))
            .isNull();

        cache.destroy();

アイドル時間ではなく、TTLのようなのでエントリにアクセスしても有効期限は伸びません…。

書き込めば延長されるという説明がサイトにありますが、それはそうだろうという気も。

        Cache<String, String> cache =
            CacheBuilder
            .newCache(String.class, String.class)
            .expiryDuration(3, TimeUnit.SECONDS)
            .build();

        cache.put("key1", "value1");
        cache.put("key2", "value2");

        TimeUnit.SECONDS.sleep(2);

        cache.put("key1", "value1-1");

        TimeUnit.SECONDS.sleep(2);

        assertThat(cache.peek("key1"))
            .isEqualTo("value1-1");
        assertThat(cache.peek("key2"))
            .isNull();

        TimeUnit.SECONDS.sleep(4);

        assertThat(cache.peek("key1"))
            .isNull();

        cache.destroy();

有効期限切れしないようにする場合は、externalでtrueを指定すればOKです。

        Cache<String, String> cache =
            CacheBuilder
            .newCache(String.class, String.class)
            .eternal(true)
            .build();

ちなみに、デフォルトの有効期限は10分みたいです。

キャッシュエントリの最大登録数を指定する

CacheBuilderのmaxSizeで指定します。

       Cache<String, String> cache =
            CacheBuilder
            .newCache(String.class, String.class)
            .maxSize(3)  // デフォルトの最大サイズは、2,000みたい
            .build();

今回、3にしてみました。

ちなみに、デフォルトはLRUキャッシュみたいですよ。

        assertThat(cache.getClass().getName())
            .isEqualTo("org.cache2k.impl.LruCache");

確かに、それっぽい動きをします。

        cache.put("key1", "value1");
        cache.put("key2", "value2");
        cache.put("key3", "value3");

        cache.peek("key1");
        cache.peek("key1");
        cache.peek("key3");
        cache.peek("key3");

        cache.put("key4", "value4");

        assertThat(cache.peek("key2"))
            .isNull();

        assertThat(cache.getTotalEntryCount())
            .isEqualTo(3);

        assertThat(cache.peek("key1"))
            .isEqualTo("value1");
        assertThat(cache.peek("key3"))
            .isEqualTo("value3");
        assertThat(cache.peek("key4"))
            .isEqualTo("value4");
Cacheの実装を指定する

最初に少し触れましたが、cache2kでは利用するCacheの実装を指定してアルゴリズムを変更することができます。

Maven依存関係で、coreのスコープをコンパイルにします。

    <dependency>
      <groupId>org.cache2k</groupId>
      <artifactId>cache2k-core</artifactId>
      <version>0.20</version>
      <scope>compile</scope>
    </dependency>

Cacheの実装クラスがコンパイルスコープに入るので、使いたいクラスをimportしましょう。

import org.cache2k.impl.ClockCache;

他には、ClockProPlusCache、ArcCache、RandomCacheなどがあります。

サイトからたどれるJavadocからは、これらのクラスは確認できないのですが…。

Cacheの実装を指定するには、CacheBuilderのimplementationを使います。

        Cache<String, String> cache =
            CacheBuilder
            .newCache(String.class, String.class)
            .implementation(ClockCache.class)
            .maxSize(3)
            .build();

Cacheの実装が切り替わります。

        assertThat(cache.getClass().getName())
            .isEqualTo("org.cache2k.impl.ClockCache");

LruCacheの時と同様のコードでも、ちょっと動きが変わります。

        cache.put("key1", "value1");
        cache.put("key2", "value2");
        cache.put("key3", "value3");

        cache.peek("key1");
        cache.peek("key1");
        cache.peek("key3");
        cache.peek("key3");


        cache.put("key4", "value4");

        assertThat(cache.peek("key2"))
            .isEqualTo("value2");

        assertThat(cache.getTotalEntryCount())
            .isEqualTo(3);

        assertThat(cache.peek("key1"))
            .isNull();

        assertThat(cache.peek("key3"))
            .isEqualTo("value3");
        assertThat(cache.peek("key4"))
            .isEqualTo("value4");
バグっぽい動き?

使っててちょっと気になったのは、CacheSourceを設定せずにCache#getを呼び出すと、Cacheが認識しているエントリ数が増えたように見えることですね。

        Cache<String, String> cache =
            CacheBuilder.newCache(String.class, String.class).build();

        IntStream
            .rangeClosed(1, 5)
            .forEach(i -> cache.put("key" + i, "value" + i));

        assertThat(cache.getTotalEntryCount())
            .isEqualTo(5);

        assertThat(cache.peek("key1"))
            .isEqualTo("value1");
        assertThat(cache.get("key2"))
            .isEqualTo("value2");

        assertThat(cache.peek("key10"))
            .isNull();

        try {
            cache.get("key10");
            failBecauseExceptionWasNotThrown(CacheException.class);
        } catch (CacheException e) {
            assertThat(e).hasMessage("org.cache2k.impl.CacheUsageExcpetion: source not set");
        }

        cache.remove("key1");

        assertThat(cache.getTotalEntryCount())
            .isEqualTo(5);  // 増えた??

        assertThat(cache.peek("key1"))
            .isNull();

        cache.destroy();

5個エントリを登録して、ひとつ削除しているのに5個あることになっています…。

ちょっとバグっぽいような動きに見えますが…。

感想

設定ファイルもありませんし、ちょっと癖がありそうなのですがまあ悪くないかなぁと。

気になるのは、アイドル時間によるタイムアウトの概念がないこと、まだ0.20とバージョンが低いこと、今後も開発が続けられるかどうかでしょうか?
アイドル時間によるタイムアウトは設定可能にして欲しかったですかね。

とりあえず、Cacheの選択肢として覚えておきましょう。