OSSになったCoherence Community Editionでリードスルーとライトビハインドを試す

# はじめに

インメモリデータグリッドを構成するキャッシュ製品であるOracle Coherenceのオープンソース版であるCoherence Community Editionがリリースされました。

- [Announcing Coherence Community Edition @ OracleBlog](https://blogs.oracle.com/oraclecoherence/announcing-coherence-community-edition)
- [Coherence Community](https://coherence.community/)
- [GitHub oracle/coherence](https://github.com/oracle/coherence)

Oracle Coherenceは会社の同じ部署の人がかなり昔に扱っていただけで個人的には触れたことはないのですが、当時から高速性、DBとの連携、トランザクションなどを売りにしていたと記憶しています。

本記事ではDBとの連携のうち、リードスルー（Read Through）とライトビハインド（Write Behind）についてCommunity Editionで試した内容を記述します。（ちなみになぜこの機能を試そうと思ったかというと、昔のCoherence Standard Editionではライトビハインドは使えなかったからです。後で調べたら、最新のStandard Editionでは使えるそうなのですが…）

ちなみに、リードスルーとライトビハインドを雑に説明すると以下になります。

| 機能 | 説明 |
|:--|:--|
| リードスルー（Read Through）  | 必要なデータがキャッシュ上にない場合、背後でDBへアクセスして取得したデータをアプリケーションに返す。その際に取得したデータはキャッシュし、2回目以降のアクセスはDBにアクセスせずにキャッシュ上のデータを返す。  |
| ライトビハインド（Write Behind）  | アプリケーションがキャッシュ上のデータを更新した場合に、背後のDBにも「非同期」に変更を反映させる。  |

環境はVirtualBox上のCentOS 8.2×1台で試しています。

ちなみに短い時間でマニュアルを断片的に斜め読みしながら確認した手順なので、適切でない設定も多いですが、その点はご容赦ください。

# インストール

Mavenでダウンロードするので、まずrootユーザーでMavenをインストールします。

```
# dnf install maven
```

その後、Coherenceを起動するユーザー（gridユーザーとしました）でCoherence Community Editionをダウンロードします。また、今回はCoherenceのバックにMySQL 8.0.17を使うのでJDBCドライバのConnector/Jもダウンロードします。

```
$ mvn -DgroupId=com.oracle.coherence.ce -DartifactId=coherence -Dversion=20.06 dependency:get
$ mvn -DgroupId=mysql -DartifactId=mysql-connector-java -Dversion=8.0.17 dependency:get
```

これでインストールは完了ですが、後のことを考えて環境変数を設定しておきます。

```
export COH_JAR=$HOME/.m2/repository/com/oracle/coherence/ce/coherence/20.06/coherence-20.06.jar
export COH_CONFIG=$HOME/config
export C4J_JAR=$HOME/.m2/repository/mysql/mysql-connector-java/8.0.17/mysql-connector-java-8.0.17.jar
```

OSSになると外部リポジトリから自動で取得できるのでとても楽ですね。

# 設定ファイルの作成

$COH_CONFIGディレクトリに設定ファイルを作成します。

まずはクラスタ構成ファイルです。これはファイル名固定です。ファイルのサンプルは[こちら](https://docs.oracle.com/cd/F32751_01/coherence/14.1.1.0/develop-applications/building-your-first-coherence-application.html#GUID-F740BD4F-6D3F-4CEA-BA22-11B0D8BB468B)にありますが、以下の3点を変更しておきます。

- クラスタ名 (my-cluster)
- 利用するマルチキャストIPアドレス (224.0.0.1) ※今回は使いませんが
- キャッシュ定義ファイルの名前 (my-config.xml)

```tangosol-coherence-override.xml
<?xml version='1.0'?>

<coherence
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xmlns="http://xmlns.oracle.com/coherence/coherence-operational-config"
  xsi:schemaLocation="http://xmlns.oracle.com/coherence/coherence-operational-config
  coherence-operational-config.xsd">

  <cluster-config>
    <member-identity>
      <cluster-name>my-cluster</cluster-name>
    </member-identity>
    <multicast-listener>
      <address>224.0.0.1</address>
      <time-to-live>0</time-to-live>
    </multicast-listener>
  </cluster-config>

  <configurable-cache-factory-config>
    <init-params>
      <init-param>
        <param-type>java.lang.String</param-type>
        <param-value system-property="coherence.cacheconfig">
          my-config.xml
        </param-value>
      </init-param>
    </init-params>
  </configurable-cache-factory-config>
</coherence>
```

次に、クラスタ構成ファイル内で指定したキャッシュ定義ファイルを作成します。ポイントは以下の3点です。

- CacheStore実装ファイル (mypkg.MyCacheStore)
- ライトビハインドのタイミング (\<write-delay\> 今回は1分ごとにDBに変更を反映)
- 自動起動 (\<autostart\>)

```my-config.xml
<?xml version="1.0"?>

<cache-config
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xmlns="http://xmlns.oracle.com/coherence/coherence-cache-config"
  xsi:schemaLocation="http://xmlns.oracle.com/coherence/coherence-cache-config
  coherence-cache-config.xsd">

  <caching-scheme-mapping>
    <cache-mapping>
      <cache-name>my-cache</cache-name>
      <scheme-name>distributed-rwbm</scheme-name>
    </cache-mapping>
  </caching-scheme-mapping>

  <caching-schemes>
    <distributed-scheme>
      <scheme-name>distributed-rwbm</scheme-name>
      <service-name>DistributedCache</service-name>
      <backing-map-scheme>
        <read-write-backing-map-scheme>
          <internal-cache-scheme>
            <local-scheme/>
          </internal-cache-scheme>
          <cachestore-scheme>
            <class-scheme>
              <class-name>mypkg.MyCacheStore</class-name>
              <init-params>
                <init-param>
                  <param-type>java.lang.String</param-type>
                  <param-value>{cache-name}</param-value>
                </init-param>
              </init-params>
            </class-scheme>
          </cachestore-scheme>
          <write-delay>1m</write-delay>
        </read-write-backing-map-scheme>
      </backing-map-scheme>
      <autostart>true</autostart>
    </distributed-scheme>
  </caching-schemes>
</cache-config>
```

# CacheStore実装

CacheStoreはCoherenceにおけるデータ永続化の役割を果たします。リードスルーやライトビハインドを実現するためには、キャッシュ定義ファイルで指定したCacheStoreの実装クラスを作成します。

CacheStore実装クラスでは以下の6つのメソッドをオーバーライドする必要があります。

- load （1件取得時のDBアクセス）
- loadAll （複数件取得時のDBアクセス）
- store （1件更新時のDBアクセス）
- storeAll （複数件取得時のDBアクセス）
- erase （1件削除時のDBアクセス） ※今回は不要のため例外を投げるのみ
- eraseAll （複数件削除時のDBアクセス）　※今回は不要のため例外を投げるのみ

```mypkg/MyCacheStore.java
package mypkg;

import com.tangosol.net.cache.CacheStore;
import com.tangosol.util.Base;

import java.util.Collection;
import java.util.Map;
import java.util.HashMap;

import java.sql.DriverManager;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;

public class MyCacheStore extends Base implements CacheStore {

    private static String username = "db_user";
    private static String password = "********";
    private static String url = "jdbc:mySQL://localhost/mydb";

    private static String selectSQL =
        "SELECT `value` FROM `mydata` WHERE `key` = ?";
    private static String replaceSQL =
        "REPLACE INTO `mydata` VALUES (?, ?)";

    private static Connection conn;

    public MyCacheStore(String cacheName) {
        super();
        try {
            conn = DriverManager.getConnection(url, username, password);
        } catch(Exception e) {
            throw new RuntimeException("fail to connect.", e);
        }
    }

    public Object load(Object key) {
        try {
            PreparedStatement stmt =
                conn.prepareStatement(selectSQL);
            stmt.setString(1, key.toString());

            ResultSet set = stmt.executeQuery();
            set.next();
            String value = set.getString(1);

            set.close();
            stmt.close();
            return value;

        } catch(Exception e) {
            throw new RuntimeException("fail to select.", e);
        }
    }

    public Map loadAll(Collection keys) {
        HashMap map = new HashMap();
        keys.forEach(key -> {map.put(key, load(key));});

        return map;
    }

    public void store(Object key, Object value) {
        try {
            PreparedStatement stmt =
                conn.prepareStatement(replaceSQL);
            stmt.setString(1, key.toString());
            stmt.setString(2, value.toString());

            stmt.executeUpdate();

            stmt.close();

        } catch(Exception e) {
            throw new RuntimeException("fail to replace.", e);
        }
    }

    public void storeAll(Map entries) {
        entries.forEach((key, value) -> {store(key, value);});
    }

    public void erase(Object key) {
        throw new UnsupportedOperationException();
    }

    public void eraseAll(Collection keys) {
        throw new UnsupportedOperationException();
    }
}
```

今回は簡略な実装にしていますが、実際には以下のような考慮が必要です。

- 同時アクセスを可能にするためにDB接続にコネクションプールを用いる
- loadAll/storeAllは性能向上のためにバルクSELECT/バルク更新を用いる

# DB上のテーブル作成

Coherenceと連携するDB上のテーブルを作成します。

```
CREATE TABLE `mydata` (
    `key` VARCHAR(5) PRIMARY KEY,
    `value` VARCHAR(10)
);
INSERT INTO `mydata` VALUES
(1, 'Tanaka'),
(2, 'Suzuki');
```
# キャッシュサーバーの起動と動作確認

ここまでくるとCoherenceをリードスルーとライトビハインドを有効にして起動できます。

まず、キャッシュサーバーの起動です。以下を実行後「Started DefaultCacheServer...」というメッセージが出力されれば起動完了です。

- クラスパスではCoherenceのJARファイル、Connector/JのJARファイル、CacheStoreの実装クラスへのパス（/home/grid/myCS）を通します。
- 今回の環境はマルチキャストが使えなかったので-Dcoherence.wka=localhost でWKAを有効にします。

```
$ java -cp $COH_CONFIG:$COH_JAR:/home/grid/myCS:$C4J_JAR \
       -Dcoherence.wka=localhost \
       -Dtangosol.coherence.distributed.localstorage=true \
       -Dcoherence.distributed.localstorage=true 
       com.tangosol.net.DefaultCacheServer
```

次にキャッシュを検索・更新するために、コマンドラインツールを起動します。

```
$ java -cp $COH_CONFIG:$COH_JAR
       -Dcoherence.distributed.localstorage=false
       -Dcoherence.wka=localhost
       com.tangosol.net.CacheFactory
```

コマンドラインツール上で以下の操作を実行します。「Map (...)」はプロンプトです。

```
Map (?): cache my-cache
（出力省略）
Map (my-cache): get 1
Tanaka

Map (my-cache): get 2
Suzuki

Map (my-cache): get 1
Tanaka

Map (my-cache): put 3 Sato
null
```

実行の中身は以下になります。

- cache my-cache：アクセスするキャッシュとしてmy-cacheを選択
- get 1：Key = "1"のデータを取得
- get 2：Key = "2"のデータを取得
- get 1：Key = "1"のデータを再度取得
- put 3 Sato：(Key, Value) = ("3", "Sato")のデータを登録

この処理を実行した際のMySQL側の一般クエリーログは以下になります。

```
2020-06-28T15:11:25.112057Z        12 Query     SELECT `value` FROM `mydata` WHERE `key` = '1'
2020-06-28T15:11:27.049899Z        12 Query     SELECT `value` FROM `mydata` WHERE `key` = '2'
2020-06-28T15:12:34.950153Z        12 Query     REPLACE INTO `mydata` VALUES ('3', 'Sato')
```

- Key = "1", "2"に最初にアクセスした際はDBへアクセスが発生している。
- Key = "1"に2回目にアクセスした際はDBへはアクセスしていない（Coherence上にデータがキャッシュされている）。
- putした際は、少し遅れてDBにデータ登録の処理が実行されている。

ということが分かり、リードスルー、ライドビハインドが想定通り動いているのが確認できました。

# おわりに

今回はOSSになったCoherence Commnity Editionでリードスルー、ライトビハインドの動作を確認してみました。

Webアプリの高速化の手法としてDBのデータをRedisなどでキャッシュする方法は良く利用されますが、キャッシュデータとDBデータの整合性の取り方が一つ課題になるケースがあります。そういったケースでリードスルーやライトビハインドを備えたCoheence Community Editionが使えるのかもしれません。（それ以外にも高速性やCQLなどメリットはありますが）。

まぁ、機能・性能だけで製品を選定するわけではないので何とも言えませんが。