Hibernateのキャッシュについて考えてみる(その4)

前回はキャッシュ並列性戦略の概要を確認してみました。今回は CacheConcurrencyStrategy インタフェースにフォーカスしてみようと思います。

インタフェース説明

Implementors manage transactional access to cached data. Transactions pass in a timestamp indicating transaction start time. Two different implementation patterns are provided for.(実装者は、キャッシュデータへのトランザクショナルなアクセスを管理します。トランザクションは、トランザクションの開始時刻を示すタイムスタンプとして渡されます。2つの異なる実装パターンが用意されています。)

  • A transaction-aware cache implementation might be wrapped by a "synchronous" concurrency strategy, where updates to the cache are written to the cache inside the transaction.(トランザクションを意識したキャッシュ実装は、同期された並列性戦略(キャッシュへの更新はトランザクション内で書き込まれる)にラップされているかもしれません。)
  • A non transaction-aware cache would be wrapped by an "asynchronous" concurrency strategy, where items are merely "soft locked" during the transaction and then updated during the "after transaction completion" phase; the soft lock is not an actual lock on the database row - only upon the cached representation of the item.(トランザクションを意識していないキャッシュは、非同期の並列性戦略(トランザクション中に、アイテムが稀に "ソフトロックされた状態" になり、"after transaction completion" フェーズに更新される)にラップされています。ソフトロックは、実際のデータベースの行ロックではなく、item のキャッシュされた表現上のみのものです。)

In terms of entity caches, the expected call sequences are(エンティティキャッシュの視点では、以下の呼び出し順序が期待されています):

  • DELETES : {@link #lock} -> {@link #evict} -> {@link #release}
  • UPDATES : {@link #lock} -> {@link #update} -> {@link #afterUpdate}
  • INSERTS : {@link #insert} -> {@link #afterInsert}

In terms of collection caches, all modification actions actually just invalidate the entry(s). The call sequence here is(コレクションキャッシュの視点では、すべての更新動作は、単純にエントリが invalidate されるだけとなります。この場合の呼び出し順序は):
{@link #lock} -> {@link #evict} -> {@link #release}

Note that, for an asynchronous cache, cache invalidation must be a two step process (lock->release, or lock-afterUpdate), since this is the only way to guarantee consistency with the database for a nontransactional cache implementation. For a synchronous cache, cache invalidation is a single step process (evict, or update). Hence, this interface defines a three step process, to cater for both models.(非同期キャッシュにとっては、キャッシュの invalidation は2つのステップを持つプロセス(lock->release もしくは lock-afterUpdate) ですが、これは、トランザクションを意識しないキャッシュ実装がデータベースとのデータの整合性を保証する唯一の方法だからです。同期キャッシュにとっては、キャッシュの invalidation は単一ステップ(evict もしくは update)のプロセスです。それゆえに、両方のモデルをサポートするため、このインタフェースは3ステップのプロセスを定義しています。)

Note that query result caching does not go through a concurrency strategy; they are managed directly against the underlying {@link Cache cache regions}.(クエリ結果のキャッシングは、並列性戦略を経由せず、下位のキャッシュリージョンに対して直接行われます。)

http://www.hibernate.org/hib_docs/v3/api/org/hibernate/cache/CacheConcurrencyStrategy.html

メソッド(両実装パターン共通)

Object get(Object key, long txTimestamp)

Attempt to retrieve an object from the cache. Mainly used in attempting to resolve entities/collections from the second level cache.(オブジェクトをキャッシュから取得することを試みます。主に、エンティティもしくはコレクションを2次キャッシュから解決するのに使用されます。)

http://www.hibernate.org/hib_docs/v3/api/org/hibernate/cache/CacheConcurrencyStrategy.html#get(java.lang.Object,%20long)
boolean put(Object key, Object value, long txTimestamp, Object version, Comparator versionComparator, boolean minimalPut)

Attempt to cache an object, after loading from the database.(DBからデータをロード後、オブジェクトのキャッシングを試みます。)

http://www.hibernate.org/hib_docs/v3/api/org/hibernate/cache/CacheConcurrencyStrategy.html#put(java.lang.Object,%20java.lang.Object,%20long,%20java.lang.Object,%20java.util.Comparator,%20boolean)
void remove(Object key)

Evict an item from the cache immediately (without regard for transaction isolation).(キャッシュから item を(トランザクション分離レベルに関わらず)即座に退避します。)

http://www.hibernate.org/hib_docs/v3/api/org/hibernate/cache/CacheConcurrencyStrategy.html#remove(java.lang.Object)
void clear()

Evict all items from the cache immediately.(キャッシュからすべての item を即座に削除します。)

http://www.hibernate.org/hib_docs/v3/api/org/hibernate/cache/CacheConcurrencyStrategy.html#clear()

メソッド(非同期系実装パターンのみ)

boolean afterInsert(Object key, Object value, Object version)

Called after an item has been inserted (after the transaction completes), instead of calling release(). This method is used by "asynchronous" concurrency strategies.(release()の呼び出しに先立って、itemが挿入された後(トランザクションが完了した後)に呼び出されます。このメソッドは、非同期の並列性戦略で使用されます。)

http://www.hibernate.org/hib_docs/v3/api/org/hibernate/cache/CacheConcurrencyStrategy.html#afterInsert(java.lang.Object,%20java.lang.Object,%20java.lang.Object)
boolean afterUpdate(Object key, Object value, Object version,CacheConcurrencyStrategy.SoftLock lock)

Called after an item has been updated (after the transaction completes), instead of calling release(). This method is used by "asynchronous" concurrency strategies.(release()の呼び出しに先立って、itemが更新された後(トランザクションが完了した後)に呼び出されます。このメソッドは、非同期の並列性戦略で使用されます。)

http://www.hibernate.org/hib_docs/v3/api/org/hibernate/cache/CacheConcurrencyStrategy.html#afterUpdate(java.lang.Object,%20java.lang.Object,%20java.lang.Object,%20org.hibernate.cache.CacheConcurrencyStrategy.SoftLock)
CacheConcurrencyStrategy.SoftLock lock(Object key, Object version)

We are going to attempt to update/delete the keyed object. This method is used by "asynchronous" concurrency strategies. The returned object must be passed back to release(), to release the lock. Concurrency strategies which do not support client-visible locks may silently return null. (キーで指定されたオブジェクトの更新もしくは削除を試みます。このメソッドは、非同期の並列性戦略で使用されます。返されたオブジェクトは、release()メソッドに渡されなくてはなりません。クライアント可視のロックをサポートしない並列性戦略は、静かにnullを返すかもしれません。)

http://www.hibernate.org/hib_docs/v3/api/org/hibernate/cache/CacheConcurrencyStrategy.html#lock(java.lang.Object,%20java.lang.Object)
void release(Object key, CacheConcurrencyStrategy.SoftLock lock)

Called when we have finished the attempted update/delete (which may or may not have been successful), after transaction completion. This method is used by "asynchronous" concurrency strategies.(成否に関わらず、更新もしくは削除の試みが完了した後(トランザクション完了後)に呼び出されます。このメソッドは、非同期の並列性戦略で使用されます。)

http://www.hibernate.org/hib_docs/v3/api/org/hibernate/cache/CacheConcurrencyStrategy.html#release(java.lang.Object,%20org.hibernate.cache.CacheConcurrencyStrategy.SoftLock)

メソッド(同期系実装パターンのみ)

boolean insert(Object key, Object value, Object currentVersion)

Called after an item has been inserted (before the transaction completes), instead of calling evict(). This method is used by "synchronous" concurrency strategies.(evict() の呼び出しに先立って、item の挿入中(トランザクション完了前)に呼び出されます。このメソッドは、同期の並列性戦略で使用されます。)

http://www.hibernate.org/hib_docs/v3/api/org/hibernate/cache/CacheConcurrencyStrategy.html#insert(java.lang.Object,%20java.lang.Object,%20java.lang.Object)
boolean update(Object key, Object value, Object currentVersion, Object previousVersion)

Called after an item has been updated (before the transaction completes), instead of calling evict(). This method is used by "synchronous" concurrency strategies.(evict()の呼び出しに先立って、item が更新された後(トランザクション完了前)に呼び出されます。このメソッドは、同期の戦略で使用されます。)

http://www.hibernate.org/hib_docs/v3/api/org/hibernate/cache/CacheConcurrencyStrategy.html#update(java.lang.Object,%20java.lang.Object,%20java.lang.Object,%20java.lang.Object)
void evict(Object key)

Called after an item has become stale (before the transaction completes). This method is used by "synchronous" concurrency strategies.(item が古くなった後(トランザクション終了前)に呼び出されます。このメソッドは、同期の並列性戦略で使用されます。)

http://www.hibernate.org/hib_docs/v3/api/org/hibernate/cache/CacheConcurrencyStrategy.html#evict(java.lang.Object)

メソッド(アーキテクチャ系?)

void destroy()

Clean up all resources.(すべてのリソースをクリーンアップします。)

http://www.hibernate.org/hib_docs/v3/api/org/hibernate/cache/CacheConcurrencyStrategy.html#destroy()
Cache getCache()

Get the wrapped cache implementation.(ラップされたキャッシュ実装を返します。)

http://www.hibernate.org/hib_docs/v3/api/org/hibernate/cache/CacheConcurrencyStrategy.html#getCache()
String getRegionName()

Get the cache region name.(キャッシュのリージョン名を取得します。)

http://www.hibernate.org/hib_docs/v3/api/org/hibernate/cache/CacheConcurrencyStrategy.html#getRegionName()
void setCache(Cache cache)

Set the underlying cache implementation.(下位のキャッシュ実装をセットします。)

http://www.hibernate.org/hib_docs/v3/api/org/hibernate/cache/CacheConcurrencyStrategy.html#setCache(org.hibernate.cache.Cache)

同期系と非同期系の違いはこんな感じ?

非同期系
  • トランザクションの外でキャッシュが動作する。
  • lock()はトランザクション内部でかけられる。(実装を見たところ、このタイミングでキャッシュはinvalidateされるようですね。)
  • afterInsert() or afterUpdate() はトランザクション完了後に呼び出され、キャッシュの内容が更新される。このタイミングでは、lock() がかかっているため、古い情報が読まれることはない。(と言いたいところだが、実装がそうなっているかどうかは信用できないなぁ)
  • 対応する CacheConcurrencyStrategy は以下。
同期系
  • キャッシュもJTAトランザクションリソースとして動作する。当然、コミットやロールバックが行われる。
  • 複数のJTAリソースのコミットのタイミングが完全に同期するとは限らないですよね。キャッシュミスするのはいいけど、間違ったデータを読む危険性があるかも。
  • 対応する CacheConcurrencyStrategy は以下。

感想

TransactionalCache

正直、あんまり使いたくないかも。おいらも JTA は、複数の DB とか JMS を組み合わせて使うけど、そういう場合はコミットのタイミングの違いが問題になるような使い方はしないですね。

ReadOnlyCache

アリですね。実際に業務での使用経験もありますが、特に問題は起こっていないし、パフォーマンスも悪くないし。

ReadWriteCache

実は業務で使ったことはありません。通常、業務では分散環境以外ありえないのですが、ReadWriteCache で Cluster Safe のキャッシュプロバイダがないからなぁ。実装もいまいち信用できないし。

NonstrictReadWriteCache

条件付でアリだと思います。業務での利用経験もあります。エンティティのキャッシュ用途のみで使っていて、キャッシュ実装の機能にて分散環境で invalidate させてました。invalidate に失敗しても全然OKなデータに対するキャッシュで、そのデータも定期的にすべてリフレッシュさせてました。

自分でキャッシュプロバイダを作るとしたら。

高速な ReadOnlyCacheと、安心して使える ReadWriteCache を実現できるのが欲しいなぁ。高速な ReadOnlyCache は、キャッシュ側でHibernateのキャッシュ実装を考慮すれば結構イケそう。というのも、現行のキャッシュプロバイダの実装でかなり苦労したので、、、。例えば OSCache はオンメモリでキャッシュする場合でも、キャッシュ実装側のキーとして、Hibernate側から渡されるキーの toString() で取得した値を使ってるから、クエリキャッシュはキーだけで1KBを超えることもザラです。EHCache は分散環境でレプリケーションを行うと、各キャッシュリージョンごとに分散オブジェクト作ってRMIで通信します。マルチキャスト使ってないし、あまつさえデータを毎回シリアライズ -> zip -> デシリアライズ -> unzip しています。設計的にはキレイなんだけど、性能的に難アリなんですよね。

今回のまとめ。

今回は CacheConcurrencyStrategy インタフェースにフォーカスしてみました。

おいら的には、既存のキャッシュプロバイダ実装を前提にすると、条件付で ReadOnlyCache と NonstrictReadWriteCache は使ってもいいかなというところに落ち着きました。でも、満足はしていません。また、ReadWriteCache については、満足する実装があれば使いたいなぁという感じです。ということで、近いうちに満足するものを作っちゃおうと思います。

その前に一応、ReadOnlyCache と ReadWriteCache については、軽く触れておきたいと思います。ということで、次回は ReadOnlyCache を少しばかり触ってみようと思います。