Top / Hibernate / 悲観的ロックを実装する

悲観的ロックとは

※私はデータベースの専門家でもないですし、OracleやHibernateのエキスパートでもないのでココに書いてある内容は正しいかどうか保証できません。あしからずご了承ください

Hibernate/楽観的ロックを実装する で楽観的ロックの実装については試してみました。次は悲観的ロックを試してみようと思います。悲観的ロックとは、複数のトランザクションが同じrowを更新してしまわないように、トランザクション中はそのrowを占有ロックしてしまおうという方法です。あるトランザクションがあるrowに対して悲観的ロックをかけると、他のトランザクションはそのrowを参照することができなくなります。ロックを取得したトランザクションが終了するまで待ちになったり、参照しようとした時点で例外になったりします。ようするにOracleなどでいうところのselect 〜 for updateの事ですね。

ところでこのOracleのselect for updateですが、私は勘違いしていました。つまり

トラン1がselect for update で占有ロックする
トラン2がselectしようとしたら、待ちor例外

だと思っていたんですが、違いました。正しくは

トラン1がselect for update で占有ロックする
トラン2がselect for update しようとしたら、待ちor例外

なんですね。つまりあるトランザクションが占有ロックをしても、単純なselectは実行できちゃいました。これって正しい動き???知ってるヒト、教えてください。環境はOracle 10g XEです。

Hibernateでの悲観的ロックの実装方法

Hibernateで悲観的ロックを取得するには、update前のselect時に、ロックするという指定をしておけばOKです。具体的には

UserAttr user = (UserAttr) session.load(UserAttr.class, key,LockMode.UPGRADE);

とloadやget時に LockMode?.UPGRADE という占有ロックするという指定をするだけです。

やってみる

テーブル構成などは Hibernate/Springを使ってトランザクション処理を記述する の構成を用いてサンプルを実行してみます。やっている内容は

  • トランザクション1のスレッドがあるrowを
     session.load(UserAttr.class, key,LockMode.UPGRADE);
    という指定で検索
  • トランザクション2のスレッドが同じrowを
     session.load(UserAttr.class, key,LockMode.UPGRADE);
    という指定で検索。ここでこのトランザクション2は待ち状態になる
  • トランザクション1のスレッドがそのrowを変更してコミット
  • トランザクション2の待ち状態が解除され、このトランザクションの上の検索が実行され、トランザクション1の変更を反映させたインスタンスが取得される

ということですね。

ソース

詳細はソースを見て欲しいですが、抜粋するとこんな感じです。

new Thread(new Runnable() {
  public void run() {
    Session session = sessionFactory.openSession();
    Transaction tx = session.beginTransaction();
    System.out.println("トラ1開始");
    // select for updateする
    UserAttr user = (UserAttr) session.load(UserAttr.class, key, LockMode.UPGRADE);
    System.out.println("トラ1の取得結果" + user);
    // このトランザクション内でカラムを変更し
    user.setName("Thread1:" + System.currentTimeMillis());
    waitt(3000);

    // 上で3000msわざと待ちにしたこのスキにトランザクション2のスレッドが動き出す
    // この間にアクセスしたトランはロック解除待ちになるはず
    tx.commit();
    session.close();
    System.out.println("トラ1終了");
    // 最後に表示する。
    print();
    System.out.println("トラ1終了");

  }

}).start();

new Thread(new Runnable() {
  public void run() {
    waitt(1000);
    Session session = sessionFactory.openSession();
    Transaction tx = session.beginTransaction();
    System.out.println("トラ2開始");
    // 普通にselectしようとする。待ちにはいるはず
    UserAttr user = (UserAttr) session.load(UserAttr.class, key,
        LockMode.UPGRADE);// ココもロックをつけないと、検索できちゃうんだね。
    System.out.println("トラ2の取得結果" + user);
    tx.commit();
    session.close();
    System.out.println("トラ2終了");
  }
}).start();

実行結果

実行結果は以下の通り

org.hibernate.impl.SessionFactoryImpl - building session factory
ユーザ作成のトラン開始
org.hibernate.SQL - insert into MKINO.USER_ATTR (NAME, USERID) values (?, ?)
ユーザ作成のトラン終了
Keyは abc40619
トラ1開始
[Thread-0] DEBUG org.hibernate.SQL - select userattr0_.USERID as USERID0_0_, userattr0_.NAME as NAME0_0_ 
  from MKINO.USER_ATTR userattr0_ where userattr0_.USERID=? for update
トラ1の取得結果nu.mine.kino.entity.UserAttr@10e35d5[id=abc40619,name=サンプルユーザ]
トラ2開始
[Thread-1] DEBUG org.hibernate.SQL - select userattr0_.USERID as USERID0_0_, userattr0_.NAME as NAME0_0_ 
  from MKINO.USER_ATTR userattr0_ where userattr0_.USERID=? for update
[Thread-0] DEBUG org.hibernate.SQL - update MKINO.USER_ATTR set NAME=? where USERID=?
トラ2の取得結果nu.mine.kino.entity.UserAttr@6a3960[id=abc40619,name=Thread1:1178297648171]
トラ1終了
トラ2終了

このように

  • selectはloadメソッドの指定により、select for updateになっている
  • トランザクション1の途中(スレッドを待たせてるから)にトランザクション2を開始したにもかかわらず、トラン1のupdateがコミットされるまで、トラン2は結果を取得できていない。
  • そのためトラン2のselect for update はトラン1のupdateが反映されたモノが取得され、表示された

となりました。確かにトランザクション2は待ち状態になったようです。

待ちではなく、あきらめる

待ち状態にするのではなく、すぐにあきらめるようにするにはロックの指定を

前:session.load(UserAttr.class, key,LockMode.UPGRADE);
後:session.load(UserAttr.class, key,LockMode.UPGRADE_NOWAIT);

に変更します。実行すると

Exception in thread "Thread-1" org.hibernate.exception.LockAcquisitionException:
could not load an entity: [nu.mine.kino.entity.UserAttr#abc384]

という例外がスローされました。


この記事は

選択肢 投票
おもしろかった 58  
そうでもない 12  
  • トラ1がSELECT〜FOR UPDATEしたとき、トラ2がFOR UPDATEなしのSELECTならば更新前のレコードを読みます。SELECT〜FOR UPDATEならばトラ1がCOMMIT/ROLLBACKするまで待機します。SELECT〜FOR UPDATE NOWAITすると待機せずにORA-OOO54(リソース・ビジー)となります。SELECT〜FOR UPDATE WAIT nならば、n秒待機後、ORA-OOO54(リソース・ビジー)となります。UPDATE/DELETEは待機時間を指定できないので無限に待機します。これで補足になりますでしょうか。 -- 2008-03-12 (水) 00:45:28
  • 若干追加。「トラ1がSELECT〜FOR UPDATEしたとき、トラ2がFOR UPDATEなしのSELECTならば」どう読むか、は、読み込み一貫性の設定にもよります。「トランザクション分離レベル」でググってみるといろいろと書いてあります。わかりやすいのはこの辺かな? http://d.hatena.ne.jp/zecl/20080204/p3 -- 2008-07-31 (木) 17:36:37
  • 半年以上たって亀レスですが、、、トラ2がfor Updateつけないでselectする場合は「読みたいだけ」なんで、コミット前なら「更新前レコードを読む」コミット後なら「更新後レコードを読む(READ COMMITTEDとかなら)」ってことですね。ご指摘、ありがとうございました! -- きの? 2009-04-15 (水) 17:05:37

Top / Hibernate / 悲観的ロックを実装する

現在のアクセス:45721


トップ   編集 凍結 差分 バックアップ 添付 複製 名前変更 リロード   新規 一覧 単語検索 最終更新   ヘルプ   最終更新のRSS
Last-modified: 2023-10-27 (金) 11:03:15 (174d)