原文はこちら
オブジェクトのロック方式とタイミングを制御することは、アプリケーションロード時のパフォーマンスを最大限に発揮するための重要な要素のひとつである。この章では、OpenJPA の明示的なロック及び暗黙のロックについて記述する。
3.1. デフォルトのロックの設定
OpenJPA のデフォルトトランザクションのreadとwriteロックレベルをopenjpa.ReadLockLevel及びopenjpa.WriteLockLevelのプロパティ設定により制御可能である。それぞれのプロパティに設定可能な値はnone、read、write、またはlock managerで使用される目的で定義されているロックレベルに対応した数値である。OpenJPAはデフォルトではオブジェクトをロックしないので、このプロパティは楽観的トランザクションから非楽観的トランザクションに変更する目的で使用する。
また、openjpa.LockTimeoutのプロパティを設定することにより、OpenJPAがオブジェクトのロックを取得するまでのデフォルト待機時間を制限することが可能である。この値にミリ秒単位の数値で、OpenJPAがロックを取得するまでの待ち時間を指定し、その時間内に取得できなかった場合には例外が投げられる。また、-1は無制限でこれがデフォルト値である。
Example 9.3. デフォルトのロックレベルの設定
3.2. 実行時でのロックレベル設定
実行時において、前述のFetchPlanインターフェイスを介してデフォルトのロックレベルを上書きすることが可能である。各データストアトランザクションの開始時に、OpenJPAはデフォルトのレベルと前のセクションのタイムアウト値でEntityManagerのフェッチプランを初期化する。フェッチプランのロックプロパティを変更することにより、トランザクション内で異なるポイントでロードされたオブジェクトをいかにロックさせるかということを制御することができるようになる。また、個々のQueryのフェッチプランを使用して、(そのロードされたQueryを介してのみ)ロックの変更が可能である。
これらランタイムAPIを使用したロック制御は楽観的トランザクションにおいてのみ有効である。トランザクションの終了時にOpenJPAはフェッチプランのロックレベルをnoneにリセットする。トランザクションの外部からオブジェクトをロックすることはできない。
Example 9.4. ランタイムでのロックレベル設定
3.3. オブジェクトロックAPI
暗黙のロックレベル制御に加え、OpenJPAは、明示的なオブジェクトロック及び現在のロックレベルを取得するAPIを提供している。
基本的なメソッドEntityManager.lock(Object,LockModeType)に加え、以下に示す明示的にオブジェクトロックを行うメソッドをOpenJPAEntityManagerでより公開している。
メソッドがロックレベルやタイムアウトのパラメータを受け付けない場合は、現在のフェッチプランがデフォルト値になる。以下に示す例を実行することによりその動作を検証することができる。
Example 9.5. ロックAPI
3.4. ロックマネージャ
OpenJPAはオブジェクトの実際のロック動作をシステムのorg.apache.openjpa.kernel.LockManagerに委譲している。このプラグインはopenjpa.LockManagerのプロパティ設定により制御される。また、独自のロックマネージャを作成することや、バンドルされたオプションを選択して使用することも可能である。
none : これはorg.apache.openjpa.kernel.NoneLockManagerの別名であり、ロック制御を全く行わない。
version : これはorg.apache.openjpa.kernel.VersionLockManagerの別名である。このロックマネージャは排他的ロックを実行しない代わりに、トランザクションが終了するまでの間、全てのreadロックされたインスタンスのバージョンが変更されていないことを比較することで、読み込みの一貫性を保証するものである。さらにトランザクションの終了時に、対象のオブジェクトに対する更新の有無に関わらず、writeロックにより強制的にバージョンがインクリメントされる。これはノンブロッキングの振る舞いでの読み込み一貫性を保証している。
これはJPAにおけるopenjpa.LockManagerのデフォルトである。
注意
versionロックマネージャにダーティリードを許可しないようにするためには、対象のデータストアのトランザクションの分離レベルを"read committed"またはそれ以上にする必要がある。
Example 9.6. ロックの無効化
3.5. ロックの振る舞いに対するルール
Lazyローディングのような高度な永続化やオブジェクトの一意化により、さまざまなロッキングパターンが生成されるようになった。以下に、そのようなケースでの暗黙ロックの振る舞いのルールの概要を示す。
3.6. 既知の問題及び制約事項
パフォーマンスの観点及びデータベースの制約などにより、ロックが完全に機能するとはいえない。このセクションで指摘した問題点を踏まえ、アプリケーションへの影響を図る必要がある。
3.1. デフォルトのロックの設定
3.2. 実行時でのロックレベル設定
3.3. オブジェクトロックAPI
3.4. ロックマネージャ
3.5. ロックの振る舞いに対するルール
3.6. 既知の問題及び制約事項
オブジェクトのロック方式とタイミングを制御することは、アプリケーションロード時のパフォーマンスを最大限に発揮するための重要な要素のひとつである。この章では、OpenJPA の明示的なロック及び暗黙のロックについて記述する。
3.1. デフォルトのロックの設定
OpenJPA のデフォルトトランザクションのreadとwriteロックレベルをopenjpa.ReadLockLevel及びopenjpa.WriteLockLevelのプロパティ設定により制御可能である。それぞれのプロパティに設定可能な値はnone、read、write、またはlock managerで使用される目的で定義されているロックレベルに対応した数値である。OpenJPAはデフォルトではオブジェクトをロックしないので、このプロパティは楽観的トランザクションから非楽観的トランザクションに変更する目的で使用する。
また、openjpa.LockTimeoutのプロパティを設定することにより、OpenJPAがオブジェクトのロックを取得するまでのデフォルト待機時間を制限することが可能である。この値にミリ秒単位の数値で、OpenJPAがロックを取得するまでの待ち時間を指定し、その時間内に取得できなかった場合には例外が投げられる。また、-1は無制限でこれがデフォルト値である。
Example 9.3. デフォルトのロックレベルの設定
<property name="openjpa.ReadLockLevel" value="none"/>
<property name="openjpa.WriteLockLevel" value="write"/>
<property name="openjpa.LockTimeout" value="30000"/>
3.2. 実行時でのロックレベル設定
実行時において、前述のFetchPlanインターフェイスを介してデフォルトのロックレベルを上書きすることが可能である。各データストアトランザクションの開始時に、OpenJPAはデフォルトのレベルと前のセクションのタイムアウト値でEntityManagerのフェッチプランを初期化する。フェッチプランのロックプロパティを変更することにより、トランザクション内で異なるポイントでロードされたオブジェクトをいかにロックさせるかということを制御することができるようになる。また、個々のQueryのフェッチプランを使用して、(そのロードされたQueryを介してのみ)ロックの変更が可能である。
public LockModeType getReadLockMode();
public FetchPlansetReadLockMode(LockModeType mode);
public LockModeTypegetWriteLockMode();
public FetchPlansetWriteLockMode(LockModeType mode);
long getLockTimeout();
FetchPlan setLockTimeout(long timeout);
これらランタイムAPIを使用したロック制御は楽観的トランザクションにおいてのみ有効である。トランザクションの終了時にOpenJPAはフェッチプランのロックレベルをnoneにリセットする。トランザクションの外部からオブジェクトをロックすることはできない。
Example 9.4. ランタイムでのロックレベル設定
import org.apache.openjpa.persistence.*;
...
EntityManager em = ...;
em.getTransaction().begin();
// writeロックモードで更新する予定のstockをロードする
Query q = em.createQuery("select s from Stock s where symbol = :s");
q.setParameter("s", symbol);
OpenJPAQuery oq = OpenJPAPersistence.cast(q);
FetchPlan fetch = oq.getFetchPlan ();
fetch.setReadLockMode(LockModeType.WRITE);
fetch.setLockTimeout(3000); // 3秒
Stock stock = (Stock) q.getSingleResult();
// ロックの必要がないオブジェクトはnoneロックモードにする
fetch = OpenJPAPersistence.cast(em).getFetchPlan();
fetch.setReadLockMode(null);
Market market = em.find(Market.class, marketId);
stock.setPrice(market.calculatePrice(stock));
em.getTransaction().commit();
3.3. オブジェクトロックAPI
暗黙のロックレベル制御に加え、OpenJPAは、明示的なオブジェクトロック及び現在のロックレベルを取得するAPIを提供している。
public LockModeType OpenJPAEntityManager.getLockMode(Object pc);
指定したオブジェクトの現在のロックのレベルを取得する。
基本的なメソッドEntityManager.lock(Object,LockModeType)に加え、以下に示す明示的にオブジェクトロックを行うメソッドをOpenJPAEntityManagerでより公開している。
public void lock(Object pc);
public void lock(Object pc, LockModeType mode, long timeout);
public void lockAll(Object... pcs);
public void lockAll(Object... pcs, LockModeType mode, long timeout);
public void lockAll(Collection pcs);
public void lockAll(Collection pcs, LockModeType mode, long timeout);
メソッドがロックレベルやタイムアウトのパラメータを受け付けない場合は、現在のフェッチプランがデフォルト値になる。以下に示す例を実行することによりその動作を検証することができる。
Example 9.5. ロックAPI
import org.apache.openjpa.persistence.*;
// オブジェクトのロックレベルを取得する
OpenJPAEntityManager oem = OpenJPAPersistence.cast(em);
Stock stock = ...;
LockModeType level = oem.getLockMode(stock);
if (level == OpenJPAModeType.WRITE) ...
...
oem.setOptimistic(true);
oem.getTransaction().begin ();
// stockオブジェクトをロックするために楽観的トランザクション内でロックなし
// となっているデフォルト値を上書きする
oem.lock(stock, LockModeType.WRITE, 1000);
stock.setPrice(market.calculatePrice(stock));
oem.getTransaction().commit();
3.4. ロックマネージャ
OpenJPAはオブジェクトの実際のロック動作をシステムのorg.apache.openjpa.kernel.LockManagerに委譲している。このプラグインはopenjpa.LockManagerのプロパティ設定により制御される。また、独自のロックマネージャを作成することや、バンドルされたオプションを選択して使用することも可能である。
- pessimistic : これはorg.apache.openjpa.jdbc.kernel.PessimisticLockManagerの別名で、ロックオブジェクトに対応する行をロックするために、SELECT FOR UPDATEステートメント(またはデータベースにより異なる)を使用する。このロックマネージャはreadロックとwriteロックを区別するものではなく、全てのロックはwriteロックとなる。
この悲観的ロックマネージャは、以下に示すように、VersionCheckOnReadLock及びVersionUpdateOnWriteLockのプロパティ設定を行うことにより付加的にversionチェックやversionロックマネージャのインクリメント機能を追加することができる。
<property name="openjpa.LockManager"
value="pessimistic(VersionCheckOnReadLock=true,VersionUpdateOnWriteLock=true)"/>
これはJPAにおけるopenjpa.LockManagerのデフォルトである。
注意
versionロックマネージャにダーティリードを許可しないようにするためには、対象のデータストアのトランザクションの分離レベルを"read committed"またはそれ以上にする必要がある。
Example 9.6. ロックの無効化
<property name="openjpa.LockManager" value="none"/>
3.5. ロックの振る舞いに対するルール
Lazyローディングのような高度な永続化やオブジェクトの一意化により、さまざまなロッキングパターンが生成されるようになった。以下に、そのようなケースでの暗黙ロックの振る舞いのルールの概要を示す。
- トランザクションの内部で初めて状態が読み出されたとき、オブジェクトはフェッチプランの現在のreadロックレベルでロックされる。たとえフェッチプランのレベルが変更されたとしても、その後のオブジェクトに対する付加的なlazy状態読み込みにより、同じreadロックレベルが使用される。
- トランザクションの内部で初めて状態が変更されたとき、たとえフェッチプランのレベルが変更されたとしても、オブジェクトは、それが実際に初めて読み出されたときのwriteロックレベルでロックされる。そのオブジェクトが以前に読み出されていない場合は、現在のwriteロックレベルが使用される。
- オブジェクトが永続関連フィールドを介してアクセスされたとき、その関連オブジェクトは、現在のフェッチプランのロックレベルでロードされ、フィールドが所有するオブジェクトのロックレベルにはならない。
- トランザクションの内部でオブジェクトがアクセスされると、そのオブジェクトは現在のreadロックレベルで再度ロックされる。現在のwriteロック及びreadロックレベルは、上記1、2のルールに従い記憶されたオブジェクトのそれらと同じになる。
- 上記検証で示したAPIによる明示的ロックを行った場合、その特定したレベルで再度ロックが行われる。このレベルは上記1、2のルールに従い記憶されたオブジェクトのread及びwriteレベルと同じになる。
- すでにロックされている状態でロック指定した場合は、そのレベルより低いレベルは効果がない。トランザクションの間ではダウングレードのロックはできない。
3.6. 既知の問題及び制約事項
パフォーマンスの観点及びデータベースの制約などにより、ロックが完全に機能するとはいえない。このセクションで指摘した問題点を踏まえ、アプリケーションへの影響を図る必要がある。
- 一般的に、楽観的トランザクションの間は、OpenJPAは、flushするか楽観的トランザクションをコミットするまで、データベースに実際のトランザクションを開始させない。これにより、データベースのリソースを消費させることなく、長時間のトランザクションを生かすことが可能となる。一方、悲観的ロックマネージャを使用した場合、OpenJPAは悲観的ロックトランザクションの間にオブジェクトをロックすると決定した時点でデータベースのトランザクションを開始する義務を負っている。これは、悲観的ロックマネージャはデータベースのロックを使用していることと、データベースはトランザクションの処理中でなければ行ロックを実行できないという2つの理由からである。OpenJPAはopenjpa.RuntimeロギングチャネルをINFOにしておくことにより、データストアトランザクションがオブジェクトをロックした瞬間をログに出力させることが可能である。
- オブジェクトの状態がロードされたときに適正なパフォーマンスレベルを維持するために、データベースから状態を取得した後で、適切なロックレベルでオブジェクトをロックしているということを保証できるのは、OpenJPAだけである。これは、OpenJPAがオブジェクトをロックする前ではなく、状態を取得した後、他のトランザクションがこっそりデータベースのレコードを書き換えることが技術的に可能であることを意味している。オブジェクトがロックされ、ロック後のオブジェクトの最新の状態を保持することを必ず保証する手法である。
悲観的ロックマネージャを使用する場合は、例えば、joinを使用してSELECT文を発行するような場合は、ロックできないデータベースがありうる。悲観的ロックマネージャでは、データベースの制限によりロックできない初期SELECTの情報をopenjpa.RuntimeロギングチャネルのINFOメッセージで出力する。これらのログメッセージに注意を払うことにより、最新の状態を保持することを保証するためにオブジェクトのリフレッシュを行うべき場所を特定し、早い段階でデータベースの制限によるSELECTロックによる問題を未然に防ぎ、状態のロードする場所を再考する手助けとなる。