前回InfinispanのRemote Storeを使ってみましたが、今度はJDBC Cache Store(JDBC based cache loaders)を試してみようと思います。
4.8. JDBC based cache loaders
http://infinispan.org/docs/6.0.x/user_guide/user_guide.html#_jdbc_based_cache_loaders
Cacheにエントリを保存した時に、JDBCを通してデータベースに永続化するCache Storeですね。
Storeの種類
JDBCを使ったCache Storeは、以下の3つがあります
名前 | 説明 |
---|---|
JdbcStringBasedStore | キーがString(もしくはStringに変換可能)の場合、をプライマリキーとして、そのまま行単位で保存するStore |
JdbcBinaryStore | キーの種類によらず、保存可能なStore。同じハッシュ値を持つキーを、同じ行に保存するようです |
JdbcMixedStore | JdbcStringBasedStoreとJdbcBinaryStoreのハイブリッドなStore |
で、どのStoreを使用すればよいかというと…。
通常、キーがString(もしくは、Stringに変換可能)であれば、JdbcStringBasedStoreを使用するのがよいようです。スループットもよいらしいです。
そうでなければ、JdbcMixedStoreまたはJdbcBinaryStoreを使用しましょうと。
Connection管理
JDBC Cache Storeを使用した場合のデータベースとのConnection管理ですが、以下の3つから選択可能なようです。
- PooledConnectionFactory … C3P0を使ったConnection Pool
- ManagedConnectionFactory … アプリケーションサーバでJNDIルックアップを使用して、Connection管理
- SimpleConnectionFactory … 単純に呼び出し毎にConnectionを確立する実装で、本番環境では非推奨
後でサンプルを書きますが、今回はSimpleConnectionFactoryを使用します。
使ってみる
それでは、JDBC Cache Storeを使用してみましょう。
データベース
適当なデータベースを用意してください。ログを見ている限り、JDBC Cache Storeがサポートしているデータベースは、以下のようです。
- MySQL
- PostgreSQL
- Derby
- Hsql
- H2
- SQLite
- DB2
- DB2_390
- Informix
- Interbase
- Firebird
- SQL Server
- Access
- Oracle
- Sybase
自分は、MySQLにしました。
設定次第ですが、あらかじめテーブルを用意しなくても一応大丈夫です。
依存関係の定義
build.sbtは、このような形で定義しました。
build.sbt
name := "infinispan-jdbc-cachestore" version := "0.0.1-SNAPSHOT" scalaVersion := "2.11.1" organization := "org.littlewings" scalacOptions ++= Seq("-Xlint", "-deprecation", "-unchecked", "-feature") incOptions := incOptions.value.withNameHashing(true) parallelExecution in Test := false libraryDependencies ++= Seq( "org.infinispan" % "infinispan-cachestore-jdbc" % "6.0.2.Final" excludeAll( ExclusionRule(organization = "org.jgroups", name = "jgroups"), ExclusionRule(organization = "org.jboss.marshalling", name = "jboss-marshalling-river"), ExclusionRule(organization = "org.jboss.marshalling", name = "jboss-marshalling"), ExclusionRule(organization = "org.jboss.logging", name = "jboss-logging"), ExclusionRule(organization = "org.jboss.spec.javax.transaction", name = "jboss-transaction-api_1.1_spec") ), "org.jgroups" % "jgroups" % "3.4.1.Final", "org.jboss.spec.javax.transaction" % "jboss-transaction-api_1.1_spec" % "1.0.1.Final", "org.jboss.marshalling" % "jboss-marshalling-river" % "1.4.4.Final", "org.jboss.marshalling" % "jboss-marshalling" % "1.4.4.Final", "org.jboss.logging" % "jboss-logging" % "3.1.2.GA", "net.jcip" % "jcip-annotations" % "1.0", "mysql" % "mysql-connector-java" % "5.1.30" % "test", "org.scalatest" %% "scalatest" % "2.1.7" % "test", "log4j" % "log4j" % "1.2.17" % "test" )
Scala/sbtやテスト、ログの都合上、いろいろくっついていますが重要なのは
"org.infinispan" % "infinispan-cachestore-jdbc" % "6.0.2.Final"
と
"mysql" % "mysql-connector-java" % "5.1.30" % "test",
ですね。
*スコープがテストになっているのは、テストコードで動作確認したからです…
Infinispanの設定
個々のCacheの設定は、後で書いていきます。ベースはこんな感じです。
src/test/resources/infinispan.xml
<?xml version="1.0" encoding="UTF-8"?> <infinispan xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="urn:infinispan:config:6.0 http://www.infinispan.org/schemas/infinispan-config-6.0.xsd" xmlns="urn:infinispan:config:6.0"> <global> <transport clusterName="JdbcCacheStoreCluster"> <properties> <property name="configurationFile" value="jgroups.xml" /> </properties> </transport> <globalJmxStatistics enabled="true" jmxDomain="org.infinispan" cacheManagerName="DefaultCacheManager" allowDuplicateDomains="true" /> <shutdown hookBehavior="REGISTER"/> </global> <default /> 〜省略〜 </infinispan>
クラスタは組みますが、JGroupsの設定は端折ります。
それでは、実際のコードを書いていってみましょう。
String以外のキー用のクラスと、値に使うクラス
先に、キー用のクラスと値用のクラスを定義しておきます。
src/test/scala/org/littlewings/infinispan/jdbccachestore/Entry.scala
package org.littlewings.infinispan.jdbccachestore import org.infinispan.persistence.keymappers.{Key2StringMapper, TwoWayKey2StringMapper} @SerialVersionUID(1L) class KeyClass(val value: String) extends Serializable { override def equals(other: Any): Boolean = other match { case o: KeyClass => value == o.value case _ => false } override def hashCode: Int = value.## } @SerialVersionUID(1L) class ValueClass(val value: String) extends Serializable { override def equals(other: Any): Boolean = other match { case o: ValueClass => value == o.value case _ => false } override def hashCode: Int = value.## }
キーは、Stringの場合とこのKeyClassというクラスを使用する場合の2パターンで試していきます。
テストコードの骨格
使用するテストコードは、こんな骨格実装とします。
src/test/scala/org/littlewings/infinispan/jdbccachestore/InfinispanJdbcCacheStoreSpec.scala
package org.littlewings.infinispan.jdbccachestore import scala.collection.JavaConverters._ import org.infinispan.Cache import org.infinispan.manager.DefaultCacheManager import org.scalatest.FunSpec import org.scalatest.Matchers._ class InfinispanJdbcCacheStoreSpec extends FunSpec { // ここに、テストコードを書く! def withCache[A, B](numInstances: Int, cacheName: String)(fun: Cache[A, B] => Unit): Unit = { val managers = (1 to numInstances).map(_ => new DefaultCacheManager("infinispan.xml")) try { val cache = managers.head.getCache[A, B](cacheName) fun(cache) } finally { for { manager <- managers cacheName <- manager.getCacheNames.asScala } { manager.getCache[Any, Any](cacheName).stop() } managers.foreach(_.stop()) } } }
JdbcStringBasedStore
それでは、JdbcStringBasedStoreから定義していってみましょう。
Cacheの定義
Cacheの定義は、以下のように2つ用意しました。それぞれ、キーがString用、キーがKeyClass用のものです。
<namedCache name="jdbcStringBasedStoreCache"> <clustering mode="dist" /> <!-- <expiration lifespan="60000" /> --> <persistence> <stringKeyedJdbcStore xmlns="urn:infinispan:config:jdbc:6.0" fetchPersistentState="false" ignoreModifications="false"> <simpleConnection connectionUrl="jdbc:mysql://localhost/test?useUnicode=true&characterEncoding=utf8" driverClass="com.mysql.jdbc.Driver" username="kazuhira" password="password" /> <stringKeyedTable prefix="ispn_string_based" createOnStart="true" dropOnExit="false"> <idColumn name="id" type="VARCHAR(10)" /> <dataColumn name="data" type="BLOB" /> <timestampColumn name="expire" type="BIGINT" /> </stringKeyedTable> </stringKeyedJdbcStore> </persistence> </namedCache> <namedCache name="jdbcStringBasedStoreCacheNoStringKey"> <clustering mode="dist" /> <persistence> <stringKeyedJdbcStore xmlns="urn:infinispan:config:jdbc:6.0" fetchPersistentState="false" ignoreModifications="false" key2StringMapper="org.littlewings.infinispan.jdbccachestore.TwoWayKeyClass2StringMapper"> <simpleConnection connectionUrl="jdbc:mysql://localhost/test?useUnicode=true&characterEncoding=utf8" driverClass="com.mysql.jdbc.Driver" username="kazuhira" password="password" /> <stringKeyedTable prefix="ispn_string_based" createOnStart="true" dropOnExit="false"> <idColumn name="id" type="VARCHAR(10)" /> <dataColumn name="data" type="BLOB" /> <timestampColumn name="expire" type="BIGINT" /> </stringKeyedTable> </stringKeyedJdbcStore> </persistence> </namedCache>
persistenceタグの中身を説明すると、
<stringKeyedJdbcStore xmlns="urn:infinispan:config:jdbc:6.0" fetchPersistentState="false" ignoreModifications="false"> <simpleConnection connectionUrl="jdbc:mysql://localhost/test?useUnicode=true&characterEncoding=utf8" driverClass="com.mysql.jdbc.Driver" username="kazuhira" password="password" /> <stringKeyedTable prefix="ispn_string_based" createOnStart="true" dropOnExit="false"> <idColumn name="id" type="VARCHAR(10)" /> <dataColumn name="data" type="BLOB" /> <timestampColumn name="expire" type="BIGINT" /> </stringKeyedTable> </stringKeyedJdbcStore>
stringKeyedJdbcStoreタグで、JdbcStringBasedStoreを使用することを表しています。simpleConnectionタグは、単純にJDBC接続の定義をしているだけですね。
stringKeyedTableタグが、実際の保存先のテーブルの定義ですが、こんな感じです。
- prefix … 作成するテーブルの接頭辞を指定する。「指定した接頭辞_Cacheの名前」が、作成されるテーブルの名前になる
- createOnStart … 起動時に、テーブルを作成する場合はtrue
- dropOnExit … 終了時に、テーブルを削除する場合はtrue
その他、fetchSizeやbatchSizeも指定することができます。
あと、idColumn、dataColumn、timestampColumnとありますが、それぞれ以下の用途で使用されます。
- idColumn … 主キー用。文字列型(VARCHARなど)で定義
- dataColumn … データ保存用。バイナリ(BLOBなど)で定義
- timestampColumn … expire用。JavaのLongが格納できる型(BIGINTなど)で定義
それぞれ、カラム名とデータ型を指定します。
JdbcBinaryStoreなどでは、カラムの使われ方がちょっと変わってきますが、概ねこんな意味のようです。
それでは、テストコードを。
describe("jdbc-cache-store stringKeyedJdbcStore Spec") { it("save data, with string-key") { val clusterSize = 3 val keysValues = (1 to 5).map(i => (s"key$i", new ValueClass(s"value$i"))) withCache[String, ValueClass](clusterSize, "jdbcStringBasedStoreCache") { cache => keysValues.foreach { case (k, v) => cache.put(k, v) } } withCache[String, ValueClass](clusterSize, "jdbcStringBasedStoreCache") { cache => keysValues.foreach { case (k, v) => cache.get(k) should be (new ValueClass(v.value)) } } val keysValues2 = (1 to 3).map(i => (s"key$i", new ValueClass(s"value${i}-v2"))) withCache[String, ValueClass](clusterSize, "jdbcStringBasedStoreCache") { cache => keysValues2.foreach { case (k, v) => cache.put(k, v) } } } it("save data, with no-string-key") { val clusterSize = 3 val keysValues = (10 to 15).map(i => (new KeyClass(s"key$i"), new ValueClass(s"value$i"))) withCache[KeyClass, ValueClass](clusterSize, "jdbcStringBasedStoreCacheNoStringKey") { cache => keysValues.foreach { case (k, v) => cache.put(k, v) } } withCache[KeyClass, ValueClass](clusterSize, "jdbcStringBasedStoreCacheNoStringKey") { cache => keysValues.foreach { case (k, v) => cache.get(k) should be (new ValueClass(v.value)) } } } }
クラスタに値を保存してシャットダウンして、その後で再度クラスタを起動して値を取得できることを確認するテストコードです。
最初の方は、update的なこともやっていますが…。
前半はStringをキーに、後半はKeyClassをキーにしています。
では、実行してみます。
> test
実行後、MySQLを確認してみると
mysql> SHOW TABLES;
このようなテーブルができています。
| ispn_string_based_jdbcStringBasedStoreCache | | ispn_string_based_jdbcStringBasedStoreCacheNoStringKey |
中身は、このように。
mysql> SELECT * FROM ispn_string_based_jdbcStringBasedStoreCache; +------+--------------------------------------------------------------------------------------------------------------------------------------+--------+ | id | data | expire | +------+--------------------------------------------------------------------------------------------------------------------------------------+--------+ | key1 | ^C^A�^Cg^Cj`^C^A�^D 4org.littlewings.infinispan.jdbccachestore.ValueClass ^A ^A value^T ^V> value1-v2^Cj^Y^C^A�^Ch����������������^Cb ^A | -1 | | key2 | ^C^A�^Cg^Cj`^C^A�^D 4org.littlewings.infinispan.jdbccachestore.ValueClass ^A ^A value^T ^V> value2-v2^Cj^Y^C^A�^Ch����������������^Cb ^A | -1 | | key3 | ^C^A�^Cg^Cj`^C^A�^D 4org.littlewings.infinispan.jdbccachestore.ValueClass ^A ^A value^T ^V> value3-v2^Cj^Y^C^A�^Ch����������������^Cb ^A | -1 | | key4 | ^C^A�^Cg^Cj]^C^A�^D 4org.littlewings.infinispan.jdbccachestore.ValueClass ^A ^A value^T ^V>^Fvalue4^Cj^Y^C^A�^Ch����������������^Cb ^A | -1 | | key5 | ^C^A�^Cg^Cj]^C^A�^D 4org.littlewings.infinispan.jdbccachestore.ValueClass ^A ^A value^T ^V>^Fvalue5^Cj^Y^C^A�^Ch����������������^Cb ^A | -1 | +------+--------------------------------------------------------------------------------------------------------------------------------------+--------+ 5 rows in set (0.00 sec)
中身バイナリなので、出してもあまり嬉しくないですが…。
KeyClassをキーにした場合は、こんな感じ。
mysql> SELECT * FROM ispn_string_based_jdbcStringBasedStoreCacheNoStringKey; +-------+------------------------------------------------------------------------------------------------------------------------------------+--------+ | id | data | expire | +-------+------------------------------------------------------------------------------------------------------------------------------------+--------+ | key10 | ^C^A�^Cg^Cj^^C^A�^D 4org.littlewings.infinispan.jdbccachestore.ValueClass ^A ^A value^T ^V>value10^Cj^Y^C^A�^Ch����������������^Cb ^A | -1 | | key11 | ^C^A�^Cg^Cj^^C^A�^D 4org.littlewings.infinispan.jdbccachestore.ValueClass ^A ^A value^T ^V>value11^Cj^Y^C^A�^Ch����������������^Cb ^A | -1 | | key12 | ^C^A�^Cg^Cj^^C^A�^D 4org.littlewings.infinispan.jdbccachestore.ValueClass ^A ^A value^T ^V>value12^Cj^Y^C^A�^Ch����������������^Cb ^A | -1 | | key13 | ^C^A�^Cg^Cj^^C^A�^D 4org.littlewings.infinispan.jdbccachestore.ValueClass ^A ^A value^T ^V>value13^Cj^Y^C^A�^Ch����������������^Cb ^A | -1 | | key14 | ^C^A�^Cg^Cj^^C^A�^D 4org.littlewings.infinispan.jdbccachestore.ValueClass ^A ^A value^T ^V>value14^Cj^Y^C^A�^Ch����������������^Cb ^A | -1 | | key15 | ^C^A�^Cg^Cj^^C^A�^D 4org.littlewings.infinispan.jdbccachestore.ValueClass ^A ^A value^T ^V>value15^Cj^Y^C^A�^Ch����������������^Cb ^A | -1 | +-------+------------------------------------------------------------------------------------------------------------------------------------+--------+ 6 rows in set (0.01 sec)
見ての通り、1レコード=1エントリです。
このKeyClassのような独自のキーを、どうやってStringに変換しているかですが、先ほどの保存先の定義の時に、KeyClassをキーにする方のstringKeyedJdbcStoreの定義はこのようになっていました。
<stringKeyedJdbcStore xmlns="urn:infinispan:config:jdbc:6.0" fetchPersistentState="false" ignoreModifications="false" key2StringMapper="org.littlewings.infinispan.jdbccachestore.TwoWayKeyClass2StringMapper">
ポイントは、key2StringMapper属性ですね。
ここで、org.infinispan.persistence.keymappersTwoWayKey2StringMapperインターフェースを実装した、キー用のクラスとStringを相互変換するクラスを定義して登録します。
class TwoWayKeyClass2StringMapper extends TwoWayKey2StringMapper { override def getKeyMapping(stringKey: String): AnyRef = new KeyClass(stringKey) override def getStringMapping(key: Any): String = key.asInstanceOf[KeyClass].value override def isSupportedType(keyType: Class[_]): Boolean = keyType == classOf[KeyClass] }
ちなみに、デフォルトではorg.infinispan.persistence.keymappers.DefaultTwoWayKey2StringMapperクラスが登録されていて、short、byte、long、int、double、float、boolean、byte配列とStringの相互変換をサポートしています。この中に、Stringも入っていますが。
あと、今回使用されず常に「-1」が入っているtimestampColumnですが、Cacheの設定に
<expiration lifespan="60000" />
のように生存期間を入れてあげると、値が入るようになります。
JdbcBinaryStore
続いて、JdbcBinaryStoreを使用してみます。まずは、Cacheの定義を。
<namedCache name="jdbcBinaryStoreCache"> <clustering mode="dist" /> <persistence> <binaryKeyedJdbcStore xmlns="urn:infinispan:config:jdbc:6.0" fetchPersistentState="true" ignoreModifications="false"> <simpleConnection connectionUrl="jdbc:mysql://localhost/test?useUnicode=true&characterEncoding=utf8" driverClass="com.mysql.jdbc.Driver" username="kazuhira" password="password" /> <binaryKeyedTable prefix="ispn_binary" createOnStart="true" dropOnExit="false"> <idColumn name="id" type="VARCHAR(255)" /> <dataColumn name="data" type="BLOB" /> <timestampColumn name="expire" type="BIGINT" /> </binaryKeyedTable> </binaryKeyedJdbcStore> </persistence> </namedCache> <namedCache name="jdbcBinaryStoreCacheNoStringKey"> <clustering mode="dist" /> <persistence> <binaryKeyedJdbcStore xmlns="urn:infinispan:config:jdbc:6.0" fetchPersistentState="true" ignoreModifications="false"> <simpleConnection connectionUrl="jdbc:mysql://localhost/test?useUnicode=true&characterEncoding=utf8" driverClass="com.mysql.jdbc.Driver" username="kazuhira" password="password" /> <binaryKeyedTable prefix="ispn_binary" createOnStart="true" dropOnExit="false"> <idColumn name="id" type="VARCHAR(255)" /> <dataColumn name="data" type="BLOB" /> <timestampColumn name="expire" type="BIGINT" /> </binaryKeyedTable> </binaryKeyedJdbcStore> </persistence> </namedCache>
stringKeyedJdbcStoreだったところがbinaryKeyedJdbcStoreに、stringKeyedTableだったところがbinaryKeyedTableになっているくらいで、定義的にはそれほど大きくは変わりません。
*fetchPersistentStateとpurgeOnStartupの両方をfalseにしたままだと警告されたので、fetchPersistentStateはtrueにしておきました…
<binaryKeyedJdbcStore xmlns="urn:infinispan:config:jdbc:6.0" fetchPersistentState="true" ignoreModifications="false">
が、格納のされ方はけっこう変わります。あと、このStoreだとキーの型を変える意味はあんまりなかったり…。
では、テストコードを。
describe("jdbc-cache-store binaryJdbcStore Spec") { it("save data, with string-key") { val clusterSize = 3 val keysValues = (1 to 5).map(i => (s"key$i", new ValueClass(s"value$i"))) withCache[String, ValueClass](clusterSize, "jdbcBinaryStoreCache") { cache => keysValues.foreach { case (k, v) => cache.put(k, v) } } withCache[String, ValueClass](clusterSize, "jdbcBinaryStoreCache") { cache => keysValues.foreach { case (k, v) => cache.get(k) should be (new ValueClass(v.value)) } } } it("save data, with no-string-key") { val clusterSize = 3 val keysValues = (10 to 15).map(i => (new KeyClass(s"key$i"), new ValueClass(s"value$i"))) withCache[KeyClass, ValueClass](clusterSize, "jdbcBinaryStoreCacheNoStringKey") { cache => keysValues.foreach { case (k, v) => cache.put(k, v) } } withCache[KeyClass, ValueClass](clusterSize, "jdbcBinaryStoreCacheNoStringKey") { cache => keysValues.foreach { case (k, v) => cache.get(k) should be (new ValueClass(v.value)) } } } }
実行すると、今度はこのようなテーブルができます。
| ispn_binary_jdbcBinaryStoreCache | | ispn_binary_jdbcBinaryStoreCacheNoStringKey |
SELECT文を投げてみると、こんな感じ。
*キーをKeyClassにした方もありますが、あまり意味がないので端折ります
mysql> SELECT * FROM ispn_binary_jdbcBinaryStoreCache; +---------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+--------+ | id | data | expire | +---------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+--------+ | 3288064 | ^C^A�^C^B >^Dkey1^Ci^Cj ^C^A�>^Dkey1^Cj]^C^A�^D 4org.littlewings.infinispan.jdbccachestore.ValueClass ^A ^A value^T ^V>^Fvalue1^Cj^Y^C^A�^Ch����������������^Cb ^A>^Dkey2^Ci^Cj ^C^A�>^Dkey2^Cj]^C^A�^D 4org.littlewings.infinispan.jdbccachestore.ValueClass ^A ^A value^T ^V>^Fvalue2^Cj^Y^C^A�^Ch����������������^Cb ^A>^Dkey5^Ci^Cj ^C^A�>^Dkey5^Cj]^C^A�^D 4org.littlewings.infinispan.jdbccachestore.ValueClass ^A ^A value^T ^V>^Fvalue5^Cj^Y^C^A�^Ch����������������^Cb ^A>^Dkey3^Ci^Cj ^C^A�>^Dkey3^Cj]^C^A�^D 4org.littlewings.infinispan.jdbccachestore.ValueClass ^A ^A value^T ^V>^Fvalue3^Cj^Y^C^A�^Ch����������������^Cb ^A>^Dkey4^Ci^Cj ^C^A�>^Dkey4^Cj]^C^A�^D 4org.littlewings.infinispan.jdbccachestore.ValueClass ^A ^A value^T ^V>^Fvalue4^Cj^Y^C^A�^Ch����������������^Cb ^A | -1 | +---------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+--------+ 1 row in set (0.00 sec)
ちょっとわかりにくいですが、5つのエントリをCacheに入れましたが1レコードになっています。また、主キーのカラムには「3288064」というプログラム中には現れていない値が入っています。
これ、binaryKeyedTableで保存した場合は、Bucketという単位で複数のエントリをまとめて行に保存するみたいなのですよね。