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

今日は「14.3.3. Strategy: read/write」です.


前回の read-only 戦略も悪くはないものの,やっぱり更新系でもキャッシュを使いたい!!
そんなわけで (どんなわけで?),read-write な戦略の登場です.
でもでも,トランザクション分離レベルが serializable な場合には使わない方がいいのだとか.そんな分離レベル使ったことないからいいけど.
この戦略では,JTA と組み合わせて使うことも出来るそうです.その場合には,JTATransactionManager の実装を見つけてくれる TransactionManagerLookup の実装クラスを hibernate.transaction.manager_lookup_class というプロパティで指定するらしい.別に JTA を使わなければならないということはないようです.
それでですね,クラスタ環境でこの戦略を使用する場合は,水面下で使用するキャッシュ (EHCache とか TreeCache とか) がロックをサポートしている必要があるそうです.ところがどっこい,Hibernate がサポートしているキャッシュ (EHCache とか TreeCache とか) は,サポートしていない... のか? 使えないってこと?
なんか,それっぽいことが書いてあるんですけど,詳細は不明.


なんか,全然分かった気がしないのですが,ドキュメントにはこれくらいのことしか書いてありません.ひっどいよ.
そんなわけで (どんなわけで?),ソースを眺めてみることにしませう.
...
ふむふむ,どうやら Hibernate のキャッシング戦略は,

  • CacheConcurrencyStrategy

なる interfaceimplements したクラスによって実装されているようです.
その実装クラスは次の通り.

  • ReadOnlyCache
  • ReadWriteCache
  • NonstrictReadWriteCache
  • TransactionalCache

各戦略ごとにきっちりと実装クラスが存在します.
Hibernate のセッションは,これら CacheConcurrencyStrategy の実装クラスを介してキャッシュにアクセスします.そのキャッシュというのは,

  • Cache

という interfaceimplements したクラスで,昨日学習した

  • CacheProvider

という interfaceimplements したクラスから取得されます.
その Cache の実装クラスの多くは EHCache とか TreeChache などのラッパーです.
ということで,Session -> CacheConcurrencyStrategy -> Cache という流れがあることが分かりました.


そんなわけで (どんなわけで?),CacheConcurrencyStrategy を見てみましょう.
まず,キャッシュから情報を取得するためのメソッドが,

    • Object get(Object key, long txTimestamp)

です.key は主キーみたい.txTimestamp は,トランザクションが開始された時刻らしいです.
ここでキャッシュに該当オブジェクト (のプロパティ値の配列) があればそれが返されます.なければ null が返されます.
もし null が返されると,セッションは RDB に問い合せを行います.その結果得られた永続オブジェクト (のプロパティ値の配列) をキャッシングするために,

    • boolean put(Object key, Object value, long txTimestamp)

が呼び出されます.
永続オブジェクトが更新され,トランザクションがコミットされると,セッションはキャッシュを更新します.その時,最初に呼び出されるのが

    • SoftLock lock(Object key)

らしいです.まずキャッシュの該当オブジェクトをロックする,と.
続いて

    • void update(Object key, Object value)

が呼び出されます.
さらにその後,

    • void afterUpdate(Object key, Object value, SoftLock clientLock)

が呼び出されます.
update()afterUpdate() の違いは,どうやら前者はトランザクションのコミット前に,後者はコミット後に呼び出されるようです.JTASynchronization でいうと,beforeCompletion() のタイミングで呼び出されるのが update()afterCompletion() のタイミングで呼び出されるのが afterUpdate()
そして,ReadWriterStrategyNonstrictReadWriteStrategy はキャッシュを afterUpdate() で更新する (update() では何もしない) のに対して,TransactionalStrategy ではキャッシュを update() で更新しています(afterUpdate() では何もしない).なるほど.
TransactionalStrategy では,キャッシュはトランザクションに参加するリソースの一つなので,beforeCompletion のタイミングで更新しますが,もしかしたらロールバックされるかもしれないってことでしょう.
一方,ReadWriteStrategy では,キャッシュはトランザクションの一部ではないので,afterCompletion のタイミングでトランザクションのコミットが完了した場合のみキャッシュに反映する,と.
そんな感じっぽい.
そんなわけで (どんなわけで?),transactional 戦略は,「同期」な並行戦略,read-write および nonstrict-read-write は「非同期」な並行戦略ということになっているようです.
これから考えると,read-write / nonstrict-read-write とも,トランザクションの一貫性が重要な情報には使っちゃいけませんね.トランザクションのコミットが完了してから,キャッシュに反映されるまで,微妙なタイミングが存在しますから.
少々古い情報を見ても平気な場合には十分かも.一定時間で expire するようなキャッシュのイメージですね.


そんなわけで (どんなわけで?),ここから今日の本題,read-write 戦略を実装する ReadWriteStrategy をサクッと見てみましょう.
こいつの JavaDoc によると,この戦略は read committed をサポートするようです.
微妙な書き方ですが,repeatable read も「大体 (almost)」サポートするようです.大体ってなんだよ? (^^;
うーみゅ,read-commited をサポートするというのは,先に見たようにコミットされた情報しかキャッシュに反映しないからですね.dirty-read はあり得ない.
一方,repeatable read をサポートするには,キャッシュから情報を取得する際に共有ロックを獲得したりすると思うのですが,そうなってるのでしょうか?
...
違うっぽい.
どうやら,トランザクションが開始された時点のタイムスタンプを使って,それより新しい情報がキャッシュされている場合はそれを無視する (結果的に DB にアクセスする) ので,JDBC レベルで repeatable read になっていれば大丈夫ということみたい.たぶん.
「大体」の意味は,本来 repeatable-read ならキャッシュが有効に使えそう (同じ値が見えるので) なのに,read-write 戦略では逆にキャッシュを使わない可能性が高くなるから,ということかも?
さらに,複数のスレッド (トランザクション) で並行に更新される場合は,どれもキャッシュしないみたい.なかなか微妙な実装だ...


なんか,調べてるだけじゃ今ひとつなので,お試ししましょう.
テーブルや永続クラスは前回と同じです.
モデルのマッピングファイルのキャッシングの指定を次のように修正します.

        <cache usage="read-write"/>

そして更新用のクラス.

package study;

import net.sf.hibernate.Session;

public class Update implements Runnable {
    private static HibernateTemplate template;
    private String name;

    public static void main(String[] args) {
        try {
            template = new HibernateTemplate();
            Thread t1 = new Thread(new Update("Yu Yamada"), "A");
            Thread t2 = new Thread(new Update("Naoko Tokuzawa"), "B");
            t1.start();
            t2.start();
        }
        catch (Exception e) {
            e.printStackTrace();
        }
    }

    public Update(String name) {
        this.name = name;
    }
    public void run() {
        try {

            System.out.println("*** " + Thread.currentThread().getName() + " begin ***");
            template.process(new HibernateCallback() {
                public Object doProcess(Session session) throws Exception {
                    Model model = (Model) session.load(Model.class,
                        new Integer(0));
                    model.name = name;
                    return null;
                }
            });
            System.out.println("*** " + Thread.currentThread().getName() + " end ***");
        }
        catch (Exception e) {
            e.printStackTrace();
        }
    }

}

スレッドを二つ (AB) 用意しています.
こいつの *** X begin *** と出力しているところ (begin) と,匿名クラスの doProcess() の中 (update) にブレークポイントを付けて,タイミングを計って実行します.
まずは,

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

というタイミングで実行してみました.
その結果.

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

たくさん 0 が表示されているのは,ID プロパティの値です.ちなみに ID プロパティが 0 のモデルはあさ美ちゃん.
ともあれ (JW),A のスレッドでは最初の問い合わせでキャッシュミス (Cache miss) となり,SELECT が行われています.
その結果がキャッシュされ (Caching),さらに更新されています (Updating).
次に実行された B のスレッドでは,キャッシュにデータがあるので (Chache hit),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: 0
Hibernate: select model0_.id as id0_, model0_.name as name0_ from Model model0_ where model0_.id=?
[A] DEBUG - Caching: 0
[A] DEBUG - Cached: 0
[A] DEBUG - Invalidating: 0
Hibernate: update Model set name=? where id=?
[A] DEBUG - Updating: 0
[A] DEBUG - Updated: 0
*** A end ***
[B] DEBUG - Cache lookup: 0
[B] DEBUG - Cached item was locked: 0
Hibernate: select model0_.id as id0_, model0_.name as name0_ from Model model0_ where model0_.id=?
[B] DEBUG - Caching: 0
[B] DEBUG - Item was already cached: 0
[B] DEBUG - Invalidating: 0
Hibernate: update Model set name=? where id=?
[B] DEBUG - Updating: 0
[B] DEBUG - Updated: 0
*** B end ***

この結果で興味深いのは,B のスレッドが SELECT に行く前,キャッシュされている情報がロックされた状態になっている (Cached item was locked) ことです.
B のスレッドがキャッシュを見に行った時には,すでに A のスレッド (トランザクション) は終了しているため,通常の意味のロック (ハードロック) はかかっていません.
しかし,B のトランザクションが開始された時刻よりも新しい情報がキャッシュされているため,このような結果になるようです.


このあたり,Oracle のマルチバージョニングと似てなくもない?

みたいな.
うまく直列可能な実行 (serializability) を達成しているように見えます.
トランザクショナルではないという問題はありますが,並行な更新もうまくいくように見えるので,使いどころしだいでは効果的かも.
例えば Amazon なんかで,「在庫2点あります」みたいなのって,必ずしも共有ロックをかけたりとかしてまでトランザクショナルにアクセスする必要があるとは思えません.多少の遅延は許されそうな感じ.っていうか,許されているとしか思えません.
こういう場合には,read-write 戦略は十分に使えるように感じました.


とはいえ,複数の JVM にまたがって,どこの JVM 上で更新されるか分からないような場合は,ちょっと注意が必要になりそうです.
これは,キャッシング戦略ではなく,キャッシュの実装そのものの影響が大きそう.
ま,個人的にはトランザクショナルなキャッシュにより興味があるので,EHCache とかを使おうという気にはなかなかなれません.残念!!!!


そんなわけで (どんなわけで?),次回は「14.3.4. Strategy: nonstrict read/write」へ進みます.
今日のと何が違うのだろうか?