Java悲観ロックと楽観ロックの実現
11301 ワード
ロック
ビジネスロジックの実現過程では、データアクセスの排他性を保証する必要があることが多い.金融システムの日終決済処理では、あるcut-off時点のデータを処理したいと考えています.決済の進行中(数秒でも数時間でも)データが変化することを望んでいません.この場合、いくつかのメカニズムを通じて、これらのデータがある操作中に外部に修正されないことを保証する必要があります.このようなメカニズムは、ここで、いわゆる「ロック」です.つまり、選択したターゲットデータにロックをかけ、他のプログラムで変更できないようにします.
Hibernateは2つのロックメカニズムをサポートしています.すなわち、一般的に「悲観ロック(Pessimistic Locking)」と「楽観ロック(Optimistic Locking)」と呼ばれています.
悲観ロック(Pessimistic Locking)
悲観的なロックは、その名の通り、これは、データが外部(本システムの現在の他のトランザクション、および外部システムからのトランザクションを含む)によって修正されることに対して保守的な態度を持つため、データ処理全体の過程でデータをロック状態にすることを指す.悲観的なロックの実現は、データベースが提供するロックメカニズムに依存することが多い.(データベース・レイヤが提供するロック・メカニズムのみが、データ・アクセスの排他性を確実に保証します.そうしないと、本システムでロック・メカニズムが実現しても、外部システムがデータを変更しないことは保証されません).
データベースに依存する典型的な悲観的なロック呼び出し:
select*from account where="Erica"for updateこのsql文は、accountテーブル内の検索条件(name="Erica")に合致するすべてのレコードをロックします.
このトランザクションがコミットされる前に(トランザクションがコミットされるとトランザクション中のロックが解放されます)、これらのレコードは変更できません.
Hibernateの悲観的なロックは、データベースベースのロックメカニズムでも実現されています.次のコードは、クエリー・レコードのロックを実現します.
ここでHibernateは,データベースのfor update句を用いて悲観的なロック機構を実現した.
Hibernateのロックモードは次のとおりです.
Ø LockMode.NONE:ロック機構なし.
Ø LockMode.WRITE:HibernateはInsertとUpdateで記録したときに自動的に取得します.
Ø LockMode.READ:Hibernateは記録を読み込む時に自動的に取得します.
以上の3つのロックメカニズムは一般にHibernate内部で使用され,例えばHibernateはUpdate中にオブジェクトが外部に修正されないことを保証するためにsaveメソッド実装において自動的にターゲットオブジェクトにWRITEロックを加える.
Ø LockMode.UPGRADE:データベースのfor update句を使用してロックします.
Ø LockMode. UPGRADE_NOWAIT:Oracleの特定のインプリメンテーションで、Oracleのfor update nowait句を使用してロックを実行します.
上記の2つのロックメカニズムは、アプリケーション層で一般的に使用されています.ロックは、一般的に以下の方法で実現されます.
Criteria.setLockMode
Query.setLockMode
Session.lock
ただし、クエリが開始される前に(つまりHiberateがSQLを生成する前に)ロックを設定してこそ、データベースのロックメカニズムによってロック処理が行われます.そうしないと、for update句を含まないSelectSQLによってデータがロードされ、データベースのロックとは言えません.
楽観ロック
悲観的ロックに対して、楽観的ロックメカニズムは、より緩やかなロックメカニズムを採用している.悲観的なロックは、ほとんどの場合、データベースのロックメカニズムに依存して実現され、操作の最大限の独占性を保証します.しかし、これに伴い、データベースのパフォーマンスに多くのオーバーヘッドが発生します.特に、長いトランザクションでは、このようなオーバーヘッドは耐えられません.
あるオペレーターがユーザーのデータを読み取る金融システムのように、そして、読み出したユーザデータに基づいて修正を行う場合(ユーザ勘定残高の変更など)、悲観的なロックメカニズムを採用すると、操作中全体を意味します.(オペレータがデータを読み出し、修正を開始してから修正結果を提出するまでの全過程、さらにはオペレータが途中でコーヒーを沸かす時間も含まれている)では、データベース記録は常にロック状態にあり、数百人以上の合併に直面すれば、このような状況がどのような結果をもたらすかが考えられる.
楽観的ロックメカニズムはこの問題をある程度解決した.楽観的なロックは、ほとんどがデータ・バージョン(Version)レコード・メカニズムに基づいて実現されます.データ・バージョンとは、データにバージョンIDを追加することです.データベース・テーブル・ベースのバージョン・ソリューションでは、一般的にデータベース・テーブルに「version」フィールドを追加することによって実現されます.
データを読み出すときは、このバージョン番号とともに読み出し、その後更新するときは、このバージョン番号に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)とともにデータベース更新にコミットした.このとき、コミットデータバージョンがデータベース記録現在バージョンより大きいため、データが更新され、データベース記録バージョンが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では、現在、データオブジェクトがセッションから離れて変更されている場合にのみ有効なロックマシンです.
を作成します.従って,一般的には,Hibernate楽観ロック実装機構としてversion方式を選択した.
2.Version属性記述子を追加
注意versionノードはIDノードの後に表示される必要があります.
ここでは、TUserテーブルのversionフィールドに保存するユーザーのバージョン情報を格納するためのversionプロパティを宣言します.
コードを作成してTUserテーブルに記録されたデータを更新しようとすると、次のようになります.
Tuserを更新するたびに、データベース内のversionが増加していることがわかります.一方、tx.commitの前に別のセッションを起動し、Ericaというユーザーを操作して、同時更新時の状況をシミュレートしようとします.
以上のコードを実行すると、tx.commit()にStaleObjectStateException例外が投げ出され、バージョンチェックに失敗し、現在のトランザクションが期限切れのデータをコミットしようとしていることを示します.この異常をキャプチャすることで,楽観的ロックチェックに失敗した場合に対応する処理を行うことができる.
ビジネスロジックの実現過程では、データアクセスの排他性を保証する必要があることが多い.金融システムの日終決済処理では、あるcut-off時点のデータを処理したいと考えています.決済の進行中(数秒でも数時間でも)データが変化することを望んでいません.この場合、いくつかのメカニズムを通じて、これらのデータがある操作中に外部に修正されないことを保証する必要があります.このようなメカニズムは、ここで、いわゆる「ロック」です.つまり、選択したターゲットデータにロックをかけ、他のプログラムで変更できないようにします.
Hibernateは2つのロックメカニズムをサポートしています.すなわち、一般的に「悲観ロック(Pessimistic Locking)」と「楽観ロック(Optimistic Locking)」と呼ばれています.
悲観ロック(Pessimistic Locking)
悲観的なロックは、その名の通り、これは、データが外部(本システムの現在の他のトランザクション、および外部システムからのトランザクションを含む)によって修正されることに対して保守的な態度を持つため、データ処理全体の過程でデータをロック状態にすることを指す.悲観的なロックの実現は、データベースが提供するロックメカニズムに依存することが多い.(データベース・レイヤが提供するロック・メカニズムのみが、データ・アクセスの排他性を確実に保証します.そうしないと、本システムでロック・メカニズムが実現しても、外部システムがデータを変更しないことは保証されません).
データベースに依存する典型的な悲観的なロック呼び出し:
select*from account where="Erica"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 , (
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ロックを加える.
Ø LockMode.UPGRADE:データベースのfor update句を使用してロックします.
Ø LockMode. UPGRADE_NOWAIT:Oracleの特定のインプリメンテーションで、Oracleのfor update nowait句を使用してロックを実行します.
上記の2つのロックメカニズムは、アプリケーション層で一般的に使用されています.ロックは、一般的に以下の方法で実現されます.
Criteria.setLockMode
Query.setLockMode
Session.lock
ただし、クエリが開始される前に(つまりHiberateがSQLを生成する前に)ロックを設定してこそ、データベースのロックメカニズムによってロック処理が行われます.そうしないと、for update句を含まないSelectSQLによってデータがロードされ、データベースのロックとは言えません.
楽観ロック
悲観的ロックに対して、楽観的ロックメカニズムは、より緩やかなロックメカニズムを採用している.悲観的なロックは、ほとんどの場合、データベースのロックメカニズムに依存して実現され、操作の最大限の独占性を保証します.しかし、これに伴い、データベースのパフォーマンスに多くのオーバーヘッドが発生します.特に、長いトランザクションでは、このようなオーバーヘッドは耐えられません.
あるオペレーターがユーザーのデータを読み取る金融システムのように、そして、読み出したユーザデータに基づいて修正を行う場合(ユーザ勘定残高の変更など)、悲観的なロックメカニズムを採用すると、操作中全体を意味します.(オペレータがデータを読み出し、修正を開始してから修正結果を提出するまでの全過程、さらにはオペレータが途中でコーヒーを沸かす時間も含まれている)では、データベース記録は常にロック状態にあり、数百人以上の合併に直面すれば、このような状況がどのような結果をもたらすかが考えられる.
楽観的ロックメカニズムはこの問題をある程度解決した.楽観的なロックは、ほとんどがデータ・バージョン(Version)レコード・メカニズムに基づいて実現されます.データ・バージョンとは、データにバージョンIDを追加することです.データベース・テーブル・ベースのバージョン・ソリューションでは、一般的にデータベース・テーブルに「version」フィールドを追加することによって実現されます.
データを読み出すときは、このバージョン番号とともに読み出し、その後更新するときは、このバージョン番号に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)とともにデータベース更新にコミットした.このとき、コミットデータバージョンがデータベース記録現在バージョンより大きいため、データが更新され、データベース記録バージョンが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では、現在、データオブジェクトがセッションから離れて変更されている場合にのみ有効なロックマシンです.
を作成します.従って,一般的には,Hibernate楽観ロック実装機構としてversion方式を選択した.
2.Version属性記述子を追加
<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"
>
class="native">
id>
<version
column="version"
name="version"
type="java.lang.Integer"
/>
……
class>
注意versionノードはIDノードの後に表示される必要があります.
ここでは、TUserテーブルのversionフィールドに保存するユーザーのバージョン情報を格納するための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例外が投げ出され、バージョンチェックに失敗し、現在のトランザクションが期限切れのデータをコミットしようとしていることを示します.この異常をキャプチャすることで,楽観的ロックチェックに失敗した場合に対応する処理を行うことができる.