CLOVER🍀

That was when it all began.

JCacheのRead ThroughとWrite Throughについて

2014年、最初のエントリです。みなさま、あけましておめでとうございます。今年もマイペース、自分好みのネタで書いていきますが、どうぞよろしくお願い致します。

で、新年1発目のエントリですが、去年やったJCacheの続きをやります。Twitterで、ちょっとしたツイートを見て、「そういえば後で確認しようと思って、やってなかったなぁ」というのがきっかけです。

去年書いた、JCacheのエントリはこちらです。

Standard Caching
http://d.hatena.ne.jp/Kazuhira/20131204/1386163253

では、いってみます。お題は、JCacheのRead ThroughとWrite Throughについです。

注)
このエントリは、2014年1月1日時点で1.0.0-PFD(Proposed Final Draft)の状態のJSR-107に関する内容です。今後、変更される可能性があります。
ちなみに、GitHub上は1.0.0-RC1になってました。

JSR 107: JCACHE - Java Temporary Caching API
http://www.jcp.org/en/jsr/detail?id=107

JSR107 (JCache)
https://github.com/jsr107/jsr107spec

差分はあんまり見ていませんが、設定周りのクラスがインターフェースになったり、追加されたりしてるところは見ました…。

Cacheに持つデータを、外部データストアから読み書きする

Read Through/Write Throughは、別にJCacheに限った話ではありません。

Cacheに対して操作を行う時に、「Cacheにまだ乗っていないデータは、別のデータストアから読み込みたい」とか、「Cacheにデータを保存すると、合わせて別のデータストアに保存したい」といった要求を実現するものです。

ここでいう別のデータストアというのは、ファイルだったりデータベースだったりすることが多いようです。

外部データストアからの読み込み

読み込みについては、Read Throughと言います。JCacheの場合、

javax.cache.integration.CacheLoader<K,V>

インターフェースを実装し、MutableConfigurationに登録すると共に、MutableConfiguration#setReadThroughをtrueにすることで有効化されます。

CacheLoaderインターフェースを実装するクラスが書く必要があるのは、

// 単一のキーに対するロード
V load(K key)
       throws CacheLoaderException

// 複数のキーに対するロード
Map<K,V> loadAll(Iterable<? extends K> keys)
                 throws CacheLoaderException

となります。

これらのメソッドがJCacheのCacheインターフェースの読み込み系のメソッドを呼び出した時に、同期的に呼び出されます。
*MutableConfiguration#setReadThroughをtrueにしている場合

参考までに、Read Throughが有効かどうかを返却するMutableConfiguration#isReadThroughメソッドのJavadocには、以下のようなことが書かれています。

public boolean isReadThrough()

Determines if a Cache should operate in read-through mode.

When in "read-through" mode, cache misses that occur due to cache entries not existing as a result of performing a "get" will appropriately cause the configured CacheLoader to be invoked.

The default value is false.

デフォルトは無効ですよ、設定すればCacheLoaderが呼び出されるようになりますよ、ってところですね。

外部データストアに対する書き込み

書き込みについては、考え方としてはWrite ThroughとWrite Behindがあります。

両者の違いは同期か非同期かで、Write ThroughはCacheの書き込みオペレーションに対して同期的に呼び出しが行われるため一貫性の面では優れますが、パフォーマンスに劣ります。Write Behindは非同期の呼び出しとなり、Write Throughに比べるとパフォーマンスの劣化は少ないですが、一貫性の面では不安が残ります。

で、調べてみた感じ、JCacheで規定されているのはWrite Throughのみのようです。MutableConfiguration#setWriteThroughのtrue/falseで切り替えられないかなと思ったのですが、そういうわけでもなさそうです。

ちょっと残念…。

話を戻して、Write Throughを使用するためには

javax.cache.integration.CacheWriter<K,V>

インターフェースを実装して、MutableConfigurationに登録します。また、MutableConfiguration#setWriteThroughをtrueに設定します。

CacheWriterインターフェースを実装するクラスが書く必要があるのは、

// 単一のエントリに対する書き込み
void write(Cache.Entry<? extends K,? extends V> entry)
           throws CacheWriterException

// 複数のエントリに対する書き込み
void writeAll(Collection<Cache.Entry<? extends K,? extends V>> entries)
              throws CacheWriterException

// 単一のキーに対する削除
void delete(Object key)
            throws CacheWriterException

// 複数のキーに対する削除
void deleteAll(Collection<?> keys)
               throws CacheWriterException

となります。なんか、Objectとかの型がゴロゴロ登場しますが…。

こちらも、MutableConfiguration#isWriteThroughのJavadocに書かれている内容を載せておきます。

public boolean isWriteThrough()

Determines if a Cache should operate in write-through mode.

When in "write-through" mode, cache updates that occur as a result of performing "put" operations called via one of Cache.put(Object, Object), Cache.getAndRemove(Object), Cache.removeAll(), Cache.getAndPut(Object, Object) Cache.getAndRemove(Object), Cache.getAndReplace(Object, Object), Cache.invoke(Object, javax.cache.processor.EntryProcessor, Object...), Cache.invokeAll(java.util.Set, javax.cache.processor.EntryProcessor, Object...) will appropriately cause the configured CacheWriter to be invoked.

The default value is false.

デフォルト無効、Write Throughを有効にするとCacheWriterが呼び出されるように設定されるよというのは、Read Throughの時と同じですね。


Read ThroughtもWrite Throughもそうですが、Cacheインターフェースのどのメソッドに対してCacheLoader/CacheWriterのメソッドが起動されるかは、

  • 7.2.Read-Through Caching
  • 7.3.Write-Through Caching

に表があるので、詳しく見たい方はこちらを確認されるとよいでしょう。

それにしても、JSR-107でRead Through/Write Throughについては、本当にちょこっとしか書かれてないですね…。

試してみる

で、これで終わると面白くないので、動作確認までやってみました。

CacheLoader/CacheWriterの実装を用意する

JCacheの実装には、前回同様Infinispanを使用します。Maven依存関係は、こんな感じ。

    <dependency>
      <groupId>org.infinispan</groupId>
      <artifactId>infinispan-jcache</artifactId>
      <version>6.0.1.Final</version>
    </dependency>

6.0.1.FinalはMaven Centralにはありますが、オフィシャルサイトのダウンロードページは6.0.0.Finalのままです。利用形態で、お好きな方を…。

では、簡単なCacheLoader/CacheWriterの実装を用意します。まずはCacheLoaderから。

すごく単純な実装で、キーと値はString、キーは「key」で始まることを前提として残りを「value」という文字列とくっつけて値にする感じです。
例えば、キー:「key1」、値:「value1」

また、データストアは簡単のためにHashMapにしています。
src/main/java/org/littlewings/jcache/loaderwriter/SimpleCacheLoader.java

package org.littlewings.jcache.loaderwriter;

import java.io.Serializable;
import java.util.HashMap;
import java.util.Map;
import javax.cache.integration.CacheLoader;

public class SimpleCacheLoader implements CacheLoader<String, String>, Serializable {
    private Map<String, String> store = new HashMap<>();
    private Map<String, String> callerThreadNames = new HashMap<>();

    @Override
    public String load(String key) {
        callerThreadNames.put(key,
                              Thread.currentThread().getName());

        if (store.containsKey(key)) {
            return store.get(key);
        } else {
            // キーは、「key」で始まる前提
            String value = "value" + key.substring(3);
            store.put(key, value);
            return value;
        }
    }

    @Override
    public Map<String, String> loadAll(Iterable<? extends String> keys) {
        Map<String, String> loaded = new HashMap<>();

        for (String key : keys) {
            loaded.put(key, load(key));
        }

        return loaded;
    }

    public Map<String, String> getStore() {
        return store;
    }

    public Map<String, String> getCallerThreadNames() {
        return callerThreadNames;
    }
}

一応、動作しているスレッド名も保持しておくようにしました。

気になるところとしてSerializableインターフェースを実装していますが、これは後にCacheLoaderのインスタンスMutableConfigurationに登録する時にSerializableである必要があるようなのでそうしています。それ以外の方法で登録する場合に必要かどうかまでは、ちょっと確認していません。この点は、後述します。

続いて、CacheWriterの実装。
src/main/java/org/littlewings/jcache/loaderwriter/SimpleCacheWriter.java

package org.littlewings.jcache.loaderwriter;

import java.io.Serializable;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import javax.cache.Cache;
import javax.cache.integration.CacheWriter;

public class SimpleCacheWriter implements CacheWriter<String, String>, Serializable {
    private Map<String, String> store = new HashMap<>();
    private Map<String, String> writeCallerThreadNames = new HashMap<>();
    private Map<String, String> deleteCallerThreadNames = new HashMap<>();

    @Override
    public void delete(Object key) {
        deleteCallerThreadNames.put((String) key,
                                    Thread.currentThread().getName());

        store.remove(key);
    }

    @Override
    public void deleteAll(Collection<?> keys) {
        for (Object key : keys) {
            delete(key);
        }
    }

    @Override
    public void write(Cache.Entry<? extends String, ? extends String> entry) {
        writeCallerThreadNames.put(entry.getKey(),
                                   Thread.currentThread().getName());

        store.put(entry.getKey(), entry.getValue());
    }

    @Override
    public void writeAll(Collection<Cache.Entry<? extends String,? extends String>> entries) {
        for (Cache.Entry<? extends String, ? extends String> entry : entries) {
            write(entry);
        }
    }

    public Map<String, String> getStore() {
        return store;
    }

    public Map<String, String> getDeleteCallerThreadNames() {
        return deleteCallerThreadNames;
    }

    public Map<String, String> getWriteCallerThreadNames() {
        return writeCallerThreadNames;
    }
}

データの保存先がHashMapだったり、一応スレッド名を覚えておくところなどは同じです。ジェネリクスの上限境界が出てくるところが、CacheLoaderとの違いですね…。

動作確認コードを用意する

では、これらの動作確認コードを用意します。ユニットテストコードでやりましょう。

    <dependency>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
      <version>4.11</version>
      <scope>test</scope>
    </dependency>

    <dependency>
      <groupId>org.easytesting</groupId>
      <artifactId>fest-assert-core</artifactId>
      <version>2.0M10</version>
      <scope>test</scope>
    </dependency>

テストには、JUnit/Fest Assertionsを使うことにしました。

途中で面倒になって、テストクラスはまとめてしまいました…。
src/test/java/org/littlewings/jcache/loaderwriter/SimpleCacheLoaderWriterTest.java

package org.littlewings.jcache.loaderwriter;

import static org.fest.assertions.api.Assertions.*;

import javax.cache.Cache;
import javax.cache.CacheManager;
import javax.cache.Caching;
import javax.cache.configuration.Configuration;
import javax.cache.configuration.FactoryBuilder;
import javax.cache.configuration.MutableConfiguration;
import javax.cache.spi.CachingProvider;

import org.fest.assertions.data.MapEntry;
import org.junit.Test;

public class SimpleCacheLoaderWriterTest {

// ここにテストを書く!!

}

では、以下少しずつ試していってみましょう。

まずはRead Throughからですが、CacheLoader/CacheWriterは一応共に登録しておきます。ただし、setReadThroughはtrueにしていません。

    @Test
    public void defaultLoaderTest() {
        CachingProvider provider = Caching.getCachingProvider();
        CacheManager cacheManager = provider.getCacheManager();

        SimpleCacheLoader cacheLoader = new SimpleCacheLoader();
        SimpleCacheWriter cacheWriter = new SimpleCacheWriter();

        // CacheLoader/CacheWriterは設定する
        Configuration<String, String> configuration
            = new MutableConfiguration<String, String>()
            .setCacheLoaderFactory(FactoryBuilder.factoryOf(cacheLoader))
            .setCacheWriterFactory(FactoryBuilder.factoryOf(cacheWriter));

        Cache<String, String> cache
            = cacheManager.createCache("simpleCache", configuration);

        // Read Through/Writh Throughは共にオフ
        assertThat(configuration.isReadThrough()).isFalse();
        assertThat(configuration.isWriteThrough()).isFalse();

        // getしても、nullとなる(キャッシュエントリはロードされない)
        assertThat(cache.get("key1")).isNull();
        assertThat(cache.get("key2")).isNull();
        assertThat(cache.get("key3")).isNull();

        // CacheLoader/CacheWriterが保持している情報も、空
        assertThat(cacheLoader.getStore()).isEmpty();
        assertThat(cacheLoader.getCallerThreadNames()).isEmpty();

        assertThat(cacheWriter.getStore()).isEmpty();
        assertThat(cacheWriter.getDeleteCallerThreadNames()).isEmpty();
        assertThat(cacheWriter.getWriteCallerThreadNames()).isEmpty();

        cache.close();
        cacheManager.close();
        provider.close();
    }

この場合、CacheLoaderは呼び出されず、Cache#getを呼び出しても何も起こりません。MutableConfiguration#setReadThroughをtrueにしていないからです。

なお、今回は先ほど書いたように、結果確認のためCacheLoader/CacheWriterのインスタンスを登録したいため、このような設定を行いました。

        // CacheLoader/CacheWriterは設定する
        Configuration<String, String> configuration
            = new MutableConfiguration<String, String>()
            .setCacheLoaderFactory(FactoryBuilder.factoryOf(cacheLoader))
            .setCacheWriterFactory(FactoryBuilder.factoryOf(cacheWriter));

FactoryBuilder.factoryOfには3種類の定義があって、それぞれ以下の引数を取るものがあります。

  • Classクラス
  • String(クラス名を表す文字列)
  • 実装クラスのインスタンス(ただし、Serializableであること)

今回は最後のを使いましたが、他の2つの場合にCacheLoader/CacheWriterの実装がSerializableである必要があるかどうかまでは確認していません…。

というわけで、続いてRead Throughを明示的に有効化して確認します。

    @Test
    public void readThroughTest() {
        CachingProvider provider = Caching.getCachingProvider();
        CacheManager cacheManager = provider.getCacheManager();

        SimpleCacheLoader cacheLoader = new SimpleCacheLoader();
        SimpleCacheWriter cacheWriter = new SimpleCacheWriter();

        // Read Throughをオンにし、CacheLoader/CacheWriterを設定する
        Configuration<String, String> configuration
            = new MutableConfiguration<String, String>()
            .setReadThrough(true)
            .setCacheLoaderFactory(FactoryBuilder.factoryOf(cacheLoader))
            .setCacheWriterFactory(FactoryBuilder.factoryOf(cacheWriter));

        Cache<String, String> cache
            = cacheManager.createCache("simpleCache", configuration);

        assertThat(configuration.isReadThrough()).isTrue();
        assertThat(configuration.isWriteThrough()).isFalse();

        // Reath Throughが有効なので、Cache#getの裏でCacheLoaderが呼ばれるようになる
        assertThat(cache.get("key1")).isEqualTo("value1");
        assertThat(cache.get("key2")).isEqualTo("value2");
        assertThat(cache.get("key3")).isEqualTo("value3");

        // CacheLoaderで、同じスレッドでキャッシュエントリがロードされたことが
        // 確認できる
        assertThat(cacheLoader.getStore()).hasSize(3);
        assertThat(cacheLoader.getCallerThreadNames())
            .contains(MapEntry.entry("key1", Thread.currentThread().getName()),
                      MapEntry.entry("key2", Thread.currentThread().getName()),
                      MapEntry.entry("key3", Thread.currentThread().getName()));

        // CacheWriterが保持するデータは、空のまま
        assertThat(cacheWriter.getStore()).isEmpty();
        assertThat(cacheWriter.getDeleteCallerThreadNames()).isEmpty();
        assertThat(cacheWriter.getWriteCallerThreadNames()).isEmpty();

        cache.close();
        cacheManager.close();
        provider.close();
    }

今度は、CacheLoaderが動作したことが確認できます。

Read Throughを有効にしているのは、ここですね。

        // Read Throughをオンにし、CacheLoader/CacheWriterを設定する
        Configuration<String, String> configuration
            = new MutableConfiguration<String, String>()
            .setReadThrough(true)
            .setCacheLoaderFactory(FactoryBuilder.factoryOf(cacheLoader))
            .setCacheWriterFactory(FactoryBuilder.factoryOf(cacheWriter));

続いて、CacheWriterの確認を。

    @Test
    public void defaultWriterTest() {
        CachingProvider provider = Caching.getCachingProvider();
        CacheManager cacheManager = provider.getCacheManager();

        SimpleCacheLoader cacheLoader = new SimpleCacheLoader();
        SimpleCacheWriter cacheWriter = new SimpleCacheWriter();

        // CacheLoader/CacheWriterは設定する
        Configuration<String, String> configuration
            = new MutableConfiguration<String, String>()
            .setCacheLoaderFactory(FactoryBuilder.factoryOf(cacheLoader))
            .setCacheWriterFactory(FactoryBuilder.factoryOf(cacheWriter));

        Cache<String, String> cache
            = cacheManager.createCache("simpleCache", configuration);

        // Read Through/Write Throughは、共にオフ
        assertThat(configuration.isReadThrough()).isFalse();
        assertThat(configuration.isWriteThrough()).isFalse();

        // データを登録する
        cache.put("key1", "value1");
        cache.put("key2", "value2");
        cache.put("key3", "value3");

        // CacheLoader側は、空
        assertThat(cacheLoader.getStore()).isEmpty();
        assertThat(cacheLoader.getCallerThreadNames()).isEmpty();

        // CacheWriter側は、呼び出しが行われている
        assertThat(cacheWriter.getStore()).hasSize(3);
        // 削除オペレーションは、行われていない
        assertThat(cacheWriter.getDeleteCallerThreadNames()).isEmpty();
        // 書き込みオペレーションが、同じスレッドで行われている
        assertThat(cacheWriter.getWriteCallerThreadNames())
            .contains(MapEntry.entry("key1", Thread.currentThread().getName()),
                      MapEntry.entry("key2", Thread.currentThread().getName()),
                      MapEntry.entry("key3", Thread.currentThread().getName()));

        // データを削除してみる
        cache.remove("key1");
        cache.remove("key2");
        cache.remove("key3");

        // CacheLoader側は、空
        assertThat(cacheLoader.getStore()).isEmpty();
        assertThat(cacheLoader.getCallerThreadNames()).isEmpty();

        // ストアのデータは空になり、deleteの呼び出しが追加されている
        assertThat(cacheWriter.getStore()).isEmpty();
        assertThat(cacheWriter.getDeleteCallerThreadNames())
            .contains(MapEntry.entry("key1", Thread.currentThread().getName()),
                      MapEntry.entry("key2", Thread.currentThread().getName()),
                      MapEntry.entry("key3", Thread.currentThread().getName()));
        assertThat(cacheWriter.getWriteCallerThreadNames())
            .contains(MapEntry.entry("key1", Thread.currentThread().getName()),
                      MapEntry.entry("key2", Thread.currentThread().getName()),
                      MapEntry.entry("key3", Thread.currentThread().getName()));

        cache.close();
        cacheManager.close();
        provider.close();
    }

なぜか、Write Throughを有効にしていないのにCacheWriterが呼び出されましたが…。今のInfinispanの実装は、事実上デフォルトでWrite Throughになっちゃってる??

なので、Write Throughを明示的に有効にしても結果は同じでした。

    @Test
    public void writeThroughTest() {
        CachingProvider provider = Caching.getCachingProvider();
        CacheManager cacheManager = provider.getCacheManager();

        SimpleCacheLoader cacheLoader = new SimpleCacheLoader();
        SimpleCacheWriter cacheWriter = new SimpleCacheWriter();

        // Write Throughをオンにし、CacheLoader/CacheWriterを設定する
        Configuration<String, String> configuration
            = new MutableConfiguration<String, String>()
            .setWriteThrough(true)
            .setCacheLoaderFactory(FactoryBuilder.factoryOf(cacheLoader))
            .setCacheWriterFactory(FactoryBuilder.factoryOf(cacheWriter));

        Cache<String, String> cache
            = cacheManager.createCache("simpleCache", configuration);

        // Read Throughはオフ、Write Throughはオン
        assertThat(configuration.isReadThrough()).isFalse();
        assertThat(configuration.isWriteThrough()).isTrue();

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

        // 動きが、Write Throughオフ時と変わらない…
        assertThat(cacheLoader.getStore()).isEmpty();
        assertThat(cacheLoader.getCallerThreadNames()).isEmpty();

        assertThat(cacheWriter.getStore()).hasSize(3);
        assertThat(cacheWriter.getDeleteCallerThreadNames()).isEmpty();
        assertThat(cacheWriter.getWriteCallerThreadNames())
            .contains(MapEntry.entry("key1", Thread.currentThread().getName()),
                      MapEntry.entry("key2", Thread        // Read Throughをオンにし、CacheLoader/CacheWriterを設定する
        Configuration<String, String> configuration
            = new MutableConfiguration<String, String>()
            .setReadThrough(true)
            .setCacheLoaderFactory(FactoryBuilder.factoryOf(cacheLoader))
            .setCacheWriterFactory(FactoryBuilder.factoryOf(cacheWriter));.currentThread().getName()),
                      MapEntry.entry("key3", Thread.currentThread().getName()));

        cache.remove("key1");
        cache.remove("key2");
        cache.remove("key3");

        assertThat(cacheLoader.getStore()).isEmpty();
        assertThat(cacheLoader.getCallerThreadNames()).isEmpty();

        assertThat(cacheWriter.getStore()).isEmpty();
        assertThat(cacheWriter.getDeleteCallerThreadNames())
            .contains(MapEntry.entry("key1", Thread.currentThread().getName()),
                      MapEntry.entry("key2", Thread.currentThread().getName()),
                      MapEntry.entry("key3", Thread.currentThread().getName()));
        assertThat(cacheWriter.getWriteCallerThreadNames())
            .contains(MapEntry.entry("key1", Thread.currentThread().getName()),
                      MapEntry.entry("key2", Thread.currentThread().getName()),
                      MapEntry.entry("key3", Thread.currentThread().getName()));

        cache.close();
        cacheManager.close();
        provider.close();
    }

一応こちらについても書いておくと、以下のようにしてWrite Throughを有効にして

        // Write Throughをオンにし、CacheLoader/CacheWriterを設定する
        Configuration<String, String> configuration
            = new MutableConfiguration<String, String>()
            .setWriteThrough(true)
            .setCacheLoaderFactory(FactoryBuilder.factoryOf(cacheLoader))
            .setCacheWriterFactory(FactoryBuilder.factoryOf(cacheWriter));

Cache#putやCache#removeに対して、CacheWriterが呼び出されていることを確認しています。そして、呼び出し元スレッドと同じスレッドでCacheWriterが動作していることも(スレッド名からですが)確認しました。

PFDとはいえ、1.0.0が付いてるところまで来ているので、Read Through/Write Throughについてはほぼこのままなのかなぁ?と思います。