Hibernate悲観ロックと楽観ロック
ロック
ビジネスロジックの実現過程では、データアクセスの排他性を保証する必要があることが多い.金融システムの日終決済処理では、決済の進行中(数秒、数時間)、データが再び変化することを望んでいないカットオフポイントのデータを処理したいと考えています.
この場合、いくつかのメカニズムを通じて、これらのデータがある操作中に外部に修正されないことを保証する必要があります.このようなメカニズムは、ここで、いわゆる「ロック」、すなわち、選択したターゲットデータにロックをかけ、他のプログラムに修正されないようにする必要があります.
Hibernateは2つのロックメカニズムをサポートしています.すなわち、一般的に「悲観ロック(Pessimistic Locking)」と「楽観ロック(OptimisticLocking)」と呼ばれています.
悲観ロック(Pessimistic Locking)
悲観的なロックは、その名のように、本システムの現在の他のトランザクション、および外部システムからのトランザクションを含む外部のデータの変更に保守的な態度を持っているため、データ処理中にデータがロックされた状態になることを意味します.悲観的なロックの実現は、データベースが提供するロックメカニズムに依存することが多い(データベース層が提供するロックメカニズムだけがデータアクセスの排他性を本当に保証することができる.そうしないと、本システムでロックメカニズムを実現しても、外部システムがデータを修正しないことは保証できない).
典型的には、データベースに依存して実装される悲観的なロック呼び出しです.
for update句を使用すると、このSQLはaccountテーブル内の検索条件(name="Erica")に一致するすべてのレコードをロックします.このトランザクションがコミットされる前に(トランザクションがコミットされるとトランザクション中のロックが解放されます)、これらのレコードは変更できません.
Hibernateの悲観的なロックは、データベースベースのロックメカニズムでも実現されています.
次のコードは、クエリー・レコードのロックを実現します.
query.setLockModeは、クエリ文の特定のエイリアスに対応するレコードをロックします(「from TUser as user」でTUserクラスに別名「user」を指定します).ここで、返されたすべてのuserレコードをロックします.
実行中のHibernateによって生成されたSQL文を観察します.
Hibernateはデータベースのfor update句を用いて悲観的なロック機構を実現していることがわかる.
Hibernateのロックモードは次のとおりです.
LockMode.NONE:ロック機構なし
LockMode.WRITE:HibernateはInsertとUpdateで記録したときに自動的に取得します
LockMode.READ:Hibernateは記録を読み込む時に自動的に取得する
以上の3つのロックメカニズムは一般にHibernate内部で使用され,例えばHibernateはUpdate中にオブジェクトが外部から修正されないことを保証するためにsaveメソッド実装で自動的にターゲットオブジェクトにWRITEロックを加える.これらは、データベースに関係なくHibernate内部のデータのロックメカニズムです.
LockMode.UPGRADE:データベースのfor update句によるロック
LockMode. UPGRADE_NOWAIT:Oracleの特定の実装、Oracleのfor update nowaitの利用
句はロックを実現する
上記の2つのロックメカニズムは、アプリケーション層でよく使用されるデータベースに依存する悲観的なロックメカニズムです.
ロックは一般的に以下の方法で実現されます.
ただし、クエリが開始される前に(つまりHibernateがSQLを生成する前に)ロックを設定してこそ、データベースのロックメカニズムによってロック処理が行われます.そうしないと、for update句を含まないSelectSQLによってデータがロードされ、データベースのロックとは言えません.
楽観ロック
悲観的ロックに対して、楽観的ロックメカニズムは、より緩やかなロックメカニズムを採用している.悲観的なロックは、ほとんどの場合、データベースのロックメカニズムに依存して実現され、操作の最大限の独占性を保証します.しかし、これに伴い、データベースのパフォーマンスに多くのオーバーヘッドが発生します.特に、長いトランザクションでは、このようなオーバーヘッドは耐えられません.
金融システムのように、あるオペレータがユーザのデータを読み取り、読み出したユーザデータに基づいて修正を行う場合(ユーザ口座残高の変更など)、その全過程で悲観的なロックメカニズムを採用すれば、操作過程全体(オペレータがデータを読み取り、修正結果を提出するまでの全過程、さらにオペレータが途中でコーヒーを沸かす時間を含む)を意味する.データベース・レコードは常にロックされており、数百件以上の同時発生に直面した場合、どのような結果をもたらすかが考えられます.
楽観的ロックメカニズムはこの問題をある程度解決した.楽観的なロックは、データバージョン(Version)記録メカニズムに基づいて実現されることが多い.データ・バージョンとは?つまり、データベース・テーブル・ベースのバージョン・ソリューションでは、データベース・テーブルに「version」フィールドを追加することで、データにバージョンIDを追加します.
データを読み出した場合は、このバージョン番号を一緒に読み出し、その後更新した場合は、このバージョン番号に1を加算します.このとき、コミットされたデータのバージョンデータは、データベーステーブルに対応して記録された現在のバージョン情報と比較され、コミットされたデータのバージョン番号がデータベーステーブルの現在のバージョン番号より大きい場合は更新され、そうでない場合は期限切れのデータとみなされます.
上記のユーザーアカウント情報を変更した例では、データベース内のアカウント情報テーブルにversionフィールドがあり、現在の値は1であると仮定します.現在の口座残高フィールド(balance)は$100です.
1.オペレータAはこれを読み出し(version=1)、口座残高から$50($100-$50)を差し引く.
2.オペレータAの操作中に、オペレータBもこのユーザ情報(version=1)を読み込み、その口座残高から$20($100-$20)を差し引く.
3.オペレータAは修正作業を完了し、データバージョン番号を1(version=2)加算し、口座控除後残高とともに
(balance=$50)は、データベース更新にコミットされます.この場合、コミットされたデータのバージョンがデータベース記録の現在のバージョンより大きいため、データが更新され、データベース記録versionが2に更新されます.
4.オペレータBは操作を完了し、バージョン番号プラス1(version=2)もデータベースにデータを提出しようとした(balance=$80)が、データベースの記録バージョンに比べて、オペレータBが提出したデータバージョン番号は2であり、データベースの記録現在バージョンも2であり、「提出バージョンは現在のバージョンを記録してから更新を実行しなければならない」という楽観的なロックポリシーを満たさないため、オペレータBの提出は却下された.これにより、オペレータBがversion=1に基づく古いデータで修正した結果でオペレータAの操作結果を上書きする可能性が回避される.
上記の例から、楽観的なロックメカニズムは、長いトランザクションにおけるデータベースのロックオーバーヘッド(オペレータAとオペレータBの操作中、データベースデータにロックをかけていない)を回避し、大きな同時量でのシステム全体の性能表現を大幅に向上させることが明らかになった.
なお、楽観ロックメカニズムは、システム内のデータ記憶ロジックに基づいていることが多いため、上記の例では、楽観ロックメカニズムは私たちのシステムで実現されるため、外部システムからのユーザー残高更新操作は私たちのシステムの制御を受けないため、不正なデータがデータベースに更新される可能性があるという限界もある.
システム設計の段階では、これらの状況が発生する可能性を十分に考慮し、データベース・テーブルを直接公開するのではなく、楽観的なロック・ポリシーをデータベース・ストレージ・プロセスで実現するなど、このストレージ・プロセスに基づくデータ更新ルートのみを外部に開放する調整を行う必要があります.
Hibernateは、データ・アクセス・エンジンに楽観的なロックを内蔵しています.外部システムによるデータベースの更新操作を考慮する必要がなければ、Hibernateが提供する透明化された楽観的なロックを利用して実現することで、生産性が大幅に向上します.
Hibernateではclass記述子のoptimistic-lock属性をversion記述子と組み合わせて指定できます.
次に、前の例のTUserに楽観的なロックメカニズムを追加します.
1.まずTUserのclass記述子にoptimistic-lock属性を追加する.
optimistic-lockプロパティには、次のオプションの値があります.
none
楽観的なロックはありません.
version
バージョンメカニズムにより楽観的なロックを実現します.
dirty
変動した属性をチェックすることで楽観的なロックを実現します.
all
すべてのプロパティをチェックすることで楽観的なロックを実現します.
このうちversionによる楽観ロックメカニズムはHibernateが公式に推奨する楽観ロック実装であり、Hibernateでもあり、現在はエンティティオブジェクトがSessionから離れて修正された場合にのみ有効なロックメカニズムである.従って,一般的には,Hibernate楽観ロック実装機構としてversion方式を選択した.
2.Version属性記述子を追加
なお、バージョンノードはIDノードの後に表示される必要があります.
ここではversionプロパティを宣言し、ユーザーのバージョン情報を保存し、T_に保存します.Userテーブルのversionフィールドにあります.
このとき、TUserテーブルに記録されているデータを更新するコードを作成しようとします.
Tuserを更新するたびに、データベース内のversionが増加していることがわかります.
一方、tx.commitの前に別のセッションを起動し、Ericaというユーザーを操作して、同時更新時の状況をシミュレートしようとします.
以上のコードを実行すると、tx.commit()にStaleObjectStateException例外が投げ出され、バージョンチェックに失敗し、現在のトランザクションが期限切れのデータをコミットしようとしていることを示します.この異常をキャプチャすることで,楽観的ロックチェックに失敗した場合に対応する処理を行うことができる.
ビジネスロジックの実現過程では、データアクセスの排他性を保証する必要があることが多い.金融システムの日終決済処理では、決済の進行中(数秒、数時間)、データが再び変化することを望んでいないカットオフポイントのデータを処理したいと考えています.
この場合、いくつかのメカニズムを通じて、これらのデータがある操作中に外部に修正されないことを保証する必要があります.このようなメカニズムは、ここで、いわゆる「ロック」、すなわち、選択したターゲットデータにロックをかけ、他のプログラムに修正されないようにする必要があります.
Hibernateは2つのロックメカニズムをサポートしています.すなわち、一般的に「悲観ロック(Pessimistic Locking)」と「楽観ロック(OptimisticLocking)」と呼ばれています.
悲観ロック(Pessimistic Locking)
悲観的なロックは、その名のように、本システムの現在の他のトランザクション、および外部システムからのトランザクションを含む外部のデータの変更に保守的な態度を持っているため、データ処理中にデータがロックされた状態になることを意味します.悲観的なロックの実現は、データベースが提供するロックメカニズムに依存することが多い(データベース層が提供するロックメカニズムだけがデータアクセスの排他性を本当に保証することができる.そうしないと、本システムでロックメカニズムを実現しても、外部システムがデータを修正しないことは保証できない).
典型的には、データベースに依存して実装される悲観的なロック呼び出しです.
select * from account where name="Erica " for update
for update句を使用すると、このSQLはaccountテーブル内の検索条件(name="Erica")に一致するすべてのレコードをロックします.このトランザクションがコミットされる前に(トランザクションがコミットされるとトランザクション中のロックが解放されます)、これらのレコードは変更できません.
Hibernateの悲観的なロックは、データベースベースのロックメカニズムでも実現されています.
次のコードは、クエリー・レコードのロックを実現します.
String hqlStr ="from TUser as user where user.name='Erica'";
Query query = session.createQuery(hqlStr);
query.setLockMode("user",LockMode.UPGRADE); //
List userList = query.list();// ,
query.setLockModeは、クエリ文の特定のエイリアスに対応するレコードをロックします(「from TUser as user」でTUserクラスに別名「user」を指定します).ここで、返されたすべてのuserレコードをロックします.
実行中のHibernateによって生成されたSQL文を観察します.
select tuser0_.id as id, tuser0_.name as name, tuser0_.group_id as group_id,
tuser0_.user_type as user_type, tuser0_.sex as sex from t_user tuser0_ where
(tuser0_.name='Erica' ) for update
Hibernateはデータベースのfor update句を用いて悲観的なロック機構を実現していることがわかる.
Hibernateのロックモードは次のとおりです.
LockMode.NONE:ロック機構なし
LockMode.WRITE:HibernateはInsertとUpdateで記録したときに自動的に取得します
LockMode.READ:Hibernateは記録を読み込む時に自動的に取得する
以上の3つのロックメカニズムは一般にHibernate内部で使用され,例えばHibernateはUpdate中にオブジェクトが外部から修正されないことを保証するためにsaveメソッド実装で自動的にターゲットオブジェクトにWRITEロックを加える.これらは、データベースに関係なくHibernate内部のデータのロックメカニズムです.
LockMode.UPGRADE:データベースのfor update句によるロック
LockMode. UPGRADE_NOWAIT:Oracleの特定の実装、Oracleのfor update nowaitの利用
句はロックを実現する
上記の2つのロックメカニズムは、アプリケーション層でよく使用されるデータベースに依存する悲観的なロックメカニズムです.
ロックは一般的に以下の方法で実現されます.
Criteria.setLockMode
Query.setLockMode
Session.lock
ただし、クエリが開始される前に(つまりHibernateがSQLを生成する前に)ロックを設定してこそ、データベースのロックメカニズムによってロック処理が行われます.そうしないと、for update句を含まないSelectSQLによってデータがロードされ、データベースのロックとは言えません.
楽観ロック
悲観的ロックに対して、楽観的ロックメカニズムは、より緩やかなロックメカニズムを採用している.悲観的なロックは、ほとんどの場合、データベースのロックメカニズムに依存して実現され、操作の最大限の独占性を保証します.しかし、これに伴い、データベースのパフォーマンスに多くのオーバーヘッドが発生します.特に、長いトランザクションでは、このようなオーバーヘッドは耐えられません.
金融システムのように、あるオペレータがユーザのデータを読み取り、読み出したユーザデータに基づいて修正を行う場合(ユーザ口座残高の変更など)、その全過程で悲観的なロックメカニズムを採用すれば、操作過程全体(オペレータがデータを読み取り、修正結果を提出するまでの全過程、さらにオペレータが途中でコーヒーを沸かす時間を含む)を意味する.データベース・レコードは常にロックされており、数百件以上の同時発生に直面した場合、どのような結果をもたらすかが考えられます.
楽観的ロックメカニズムはこの問題をある程度解決した.楽観的なロックは、データバージョン(Version)記録メカニズムに基づいて実現されることが多い.データ・バージョンとは?つまり、データベース・テーブル・ベースのバージョン・ソリューションでは、データベース・テーブルに「version」フィールドを追加することで、データにバージョンIDを追加します.
データを読み出した場合は、このバージョン番号を一緒に読み出し、その後更新した場合は、このバージョン番号に1を加算します.このとき、コミットされたデータのバージョンデータは、データベーステーブルに対応して記録された現在のバージョン情報と比較され、コミットされたデータのバージョン番号がデータベーステーブルの現在のバージョン番号より大きい場合は更新され、そうでない場合は期限切れのデータとみなされます.
上記のユーザーアカウント情報を変更した例では、データベース内のアカウント情報テーブルにversionフィールドがあり、現在の値は1であると仮定します.現在の口座残高フィールド(balance)は$100です.
1.オペレータAはこれを読み出し(version=1)、口座残高から$50($100-$50)を差し引く.
2.オペレータAの操作中に、オペレータBもこのユーザ情報(version=1)を読み込み、その口座残高から$20($100-$20)を差し引く.
3.オペレータAは修正作業を完了し、データバージョン番号を1(version=2)加算し、口座控除後残高とともに
(balance=$50)は、データベース更新にコミットされます.この場合、コミットされたデータのバージョンがデータベース記録の現在のバージョンより大きいため、データが更新され、データベース記録versionが2に更新されます.
4.オペレータBは操作を完了し、バージョン番号プラス1(version=2)もデータベースにデータを提出しようとした(balance=$80)が、データベースの記録バージョンに比べて、オペレータBが提出したデータバージョン番号は2であり、データベースの記録現在バージョンも2であり、「提出バージョンは現在のバージョンを記録してから更新を実行しなければならない」という楽観的なロックポリシーを満たさないため、オペレータBの提出は却下された.これにより、オペレータBがversion=1に基づく古いデータで修正した結果でオペレータAの操作結果を上書きする可能性が回避される.
上記の例から、楽観的なロックメカニズムは、長いトランザクションにおけるデータベースのロックオーバーヘッド(オペレータAとオペレータBの操作中、データベースデータにロックをかけていない)を回避し、大きな同時量でのシステム全体の性能表現を大幅に向上させることが明らかになった.
なお、楽観ロックメカニズムは、システム内のデータ記憶ロジックに基づいていることが多いため、上記の例では、楽観ロックメカニズムは私たちのシステムで実現されるため、外部システムからのユーザー残高更新操作は私たちのシステムの制御を受けないため、不正なデータがデータベースに更新される可能性があるという限界もある.
システム設計の段階では、これらの状況が発生する可能性を十分に考慮し、データベース・テーブルを直接公開するのではなく、楽観的なロック・ポリシーをデータベース・ストレージ・プロセスで実現するなど、このストレージ・プロセスに基づくデータ更新ルートのみを外部に開放する調整を行う必要があります.
Hibernateは、データ・アクセス・エンジンに楽観的なロックを内蔵しています.外部システムによるデータベースの更新操作を考慮する必要がなければ、Hibernateが提供する透明化された楽観的なロックを利用して実現することで、生産性が大幅に向上します.
Hibernateではclass記述子のoptimistic-lock属性をversion記述子と組み合わせて指定できます.
次に、前の例のTUserに楽観的なロックメカニズムを追加します.
1.まずTUserのclass記述子にoptimistic-lock属性を追加する.
<hibernate-mapping>
<class
name="org.hibernate.sample.TUser"
table="t_user"
dynamic-update="true"
dynamic-insert="true"
optimistic-lock="version"
>
……
</class>
</hibernate-mapping>
optimistic-lockプロパティには、次のオプションの値があります.
none
楽観的なロックはありません.
version
バージョンメカニズムにより楽観的なロックを実現します.
dirty
変動した属性をチェックすることで楽観的なロックを実現します.
all
すべてのプロパティをチェックすることで楽観的なロックを実現します.
このうちversionによる楽観ロックメカニズムはHibernateが公式に推奨する楽観ロック実装であり、Hibernateでもあり、現在はエンティティオブジェクトがSessionから離れて修正された場合にのみ有効なロックメカニズムである.従って,一般的には,Hibernate楽観ロック実装機構としてversion方式を選択した.
2.Version属性記述子を追加
<hibernate-mapping>
<class
name="org.hibernate.sample.TUser"
table="t_user"
dynamic-update="true"
dynamic-insert="true"
optimistic-lock="version"
>
<id
name="id"
column="id"
type="java.lang.Integer"
>
<generator class="native">
</generator>
</id>
<version
column="version"
name="version"
type="java.lang.Integer"
/>
……
</class>
</hibernate-mapping>
なお、バージョンノードはIDノードの後に表示される必要があります.
ここではversionプロパティを宣言し、ユーザーのバージョン情報を保存し、T_に保存します.Userテーブルのversionフィールドにあります.
このとき、TUserテーブルに記録されているデータを更新するコードを作成しようとします.
Criteria criteria = session.createCriteria(TUser.class);
criteria.add(Expression.eq("name","Erica"));
List userList = criteria.list();
TUser user =(TUser)userList.get(0);
Transaction tx = session.beginTransaction();
user.setUserType(1); // UserType
tx.commit();
Tuserを更新するたびに、データベース内のversionが増加していることがわかります.
一方、tx.commitの前に別のセッションを起動し、Ericaというユーザーを操作して、同時更新時の状況をシミュレートしようとします.
Session session= getSession();
Criteria criteria = session.createCriteria(TUser.class);
criteria.add(Expression.eq("name","Erica"));
Session session2 = getSession();
Criteria criteria2 = session2.createCriteria(TUser.class);
criteria2.add(Expression.eq("name","Erica"));
List userList = criteria.list();
List userList2 = criteria2.list();
TUser user =(TUser)userList.get(0);
TUser user2 =(TUser)userList2.get(0);
Transaction tx = session.beginTransaction();
Transaction tx2 = session2.beginTransaction();
user2.setUserType(99);
tx2.commit();
user.setUserType(1);
tx.commit();
以上のコードを実行すると、tx.commit()にStaleObjectStateException例外が投げ出され、バージョンチェックに失敗し、現在のトランザクションが期限切れのデータをコミットしようとしていることを示します.この異常をキャプチャすることで,楽観的ロックチェックに失敗した場合に対応する処理を行うことができる.