Hibernate 入門記 パフォチュー その5 二次キャッシュ (nonstrict-read-write)

今日は「14.3.4. Strategy: nonstrict read/write」です.
でも,例によってこのリファレンスドキュメントを見ても詳細は分かりませんから!! 残念!!!!


ともあれ (JW),まずはそのリファレンスから見ていきましょう.
なんでも,たま〜に更新されるような情報でかつ,厳密なトランザクション特性 (一貫性とか) を要求されない場合には,この nonstrict-read-write 戦略を適用できるかもしれない,とのことです.
んで,JTA を使うのなら TransactionManagerLookup の実装クラスを hibernate.transaction.manager_lookup_class というプロパティで指定しろってさ.へいへい.
以上.


ね,なんにも分からないでしょう? 困ったものだ.しくしくしく.
厳密なトランザクション特性が要求されないにしても,どんな場合にどんな状況になるのか分からないと,怖くて使えませんって.
そんな場合には,ソースを見ることが出来るのがオープンソースのいいところ♪
そんなわけで (どんなわけで?),まずは JavaDoc のコメントを参照.
ふむふむ.
なんでも,nonstrict-read-write キャッシュはロックを使ったりはしないようです.そして,並行にアクセスが行われた場合には,最新の情報を返すことを保証しないのだとか.この場合の並行アクセスというのは,更新と参照かな?


そのあたりを確認すべく,NonstrictReadWriteCache のソースをチェッーク.
むむぅ. こ,これは...
ずいぶん単純な実装だなぁ.(^^;
なるほどねぇ.基本的な考え方は,更新されたらキャッシュしている情報を捨てるっていうことですね.
そんなわけで (どんなわけで?),

    • update(Object key, Object value)
    • void afterUpdate(Object key, Object value, SoftLock lock)

のいずれの場合でも,キャッシュしている情報を破棄します.
昨日見たように,前者はトランザクション (DB) のコミット前に,後者はコミット後にそれぞれ呼ばれるわけですが,その両方でキャッシュを破棄するのです.コメントによると,後者の方は「安全のため」に破棄するみたい.
ふーん,そういうことですか.


ということだと,問題になるケースは

T1:update() でキャッシュを破棄
        T2:キャッシュにアクセス (ミス)
        T2:DB から読んだ情報をキャッシング
T1:トランザクションをコミット
                T3:キャッシュにアクセス (ヒット)
T1:afterUpdate() でキャッシュを破棄

こんな場合とかかなぁ.

  • 一つめのトランザクション (T1) が永続オブジェクトを更新してセッションをコミットすると,update() でキャッシュを破棄します.
  • その直後,別のトランザクション (T2) が永続オブジェクトにアクセスし,キャッシュを見に来ますが空振りします.そしてDBにアクセスし,その時点のデータを読み込み,キャッシュに設定します.
  • ここで T1 が DB をコミットします.
  • その直後,さらに別のトランザクション (T3) が永続オブジェクトにアクセスすると,キャッシュがヒットしてしまい,その時点でDBに保存 (コミット) されている情報ではなく,古い情報を読み出してしまいます.
  • その後 T1 の afterUpdate() でキャッシュが破棄されるので,その後で永続オブジェクトにアクセスする場合はキャッシュではなく DB を見に行くので,最新のデータを見ることになります.

かなーり絶妙なタイミングですが,Web アプリなどのように並行で処理するスレッドが多い場合には,あり得ない話ではないでしょう.
そんなわけで (どんなわけで?),厳密なトランザクション特性が要求される場合には使っちゃいけない戦略なのですね.


おおよその動きが分かったので,早速お試ししちゃいましょう.
え? 早すぎ? 手抜きじゃないですよぉ.だって,ドキュメントにもソースにもこれ以上情報なさそうだしぃ.
そんなわけで (どんなわけで?),まずは確認のため,昨日のネタそのままでいってみましょう.
モデルのマッピングファイルのキャッシングの指定を次のように修正します.

        <cache usage="nonstrict-read-write"/>

最初は

A begin
A update
A end
        B begin
        B update
        B end

のケース.

*** A begin ***
[A] DEBUG - Cache lookup: 0
[A] DEBUG - Cache miss
Hibernate: select model0_.id as id0_, model0_.name as name0_ from Model model0_ where model0_.id=?
[A] DEBUG - Caching: 0
Hibernate: update Model set name=? where id=?
[A] DEBUG - Invalidating: 0
[A] DEBUG - Invalidating (again): 0
*** A end ***
*** B begin ***
[B] DEBUG - Cache lookup: 0
[B] DEBUG - Cache miss
Hibernate: select model0_.id as id0_, model0_.name as name0_ from Model model0_ where model0_.id=?
[B] DEBUG - Caching: 0
Hibernate: update Model set name=? where id=?
[B] DEBUG - Invalidating: 0
[B] DEBUG - Invalidating (again): 0
*** B end ***

ふむふむ.
確かに更新するたびに二回ずつキャッシュを破棄していることが分かります.
そのため,スレッド B でも SELECT が実行されています.
次に

A begin
        B begin
A update
A end
        B update
        B end

のケース.

*** A begin ***
*** B begin ***
[A] DEBUG - Cache lookup: 0
[A] DEBUG - Cache miss
Hibernate: select model0_.id as id0_, model0_.name as name0_ from Model model0_ where model0_.id=?
[A] DEBUG - Caching: 0
Hibernate: update Model set name=? where id=?
[A] DEBUG - Invalidating: 0
[A] DEBUG - Invalidating (again): 0
*** A end ***
[B] DEBUG - Cache lookup: 0
[B] DEBUG - Cache miss
Hibernate: select model0_.id as id0_, model0_.name as name0_ from Model model0_ where model0_.id=?
[B] DEBUG - Caching: 0
Hibernate: update Model set name=? where id=?
[B] DEBUG - Invalidating: 0
[B] DEBUG - Invalidating (again): 0
*** B end ***

最初のと同じですね.
前回の read-write 戦略と異なり,トランザクションの開始時刻は考慮されないので,なんの影響もありません.


では,nonstrict-read-write で問題が起きるようなケースを試してみましょう.

package study;

import net.sf.hibernate.Session;

public class UpdateAndQuery {
    private static HibernateTemplate template;
    private String name;

    public static void main(String[] args) {
        try {
            template = new HibernateTemplate();
            Thread t1 = new Thread(new Update("T1"), "T1");
            Thread t2 = new Thread(new Query("T2"), "T2");
            Thread t3 = new Thread(new Query("T3"), "T3");
            t1.start();
            t2.start();
            t3.start();
        }
        catch (Exception e) {
            e.printStackTrace();
        }
    }

    private static class Update implements Runnable {
        private String name;
        public Update(String name) {
            this.name = name;
        }
        public void run() {
            try {
                System.out.println("*** " + name + " begin ***");
                template.process(new HibernateCallback() {
                    public Object doProcess(Session session) throws Exception {
                        Model model = (Model) session.load(Model.class,
                            new Integer(0));
                        model.name = "Yu Yamada";
                        return null;
                    }
                });
                System.out.println("*** " + name + " end ***");
            }
            catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

    private static class Query implements Runnable {
        private String name;
        public Query(String name) {
            this.name = name;
        }
        public void run() {
            try {
                System.out.println("*** " + name + " begin ***");
                template.process(new HibernateCallback() {
                    public Object doProcess(Session session) throws Exception {
                        System.out.println(name + " : "
                            + session.load(Model.class, new Integer(0)));
                        return null;
                    }
                });
                System.out.println("*** " + name + " end ***");
            }
            catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
}

先に書いたように,3つのスレッド (トランザクション) を実行します.
T1 は更新,T2 と T3 は問い合せのみを行います.
こいつにブレークポイントを付けまくって,意図したとおりのタイミングで実行!!!!

*** T1 begin ***
[T1] DEBUG - begin
[T1] DEBUG - current autocommit status:false
[T1] DEBUG - Cache lookup: 0
[T1] DEBUG - Cache miss
Hibernate: select model0_.id as id0_, model0_.name as name0_ from Model model0_ where model0_.id=?
[T1] DEBUG - Caching: 0
[T1] DEBUG - commit
Hibernate: update Model set name=? where id=?
[T1] DEBUG - Invalidating: 0

ここまでで,永続オブジェクトが更新されてトランザクションのコミット処理が始まっています.
その中で,NonstrictReadWriteCache#update() が呼び出され,キャッシュが破棄されています.
ここで T2 を開始します.

        *** T2 begin ***
        [T2] DEBUG - begin
        [T2] DEBUG - current autocommit status:false
        [T2] DEBUG - Cache lookup: 0
        [T2] DEBUG - Cache miss
        Hibernate: select model0_.id as id0_, model0_.name as name0_ from Model model0_ where model0_.id=?
        [T2] DEBUG - Caching: 0
        T2 : Asami Usuda
        [T2] DEBUG - commit
        *** T2 end ***

モデルの名前が更新前の値,あさ美ちゃんになっています.まだ RDBMS に対するトランザクションのコミット処理は行われていないので,当然の結果です.
ここで,RDBMS に対するトランザクションのコミットまで T1 を進めます.
DB に問い合せをすると,モデルの名前は優ちゃんになっています.
そして,T3 を開始します (その後 T1 も再開).

                *** T3 begin ***
                [T3] DEBUG - begin
                [T3] DEBUG - current autocommit status:false
                [T3] DEBUG - Cache lookup: 0
                [T3] DEBUG - Cache hit
                T3 : Asami Usuda
                [T3] DEBUG - commit
                *** T3 end ***
[T1] DEBUG - Invalidating (again): 0
*** T1 end ***

RDB はすでにコミットされ,レコードは優ちゃんになっているのに,T3 はキャッシュを参照したため相変わらずあさ美ちゃんのままになっています.ということで,最新の情報を得られるとは限らない状況を確認することが出来ました.


そんなわけで (どんなわけで?),nonstrict-read-write がどのようなキャッシング戦略なのかは理解できたように思います.
基本的には,激しく参照されて,たまぁ〜に更新があるけど,ちょっとの間くらいなら古い情報を参照しても構わない場合に使えるって感じですかねぇ.
実際に使用するキャッシュ実装がキャッシングした情報のタイムアウトをサポートしていれば,遅延の上限も保証できるわけだし.
ルーズにアクセスできる情報に関しては悪くないかも.


read-write と nonstrict-read-write の使い分けは,更新の頻度によるのでしょうか.
ある程度更新が多い場合には,更新した情報もキャッシングする read-write 戦略の方が効率的かもしれません.ただし,read-write 戦略はキャッシュ実装がロックを提供していなければ使えませんし,ロックを使う分パフォーマンスが低いかもしれません.
一方 nonstrict-read-write 戦略は,ロックを使わないので,read-write 戦略よりも効率的かもしれませんが,頻繁に更新が行われる場合には,キャッシュの破棄も多くなるため,キャッシュのヒット率は低下すると思われます.


クラスタ環境への対応 (複数 JVM でのキャッシュの共有) についても考慮が必要かも.
クラスタ環境で使用できるかどうかは,どちらもキャッシュ実装に依存するわけですが,read-write 戦略はキャッシュ実装がロック (分散ロックですね) を提供していなくては使えません.
一方 nonstrict-read-write 戦略の場合には,キャッシュ実装がロックを提供していなくても使用することが出来ます.


個人的には,どちらもあまり使いたい気分になりそうな気がしないなぁ.
そんなわけで (どんなわけで?),次はいよいよ大本命,「14.3.5. Strategy: transactional」へ進みます.やっぱりトランザクショナルでなきゃね!
しかしですね,こいつを試すにはきっと JTA を使わなきゃいけないんですよね.ということは... S2Hibernate ですね!
そしてそれに対応したキャッシュ実装を用意しなければなりません.ということは... なんだろう?
JBoss TreeCache というのがトランザクショナルに使うことの出来るキャッシュ実装として紹介されていますが,これは簡単に使えるもの? 今試してみたら treecache.xml が見つからないって例外飛んできた.(;_;)
なんか,周辺でつまずきそうな予感...
TreeCache が難しそうだったら TANGOSOL Coherence を使っちゃうかも.こいつは特に Configuration しなくてもサクッと使えそうなので.


ともあれ (JW),いよいよトランザクショナル分散キャッシュですよ.楽しみ♪