Hibernareの悲観的なロックと楽観的なロック


ロックとは
ビジネスロジックの実現過程では、データアクセスの排他性を保証する必要があることが多い.金融システムの日終決済処理では、あるcut-off時点のデータに対して処理を行い、決済の進行中(数秒種、数時間)にデータが再び変化することを望んでいない.この場合、いくつかのメカニズムを通じて、これらのデータがある操作中に外部に修正されないことを保証する必要があります.このようなメカニズムは、ここで、いわゆる「ロック」、すなわち、選択したターゲットデータにロックをかけ、他のプログラムに修正されないようにする必要があります.
 
Hibernateは2つのロックメカニズムをサポート
Hibernateがサポートする2つのロックメカニズム:すなわち、通常「悲観ロック(Pessimistic Locking)」と「楽観ロック(Optimistic Locking)」と呼ばれる.
 
悲観ロック(Pessimistic Locking)悲観ロックは、その名前のように、本システムの現在の他のトランザクション、および外部システムからのトランザクションを含むデータが外部に修正されることに対して保守的であるため、データ処理全体でデータがロックされている状態を指す.悲観的なロックの実現は、データベースが提供するロックメカニズムに依存することが多い(データベース層が提供するロックメカニズムだけがデータアクセスの排他性を本当に保証することができる.そうしないと、本システムでロックメカニズムを実現しても、外部システムがデータを修正しないことは保証できない).
データベースに依存する典型的な悲観的なロック呼び出し:
select * from account where name=”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();//     ,    

      uery.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)記録メカニズムに基づいて実現されることが多い.
データ・バージョンとは?つまり、データベース・テーブル・ベースのバージョン・ソリューションでは、データベース・テーブルに「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)とともにデータベース更新にコミットした.この場合、コミットデータバージョンがデータベース記録の現在バージョンより大きいため、データが更新され、データベース記録バージョンが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>

注意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例外が投げ出され、バージョンチェックに失敗し、現在のトランザクションが期限切れのデータをコミットしようとしていることを示します.この異常をキャプチャすることで,楽観的ロックチェックに失敗した場合に対応する処理を行うことができる.