Hibernate 入門記 パフォチュー その4 二次キャッシュ (read-write)
今日は「14.3.3. Strategy: read/write」です.
前回の read-only 戦略も悪くはないものの,やっぱり更新系でもキャッシュを使いたい!!
そんなわけで (どんなわけで?),read-write な戦略の登場です.
でもでも,トランザクション分離レベルが serializable な場合には使わない方がいいのだとか.そんな分離レベル使ったことないからいいけど.
この戦略では,JTA と組み合わせて使うことも出来るそうです.その場合には,JTA の TransactionManager
の実装を見つけてくれる TransactionManagerLookup
の実装クラスを hibernate.transaction.manager_lookup_class
というプロパティで指定するらしい.別に JTA を使わなければならないということはないようです.
それでですね,クラスタ環境でこの戦略を使用する場合は,水面下で使用するキャッシュ (EHCache とか TreeCache とか) がロックをサポートしている必要があるそうです.ところがどっこい,Hibernate がサポートしているキャッシュ (EHCache とか TreeCache とか) は,サポートしていない... のか? 使えないってこと?
なんか,それっぽいことが書いてあるんですけど,詳細は不明.
なんか,全然分かった気がしないのですが,ドキュメントにはこれくらいのことしか書いてありません.ひっどいよ.
そんなわけで (どんなわけで?),ソースを眺めてみることにしませう.
...
ふむふむ,どうやら Hibernate のキャッシング戦略は,
CacheConcurrencyStrategy
なる interface
を implements
したクラスによって実装されているようです.
その実装クラスは次の通り.
ReadOnlyCache
ReadWriteCache
NonstrictReadWriteCache
TransactionalCache
各戦略ごとにきっちりと実装クラスが存在します.
Hibernate のセッションは,これら CacheConcurrencyStrategy
の実装クラスを介してキャッシュにアクセスします.そのキャッシュというのは,
Cache
という interface
を implements
したクラスで,昨日学習した
CacheProvider
という interface
を implements
したクラスから取得されます.
その 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()
の違いは,どうやら前者はトランザクションのコミット前に,後者はコミット後に呼び出されるようです.JTA の Synchronization
でいうと,beforeCompletion()
のタイミングで呼び出されるのが update()
,afterCompletion()
のタイミングで呼び出されるのが afterUpdate()
.
そして,ReadWriterStrategy
や NonstrictReadWriteStrategy
はキャッシュを 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(); } } }
スレッドを二つ (A
と B
) 用意しています.
こいつの *** 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 のマルチバージョニングと似てなくもない?
- データブロックの SCN がトランザクションの SCN よりも新しければロールバックセグメントを見に行く.
- キャッシュのタイムスタンプがトランザクションのタイムスタンプよりも新しければ DB を見に行く.
みたいな.
うまく直列可能な実行 (serializability) を達成しているように見えます.
トランザクショナルではないという問題はありますが,並行な更新もうまくいくように見えるので,使いどころしだいでは効果的かも.
例えば Amazon なんかで,「在庫2点あります」みたいなのって,必ずしも共有ロックをかけたりとかしてまでトランザクショナルにアクセスする必要があるとは思えません.多少の遅延は許されそうな感じ.っていうか,許されているとしか思えません.
こういう場合には,read-write 戦略は十分に使えるように感じました.
とはいえ,複数の JVM にまたがって,どこの JVM 上で更新されるか分からないような場合は,ちょっと注意が必要になりそうです.
これは,キャッシング戦略ではなく,キャッシュの実装そのものの影響が大きそう.
ま,個人的にはトランザクショナルなキャッシュにより興味があるので,EHCache とかを使おうという気にはなかなかなれません.残念!!!!
そんなわけで (どんなわけで?),次回は「14.3.4. Strategy: nonstrict read/write」へ進みます.
今日のと何が違うのだろうか?