Hibernate 入門記 パフォチュー その6 二次キャッシュ (transactional)

ついにやってまいりました.今日は待ちに待った「14.3.5. Strategy: transactional」です.トランザクショナルですよん♪


Hibernate が取り扱う永続オブジェクトが RDBMS に格納されるトランザクショナルな情報に基づく以上,キャッシュもまたトランザクショナルであって欲しいと思うのが人情というもの.その点で,昨日までの read-write 戦略や nonstrict-read-write 戦略は,データの整合性について不安をぬぐえない感じがありました.
しかし,もう大丈夫です.我らには transactional 戦略があるのだ!
でも,例によってドキュメントにはたいした情報は書いてありません.またしても JTAトランザクションマネージャを取得するための TransactionManagerLookup の実装クラスを hibernate.transaction.manager_lookup_class というプロパティで指定しろってことくらいです.
そんな場合には,ソースを見ることが出来るのがオープンソースのいいところ♪
と思ったのですが,今回はソースを見てもたいした情報がありません.というのも,transactional 戦略の場合,面倒なことは全てキャッシュの実装がよきに計らってくれるということらしい.Hibernate 的には,トランザクションの内側でキャッシュを更新するだけなのです.
ということでそのキャッシュの実装ですが,Hibernate が最初からサポートしている (CacheProvider を用意している) キャッシュ実装の中で,transactional 戦略で使用することが出来るのは,JBoss TreeCache だけらしいです.
ということで,まずはこいつを調査♪


JBoss TreeCache は,実は JBossCache というプロジェクトの一部なのですね.最新版 (1.01) はここからダウンロードすることが出来ます.
でも,ダウンロードした zip ファイルを解凍してもソースが見あたらないよ? CVS から取ってくるの? CVS ってどこよ?
うーみゅ,JBoss 界隈にはなじみがありませんから!! 残念!!!!
まぁいーや.Hibernate には,JBossCache のライブラリも含まれているので,それをそのまま使うことにしよう.
そんなことより,JBoss TreeCache を使う上で一番気になったのは,JBossアプリケーションサーバなしで,単独で使うことが出来るのか? ということ.
なんか,FAQ 見たら単独で動くって書いてありますね.一安心♪
むむっ!

Q: Does JBossCache support XA (2PC) transaction now?
A: No, although it is also on our to do list. Our internal implementation does use a similar 2PC procedure to coordinate transaction among different instances.

な,なんですとぉ?
どういうことよっ!! 2PC 抜きでトランザクショナルとか言ってるわけ!?
なんだかなぁ.いきなりやる気NotFoundException がスローされてしまったじゃあーりませんか.
いいけどね.どうせお試しの RDBHSQLDB だし (爆死).
うーみゅ,やっぱり本命は商用の TANGOSOL Coherence とかになるわけか.
でもでも,Coherence をトランザクショナルに使うには,JCA(J2EE Connector Architecture) の ResourceAdapter 経由になっちゃうんだよね.そのための RAR ファイルが提供されていたりして.WebLogic なんかで使うには,超簡単らしいけど,いまは手軽に使いたいわけで...
ひがさん,S2JCA くださぁぁぁぁぁい!!!!! と叫んでみるてすと (^^;


まぁいいや.ここは所詮お試しなので,TreeCache で雰囲気だけ味わってみましょう.
その TreeCache を使うための CachePrividerCache の実装クラスは,Hibernate が標準で用意してくれています.それぞれ,TreeCacheProviderTreeCache です.
この TreeCacheProvider ですが,TreeCache を設定するための情報が treecache.xml というファイルで提供されることを前提にしています.これがないと例外が吹っ飛んできます.昨日やられましたから!! 残念!!!!


そんなわけで,そのファイルを用意しなければなりません.どうやって?
...
おおぉぉぉっ,さすが!! Hibernate を展開したディレクトリの etc の下にちゃんとありますがな.
なんか,<mbean> 要素とかありますけど... JBoss アプリケーションサーバで使うファイルそのままな感じ.
そんなことより気になることが.CacheMode なる設定があるのですが,それが LOCAL になっていますよ? コメントによると,他に指定できる値は REPL_ASYNCREPL_SYNC.うーみゅ,LOCAL じゃダメなんじゃないか? よー分からんけど.
不安になったのでダウンロードした JBossCache の方を調査.こっちの etc/META-INF の下にたくさん XML ファイルがあります.その中に,replSync-service.xml というものが!! 普通に考えたら,レプリケーションで分散キャッシュでしょう? ローカルなわけがないよねぇ? 同期レプリケーションか非同期レプリケーションかは微妙だけど,今は transactional 戦略で使うキャッシュ実装に挑戦中なので,ここは無難に同期レプリケーションの気分.
そんなわけで,replSync-service.xml を頂くことにしましょう.こいつをクラスパスの通っているディレクトリにコピーして, treecache.xml にリネーム♪


さて,次は JTA です.JTA が必要なのです.そこで S2 の登場です.そして S2 を使うならついでに S2Hibernate も使わないわけにはいかないでしょう.
S2Hibernate は,S2Tx のトランザクションマネージャと連携するセッション (S2Session) を用意してくれます.やったね!
今回はそれに加えて,TreeCache も JTAトランザクションマネージャと連携してもらわなくてはなりません.どうやって?
そのために使うのが,TransactionManagerLookup です.一昨日も昨日も今日も出てきたやつ.
ということで,さくっと用意してみましょう.

package study;

import java.util.Properties;
import javax.transaction.TransactionManager;
import net.sf.hibernate.transaction.TransactionManagerLookup;
import org.seasar.framework.container.factory.SingletonS2ContainerFactory;

public class S2TransactionManagerLookup implements TransactionManagerLookup {
    public TransactionManager getTransactionManager(Properties props) {
        return (TransactionManager) SingletonS2ContainerFactory.getContainer()
                .getComponent(TransactionManager.class);
    }

    public String getUserTransactionName() {
        return null;
    }
}

getUserTransactionName() の実装は任意らしいので,null を返すだけで済ませています.
S2Hibernate で用意してくれてもいいかも.>id:kenichi_okazaki さん
そして,Hibernate にこいつを使ってもらうため,hibernate.cfg.xml に次の行を追加します.

        <property name="hibernate.transaction.manager_lookup_class">study.S2TransactionManagerLookup</property>

ついでに CacheProviderTreeCacheProvider に変更しましょう.

        <property name="hibernate.cache.provider_class">net.sf.hibernate.cache.TreeCacheProvider</property>


よーし,こんなもんで下ごしらえはいいかなぁ?
実験始めるぞーっ!!!!
でも,ネタは昨日までのと同じ.心より恥じる.
とはいえ,今回は S2 を使うので,昨日までの適当なクラスではちょっと困ったちゃん.
ということで,少しまじめにやってみませう.少しだけね.
まずは雑誌の DAO のインタフェース.

package study;

import java.util.List;

public interface MagazineDao {
    List findAll();
    Magazine findById(int id);
}

その実装.

package study;

import java.util.List;
import org.seasar.hibernate.S2Session;
import org.seasar.hibernate.S2SessionFactory;

public class MagazineDaoImpl implements MagazineDao {
    private S2SessionFactory sessionFactory;

    public void setSessionFactory(S2SessionFactory sessionFactory) {
        this.sessionFactory = sessionFactory;
    }

    public List findAll() {
        S2Session session = sessionFactory.getSession();
        return session.find("from study.Magazine");
    }

    public Magazine findById(int id) {
        S2Session session = sessionFactory.getSession();
        return (Magazine) session.load(Magazine.class, new Integer(id));
    }

}

モデルの DAO のインタフェース.

package study;

import java.util.List;

public interface ModelDao {
    List findAll();
    Model findById(int id);
}

その実装.

package study;

import java.util.List;
import org.seasar.hibernate.S2Session;
import org.seasar.hibernate.S2SessionFactory;

public class ModelDaoImpl implements ModelDao {
    private S2SessionFactory sessionFactory;

    public void setSessionFactory(S2SessionFactory sessionFactory) {
        this.sessionFactory = sessionFactory;
    }

    public List findAll() {
        S2Session session = sessionFactory.getSession();
        return session.find("from study.Model");
    }

    public Model findById(int id) {
        S2Session session = sessionFactory.getSession();
        return (Model) session.load(Model.class, new Integer(id));
    }
}

それから,トランザクションAspect をかけるためのクラスが必要です.
面倒なので Runnable で (苦笑).どこがマジメなんだか...
まずは問い合せ用.

package study;

import java.util.Iterator;

public class Query implements Runnable {
    private MagazineDao magazineDao;

    public void setMagazineDao(MagazineDao magazineDao) {
        this.magazineDao = magazineDao;
    }

    public void run() {
        Iterator it = magazineDao.findAll().iterator();
        while (it.hasNext()) {
            System.out.println(it.next());
        }
    }
}

続いて更新用.

package study;

public class Update implements Runnable {
    private ModelDao modelDao;
    
    public void setModelDao(ModelDao modelDao) {
        this.modelDao = modelDao;
    }
    
    public void run() {
        Model model = modelDao.findById(0);
        System.out.println("before : " + model);
        model.name = "Yu Yamada";
        System.out.println("after  : " + model);
    }
}

そして app.dicon

<?xml version="1.0" encoding="Shift_JIS"?>
<!DOCTYPE components PUBLIC "-//SEASAR//DTD S2Container//EN" 
    "http://www.seasar.org/dtd/components.dtd"
>
<components>
    <include path="j2ee.dicon"/>
    <component class="org.seasar.hibernate.impl.S2SessionFactoryImpl"/>

    <component class="study.MagazineDaoImpl"/>

    <component class="study.ModelDaoImpl"/>

    <component name="query" class="study.Query">
        <aspect>j2ee.requiredTx</aspect>
    </component>

    <component name="update" class="study.Update">
        <aspect>j2ee.requiredTx</aspect>
    </component>
</components>

そして最後に実行用のクラス.

package study;

import org.seasar.framework.container.S2Container;
import org.seasar.framework.container.factory.SingletonS2ContainerFactory;

public class Main {
    public static void main(String[] args) {
        try {
            SingletonS2ContainerFactory.init();
            S2Container container = SingletonS2ContainerFactory.getContainer();
            try {
                Runnable command = (Runnable) container.getComponent(args[0]);
                command.run();
            }
            finally {
                SingletonS2ContainerFactory.destroy();
            }
        }
        catch (Throwable e) {
            e.printStackTrace();
        }
    }
}

コマンドライン引数で指定された名前の Runnableコンポーネントを取ってきて実行するだけです.
おっと,キャッシング戦略を transactional にしないといけませんね.
雑誌およびモデルのマッピングファイルにある <cache> 要素を次のように修正します.

        <cache usage="transactional"/>


さーて,まずは分散キャッシュが働いているかどうかを確認しましょう.
専用のサーバをたてずにレプリケーションで情報を共有する分散キャッシュの場合,最低一つはプロセスがいないとキャッシュした情報は失われてしまうはず.
そんなわけで (どんなわけで?),最後に実行される SingletonS2ContainerFactory#destroy() の行にブレークポイントを付けておきます.
そして引数に query を指定して実行!!!!

[main] INFO - cache provider: net.sf.hibernate.cache.TreeCacheProvider
[main] INFO - instantiating and configuring caches
[main] INFO - instantiating TransactionManagerLookup: study.S2TransactionManagerLookup
[main] INFO - instantiated TransactionManagerLookup

-------------------------------------------------------
GMS: address is cc0040:2830
-------------------------------------------------------
[main] INFO - building session factory
[main] INFO - Not binding factory to JNDI, no JNDI name configured
Hibernate: select 
               magazine0_.id as id, magazine0_.name as name 
           from 
               Magazine magazine0_
[main] DEBUG - caching: 0
[main] DEBUG - caching: 1
[main] DEBUG - cache lookup: 1
[main] DEBUG - cache miss
Hibernate: select 
               model0_.magazine as magazine__, model0_.id as id__, 
               model0_.id as id0_, model0_.name as name0_ 
           from 
               Model model0_ 
           where 
               model0_.magazine=?
[main] DEBUG - caching: 2
[main] DEBUG - caching: 3
[main] DEBUG - caching: 1
[main] DEBUG - cache lookup: 0
[main] DEBUG - cache miss
Hibernate: select 
               model0_.magazine as magazine__, model0_.id as id__, 
               model0_.id as id0_, model0_.name as name0_ 
           from 
               Model model0_ 
           where 
               model0_.magazine=?
[main] DEBUG - caching: 0
[main] DEBUG - caching: 1
[main] DEBUG - caching: 0
CanCam[Asami Usuda, Yuri Ebihara]
ViVi[Jun Hasegawa, Sayo Aizawa]

ふむ.最初に雑誌を全件取得する SQL が発行されて (結果は 2 件),その結果行ごとにモデルを取得するための SQL が発行されています.N + 1 ってやつですね.
このプロセスを動かしたまま (ブレークポイントで停止したまま),もう一つ別のプロセスを同じく query を引数として実行!!!!

[main] INFO - cache provider: net.sf.hibernate.cache.TreeCacheProvider
[main] INFO - instantiating and configuring caches
[main] INFO - instantiating TransactionManagerLookup: study.S2TransactionManagerLookup
[main] INFO - instantiated TransactionManagerLookup

-------------------------------------------------------
GMS: address is cc0040:2841
-------------------------------------------------------
[main] INFO - building session factory
[main] INFO - Not binding factory to JNDI, no JNDI name configured
Hibernate: select 
               magazine0_.id as id, magazine0_.name as name 
           from 
               Magazine magazine0_
[main] DEBUG - caching: 0
[main] DEBUG - caching: 1
[main] DEBUG - cache lookup: 1
[main] DEBUG - cache hit
[main] DEBUG - cache lookup: 3
[main] DEBUG - cache hit
[main] DEBUG - cache lookup: 2
[main] DEBUG - cache hit
[main] DEBUG - cache lookup: 0
[main] DEBUG - cache hit
[main] DEBUG - cache lookup: 0
[main] DEBUG - cache hit
[main] DEBUG - cache lookup: 1
[main] DEBUG - cache hit
CanCam[Yuri Ebihara, Asami Usuda]
ViVi[Sayo Aizawa, Jun Hasegawa]

分散キャッシュきたーーーーーーっ!!
このプロセスとしては初めてのセッションなのですが,最初に雑誌を問い合せた後,モデルはキャッシュがヒットしています.


では,いよいよ更新の方を試してみましょう.
まず確認したいのは,キャッシュを更新しても,トランザクションがコミットされるまで,他のプロセスからは見えないということ.
そこで,HibernateTransactionalCache というクラスの update() メソッドにブレークポイントを設定します.

    public void update(Object key, Object value) throws CacheException {
        if ( log.isDebugEnabled() ) log.debug("updating: " + key);
        cache.put(key, value);
    }

この cache.put(key, value) ってところね.
そして,update を引数として,Main クラスを実行!!!!

[main] INFO - cache provider: net.sf.hibernate.cache.TreeCacheProvider
[main] INFO - instantiating and configuring caches
[main] INFO - instantiating TransactionManagerLookup: study.S2TransactionManagerLookup
[main] INFO - instantiated TransactionManagerLookup

-------------------------------------------------------
GMS: address is cc0040:2855
-------------------------------------------------------
[main] INFO - building session factory
[main] INFO - Not binding factory to JNDI, no JNDI name configured
[main] DEBUG - cache lookup: 0
[main] DEBUG - cache hit
before : Asami Usuda
after  : Yu Yamada
Hibernate: update Model set name=? where id=?
[main] DEBUG - updating: 0

ブレークポイントがヒットするので,ステップオーバーして,キャッシュを更新します.
ちなみに,このとき S2 の Transaction を見ると,S2Hibernate のセッションと共に,TreeCache も Synchronization として登録されていることが確認できます.


ここで,もう一度引数に query を指定して Main を実行!!!!

[main] INFO - cache provider: net.sf.hibernate.cache.TreeCacheProvider
[main] INFO - instantiating and configuring caches
[main] INFO - instantiating TransactionManagerLookup: study.S2TransactionManagerLookup
[main] INFO - instantiated TransactionManagerLookup

-------------------------------------------------------
GMS: address is cc0040:2866
-------------------------------------------------------
[main] INFO - building session factory
[main] INFO - Not binding factory to JNDI, no JNDI name configured
Hibernate: select 
               magazine0_.id as id, magazine0_.name as name 
           from 
               Magazine magazine0_
[main] DEBUG - caching: 0
[main] DEBUG - caching: 1
[main] DEBUG - cache lookup: 1
[main] DEBUG - cache hit
[main] DEBUG - cache lookup: 3
[main] DEBUG - cache hit
[main] DEBUG - cache lookup: 2
[main] DEBUG - cache hit
[main] DEBUG - cache lookup: 0
[main] DEBUG - cache hit
[main] DEBUG - cache lookup: 0
[main] DEBUG - cache hit
[main] DEBUG - cache lookup: 1
[main] DEBUG - cache hit
CanCam[Yuri Ebihara, Asami Usuda]
ViVi[Sayo Aizawa, Jun Hasegawa]

ほほぉぉぉっ.
キャッシュにヒットしているのに,CanCam モデルの一人は優ちゃんではなくてあさ美ちゃんです.
更新を行っているトランザクションはまだコミットの途中 (before completion) だからですね.


ということで,更新プロセスの実行を再開.一気に終わらせてしまいましょう.
そしてもう一度,引数に query を指定して Main を実行!!!!

[main] INFO - cache provider: net.sf.hibernate.cache.TreeCacheProvider
[main] INFO - instantiating and configuring caches
[main] INFO - instantiating TransactionManagerLookup: study.S2TransactionManagerLookup
[main] INFO - instantiated TransactionManagerLookup

-------------------------------------------------------
GMS: address is cc0040:2879
-------------------------------------------------------
[main] INFO - building session factory
[main] INFO - Not binding factory to JNDI, no JNDI name configured
Hibernate: select magazine0_.id as id, magazine0_.name as name from Magazine magazine0_
[main] DEBUG - caching: 0
[main] DEBUG - caching: 1
[main] DEBUG - cache lookup: 1
[main] DEBUG - cache hit
[main] DEBUG - cache lookup: 3
[main] DEBUG - cache hit
[main] DEBUG - cache lookup: 2
[main] DEBUG - cache hit
[main] DEBUG - cache lookup: 0
[main] DEBUG - cache hit
[main] DEBUG - cache lookup: 0
[main] DEBUG - cache hit
[main] DEBUG - cache lookup: 1
[main] DEBUG - cache hit
CanCam[Yu Yamada, Yuri Ebihara]
ViVi[Sayo Aizawa, Jun Hasegawa]

おおぉぉぉぉっ.
やはりキャッシュにヒットしていますが,今度はあさ美ちゃんではなく優ちゃんになっています.
これがトランザクショナルな分散キャッシュ!!


ここでの注目はキャッシュのヒット率です.
read-write 戦略では,どこかのトランザクションが更新中 (キャッシュに設定したがコミットはまだ) の情報については DB をアクセスしていました.
nonstrict-read-write 戦略では,更新が行われるとキャッシュが破棄されるため,その後は DB にアクセスしていました.
しかし,transactional では,更新中であれ,更新後であれ,キャッシュの情報が参照され,DB へのアクセスは発生しません.
今回の例だと,キャッシュを参照しない Session#find() を使っている雑誌はともかく,モデルの方は一番最初に問い合せをしたプロセスを除いて全く DB にアクセスしていません.トランザクションもプロセスも別なのに!! 恐るべし!!
それがトランザクショナルな分散キャッシュ!!


という感じなのですが,実のところ TreeCache は XAResource を提供しているわけではなく,after completion のタイミングでキャッシュが更新されるだけなので,一貫性という点で本当に安心できるのかはちょっと疑問.
...
いやいや,よくよく考えてみると,たとえ 2PC でも安心はできないわけで.
以前も書いた気がしますが,2PC はコミットが完了するタイミングは同期してくれないのですよね.
つまり,DB のコミットが完了するタイミングと,キャッシュのコミットが完了するタイミングは同期しないのです.
例えば S2 のトランザクションマネージャだと,トランザクションに参加している複数のリソースに対するコミット (2PC の commit) は単一スレッド上でシーケンシャルに行われるので,各リソースのコミットが完了するタイミングの差というのはどうしても生じます.仮にマルチスレッドであったとしても,リソースごとに処理時間は異なるわけで...
結局の所,本当にクリティカルな情報をキャッシュ頼みでアクセスするのはおっかないということか?
それなら,多少の遅延くらいは許される情報に関して nonstrict-read-write を活用するのが無難?
うーみゅ,まだまだキャッシュは悩ましいかも〜.


あと,Hibernate がどうこうではなくて,分散キャッシュ自体の出来 (性能・信頼性) も気になるところ.
今回の TreeCache はレプリケーションを使っているようですが,当然多少の遅延はあるはず.それはどの程度なのか?
あと,キャッシュが 2PC 使えるとして,キャッシュにコミットできないからって DB もろともロールバックってどうよ? って気もするわけで.
うーみゅ...


その他遊んでいて気づいた事.
LockMode#UPGRADE でアクセスすると,キャッシュをパスして DB へ SELECT 〜 FOR UPDATE でアクセスするようです.
なので,基本的にキャッシュの分散ロックは必要なさそう.たぶん.


ともあれ (JW),transactional 戦略がどのようなものか,表面的な使い方を知る事は出来ました.
これを理解して使いこなすにはまだまだ至りませんが,どうすればうまく使えるのかを考えるのはなかなか楽しそうです.
そんなわけで (どんなわけで?),「14.3. The Second Level Cache」は終了.最後全然まとまってませんが.心より恥じる.
14 章も残りわずかですね.それが終わると 15 章はツールですか.DDL 生成したりソース生成したりするやつね.うーん,あまり興味が持てないのでパスかなぁ.そして16 〜 18 章はサンプルなのでこれもパスっぽい.
うーみゅ,長く続いた「Hibernate 入門記」も最終回間近か?
次のネタ考えねば...