CLOVER🍀

That was when it all began.

HazelcastでCache(JCache)の設定をする

Hazelcast 3.4から、設定ファイルでCacheの設定ができるようになりました。これにより、JCacheの設定がJCacheのMutableConfigurationではなく、Hazelcastの設定ファイルで定義できるようになります。

JCache Configuration
http://docs.hazelcast.org/docs/3.4/manual/html-single/hazelcast-documentation.html#jcache-configuration

設定できるのは、このあたりです。

  • 定義するCacheの名前
  • キーのClass名
  • 値のClass名
  • JMX統計情報収集、提供の有効/無効
  • Read-Through、Write-Throughの有効/無効
  • CacheLoader、CacheWriter、ExpirePolicyに対するファクトリの設定
  • EntryListenerの設定

また、HazelcastのCacheの設定として、以下も可能なようです。

ICache Configuration
http://docs.hazelcast.org/docs/3.4/manual/html-single/hazelcast-documentation.html#icache-configuration

こっちは、「I」Cacheですね。バックアップ数や、メモリ内でのエントリの持ち方(シリアライズ or 参照)、Evictionを設定します。

で、これらの一部をHazelcast 3.4で軽く試そうと思っていたのですが、バグがあったりしてうまく動かなかったので、最近リリースされた3.4.1まで待っていました。

まあ、これでも不思議なところはありますが。

では、使っていってみましょう。

Maven依存関係

Maven依存関係の定義で必要なのは、JCacheとHazelcast自身ですね。

    <dependency>
      <groupId>javax.cache</groupId>
      <artifactId>cache-api</artifactId>
      <version>1.0.0</version>
    </dependency>
    <dependency>
      <groupId>com.hazelcast</groupId>
      <artifactId>hazelcast</artifactId>
      <version>3.4.1</version>
    </dependency>

今回は、Server Providerを使用します。

Hazelcastの設定ファイルで、JCacheの設定をする

設定ファイルの用意

それでは、Hazelcastの設定ファイルを作成します。
src/test/resources/hazelcast.xml

<?xml version="1.0" encoding="UTF-8"?>
<hazelcast xsi:schemaLocation="http://www.hazelcast.com/schema/config hazelcast-config-3.4.xsd"
           xmlns="http://www.hazelcast.com/schema/config"
           xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
  <group>
    <name>my-cluster</name>
    <password>my-cluster-password</password>
  </group>

  <network>
    <port auto-increment="true" port-count="100">5701</port>

    <join>
      <multicast enabled="true">
        <multicast-group>224.2.2.3</multicast-group>
        <multicast-port>54327</multicast-port>
      </multicast>
      <tcp-ip enabled="false" />
    </join>
  </network>

  <cache name="simple-cache">
    <key-type class-name="java.lang.String" />
    <value-type class-name="java.lang.String" />
  </cache>

  <cache name="with-eviction-cache">
    <key-type class-name="java.lang.String" />
    <value-type class-name="java.lang.String" />

    <eviction size="5" max-size-policy="ENTRY_COUNT" eviction-policy="LRU" />
  </cache>

  <cache name="with-expire-cache">
    <key-type class-name="java.lang.String" />
    <value-type class-name="java.lang.String" />

    <eviction size="5" max-size-policy="ENTRY_COUNT" eviction-policy="LRU" />
    <expiry-policy-factory class-name="org.littlewings.hazelcast.jcache.MyExpiryFactory" />
  </cache>
</hazelcast>

簡単ですが、キーと値のClassだけを定義したCache、Evictionの設定をしたCache、EvictionとExpireの設定をしたCacheの3つを用意しました。

注)
max-size-policyはドキュメントのXML例はENTRY-COUNTですが、正しいのはENTRY_COUNTです。Pull Req送ったりしたり、開発者に指摘してGitHub master上は直っているので、そのうち反映されるでしょう。

このファイルですが、「hazelcast.xml」という名前でクラスパス上にあることが重要です(ClientProviderの場合は、hazelcast-client.xml)。要はHazelcastの設定ファイルのデフォルトの名前なのですが。

なお、ExpiryFactoryは自分で実装しました。このようなものを用意しています。
src/main/java/org/littlewings/hazelcast/jcache/MyExpiryFactory.java

package org.littlewings.hazelcast.jcache;

import java.util.concurrent.TimeUnit;

import javax.cache.configuration.Factory;
import javax.cache.expiry.Duration;
import javax.cache.expiry.ExpiryPolicy;
import javax.cache.expiry.TouchedExpiryPolicy;

public class MyExpiryFactory implements Factory<ExpiryPolicy> {
    @Override
    public ExpiryPolicy create() {
        return new TouchedExpiryPolicy(new Duration(TimeUnit.SECONDS, 5));
    }
}

JCacheのFactoryを実装したクラスで、ここではExpiryPolicyを返すようにしています。PolicyはTouched(Cacheへのアクセス、エントリの更新で有効期限が延びる)、有効期限は5秒としました。

使ってみる

それでは、テストコードを書いて試していってみましょう。ここからのコードは、以下のimport文が書かれているものとします。

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

import java.util.concurrent.TimeUnit;
import java.util.stream.IntStream;

import javax.cache.Cache;
import javax.cache.CacheManager;
import javax.cache.Caching;
import javax.cache.spi.CachingProvider;

import org.junit.Test;

キーと値のClassクラスを定義したCacheの利用。

        CachingProvider provider = Caching.getCachingProvider();
        CacheManager manager = provider.getCacheManager();
        Cache<String, String> cache = manager.getCache("simple-cache",
                                                       String.class,
                                                       String.class);

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

        assertThat(cache.get("key1"))
            .isEqualTo("value1");
        assertThat(cache.get("key5"))
            .isEqualTo("value5");
        assertThat(cache.get("key10"))
            .isNull();

        cache.close();
        manager.close();
        provider.close();

通常、JCacheを使う時はMutableConfigurationを使ってCacheManager#createCacheしてCacheを定義するのですが、ここでは事前定義済みのCacheを取得できています。

        Cache<String, String> cache = manager.getCache("simple-cache",
                                                       String.class,
                                                       String.class);

もしも定義できていなかった場合は、ここでの戻り値はnullになります。キーと値の型を特定しなかったりすると、例外が飛ぶようですが…。

Evictionの確認。

        CachingProvider provider = Caching.getCachingProvider();
        CacheManager manager = provider.getCacheManager();
        Cache<String, String> cache = manager.getCache("with-eviction-cache",
                                                       String.class,
                                                       String.class);

        com.hazelcast.cache.ICache<String, String> icache =
            cache.unwrap(com.hazelcast.cache.ICache.class);
        com.hazelcast.config.CacheConfig config =
            icache.getConfiguration(com.hazelcast.config.CacheConfig.class);

        assertThat(config.getEvictionConfig().getSize())
            .isEqualTo(5);  // right

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

        int count = 0;
        for (Cache.Entry<String, String> entry : cache) {
            count++;
        }

        assertThat(count)
            .isGreaterThan(25);  // much larger than ENTRY_COUNT??

        cache.close();
        manager.close();
        provider.close();

なんか、普通に要素数がオーバーしていますね…。なんか使い方悪いのかなぁ…。しきい値から多少ブレるくらいだったら、そんなものかなぁと思うのですが。これはちょっとまた後で。

続いて、Expireの確認。

        CachingProvider provider = Caching.getCachingProvider();
        CacheManager manager = provider.getCacheManager();
        Cache<String, String> cache = manager.getCache("with-expire-cache",
                                                       String.class,
                                                       String.class);

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

        TimeUnit.SECONDS.sleep(3);

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

        TimeUnit.SECONDS.sleep(3);

        assertThat(cache.get("key1"))
            .isEqualTo("value1");
        assertThat(cache.get("key3"))
            .isEqualTo("value3");
        assertThat(cache.get("key2"))
            .isNull();
        assertThat(cache.get("key4"))
            .isNull();

        TimeUnit.SECONDS.sleep(6);

        assertThat(cache.get("key1"))
            .isNull();
        assertThat(cache.get("key3"))
            .isNull();

        cache.close();
        manager.close();
        provider.close();

こちらは、違和感なく使えてそうです。

で、先ほどのEvictionなのですが、ここを見る限りExpireと組み合わせた方がわかりやすい動きするのかなぁと思って試してみたのですが。

JCache Eviction
http://docs.hazelcast.org/docs/3.4/manual/html-single/hazelcast-documentation.html#jcache-eviction

Expireと組み合わせて確認。

        CachingProvider provider = Caching.getCachingProvider();
        CacheManager manager = provider.getCacheManager();
        Cache<String, String> cache = manager.getCache("with-expire-cache",
                                                       String.class,
                                                       String.class);

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

        TimeUnit.SECONDS.sleep(3);

        cache.get("key1");
        cache.get("key3");
        cache.get("key5");
        cache.get("key10");
        cache.get("key13");
        cache.get("key15");
        cache.get("key20");
        cache.get("key25");

        TimeUnit.SECONDS.sleep(3);

        int count = 0;
        for (Cache.Entry<String, String> entry : cache) {
            count++;
        }

        assertThat(count)
            .isLessThanOrEqualTo(8);

        cache.close();
        manager.close();
        provider.close();

そんなに動きが変わった気がしません…。どうなのだろう??

「hazelcast.xml」以外の名前で設定ファイルを指定する

上記までのコードでは、デフォルトのHazelcastの設定ファイルを読むため、「hazelcast.xml」(ClientProviderの場合は「hazelcast-client.xml」)の設定ファイル名がどこにも登場しませんが、これを設定することもできるようです。

Scopes and Namespaces
http://docs.hazelcast.org/docs/3.4/manual/html-single/hazelcast-documentation.html#scopes-and-namespaces

3.3.1の時から、ホントにだいぶ変わりましたねぇ。

Hazelcastの設定ファイル名をデフォルト以外の名前にするには、java.util.Propertiesを使うようです。

今度は、先ほどと同じ設定ファイルを、「my-hazelcast.xml」という名前で作ってみます。
src/test/resources/my-hazelcast.xml

<?xml version="1.0" encoding="UTF-8"?>
<hazelcast xsi:schemaLocation="http://www.hazelcast.com/schema/config hazelcast-config-3.4.xsd"
           xmlns="http://www.hazelcast.com/schema/config"
           xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
  <group>
    <name>my-cluster</name>
    <password>my-cluster-password</password>
  </group>

  <network>
    <port auto-increment="true" port-count="100">5701</port>

    <join>
      <multicast enabled="true">
        <multicast-group>224.2.2.3</multicast-group>
        <multicast-port>54327</multicast-port>
      </multicast>
      <tcp-ip enabled="false" />
    </join>
  </network>

  <cache name="simple-cache">
    <key-type class-name="java.lang.String" />
    <value-type class-name="java.lang.String" />
  </cache>

  <cache name="with-eviction-cache">
    <key-type class-name="java.lang.String" />
    <value-type class-name="java.lang.String" />

    <eviction size="5" max-size-policy="ENTRY_COUNT" eviction-policy="LRU" />
  </cache>

  <cache name="with-expire-cache">
    <key-type class-name="java.lang.String" />
    <value-type class-name="java.lang.String" />

    <eviction size="5" max-size-policy="ENTRY_COUNT" eviction-policy="LRU" />
    <expiry-policy-factory class-name="org.littlewings.hazelcast.jcache.MyExpiryFactory" />
  </cache>
</hazelcast>

また、テストコードのimport文には以下を追加します。

import com.hazelcast.cache.HazelcastCachingProvider;

先ほどのテストコードから変わるところですが、まずHazelcastCachingProvider.HAZELCAST_CONFIG_LOCATIONをキーに、設定ファイルの場所を値にしたPropertiesを作成します。

        Properties properties = new Properties();
        properties.setProperty(HazelcastCachingProvider.HAZELCAST_CONFIG_LOCATION,
                               "classpath:my-hazelcast.xml");

設定ファイルの場所の指定は、クラスパス(classpath)、そしてファイルシステム(file:///)のようにURIとして渡せるものが利用できます。

クラスパスの指定ですが、ドキュメント上は「classpath://」と書かれていますが、「//」があると動きませんでしたけれど…。

また、これの省略形として以下の記述もOKです。

        Properties properties =
            HazelcastCachingProvider.propertiesByLocation("classpath:my-hazelcast.xml");

次に、CacheManagerの取得方法が変わります。作成したPropertiesを、CachingProvider#getCacheCacheManagerの第3引数に渡します。

        CachingProvider provider = Caching.getCachingProvider();
        CacheManager manager = provider.getCacheManager(URI.create("my-cache-manager"),
                                                        null,
                                                        properties);

ここで、CachingProvider#getCacheCacheManagerの第1引数はCacheManagerの管理スコープを分けるためのURI、第2引数はClassLoaderで、それぞれnullを指定するとデフォルトのものを使おうとします。

ただ、このように設定ファイルを自分で指定する使い方の場合は、URIを指定することがすごく重要でこれを指定しないと指定した設定ファイルそのものは認識するのですが、その後でデフォルト設定のHazelcastインスタンスを起動しようとします…。

同一JavaVMにHazelcastインスタンスが2つ起動しようとする状態になって、さらにCacheManager#getCacheする時は自分が指定された設定ファイルで構築されたHazelcastインスタンスは使われません。ここ、最初にハマりました…。なお、CachingProvider#getDefaultURIで取得できる値を渡しても、たぶんHazelcastインスタンスが2つ起動する結果になる(nullを渡した時と同じになる)と思います。

ここまで書いておいて注意ですが、この話は「HazelcastのJCache実装での挙動」のみに言及しています。他のJCacheの実装では、事情が異なると思います。

あとは、普通に使うだけです。

Propertiesから作成する例。

        Properties properties = new Properties();
        properties.setProperty(HazelcastCachingProvider.HAZELCAST_CONFIG_LOCATION,
                               "classpath:my-hazelcast.xml");

        CachingProvider provider = Caching.getCachingProvider();
        CacheManager manager = provider.getCacheManager(URI.create("my-cache-manager"),
                                                        null,
                                                        properties);
        Cache<String, String> cache = manager.getCache("simple-cache",
                                                       String.class,
                                                       String.class);

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

        assertThat(cache.get("key1"))
            .isEqualTo("value1");
        assertThat(cache.get("key5"))
            .isEqualTo("value5");
        assertThat(cache.get("key10"))
            .isNull();

        cache.close();
        manager.close();
        provider.close();

HazelcastCachingProvider#propertiesByLocationを使用した例。

        Properties properties =
            HazelcastCachingProvider.propertiesByLocation("classpath:my-hazelcast.xml");

        CachingProvider provider = Caching.getCachingProvider();
        CacheManager manager = provider.getCacheManager(URI.create("my-cache-manager"),
                                                        null,
                                                        properties);

        Cache<String, String> cache = manager.getCache("with-expire-cache",
                                                       String.class,
                                                       String.class);

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

        TimeUnit.SECONDS.sleep(3);

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

        TimeUnit.SECONDS.sleep(3);

        assertThat(cache.get("key1"))
            .isEqualTo("value1");
        assertThat(cache.get("key3"))
            .isEqualTo("value3");
        assertThat(cache.get("key2"))
            .isNull();
        assertThat(cache.get("key4"))
            .isNull();

        TimeUnit.SECONDS.sleep(6);

        assertThat(cache.get("key1"))
            .isNull();
        assertThat(cache.get("key3"))
            .isNull();

        cache.close();
        manager.close();
        provider.close();

今回のコード例では、CacheManagerを取得しているところを変えれば「hazelcast.xml」を使っていた時のコードがそのまま動きます。設定ファイルの置き場所を変えただけなので、そりゃあそうだという話ですが。

ただ、挙動がわかりにくかったりするのですが…どうなんでしょう?

今回作成したコードは、こちらに置いています。

https://github.com/kazuhira-r/hazelcast-examples/tree/master/hazelcast-jcache-configuration