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

Hibernateは業務用アプリとかで出てくる、楽観的ロック機構を標準でサポートしていますので、その機能を試してみました。楽観的ロックとは、複数のトランザクションが同じrowを更新しようとしたときに、それを検知する仕組みです。検知とは、先勝ちしたり、後勝ちしたりといろいろあるみたいですが、後にコミットかけた方で例外が発生する、が一般的なのではないでしょうか。

実装方法は対象のテーブルにバージョンカラムを設けて、更新されたらインクリメントされるようにしておきます。で、それぞれのトランザクションが更新するときに、検索時のバージョンの値と、更新時のテーブルのバージョンの値を見比べて、同じだったら誰も更新されていない、違ったら誰かが更新している、ということを検知するわけです。

対象テーブルの情報

さて実験ですが、複数のトランザクションである同じrowを検索してきて、それをそれぞれのトランザクションでupdateしてみようと思います。

MESSAGES.png

対象のテーブルMESSAGESは以下の通り:

mysql> describe test.MESSAGES;
+-----------------+------------------+------+-----+---------+----------------+
| Field           | Type             | Null | Key | Default | Extra          |
+-----------------+------------------+------+-----+---------+----------------+
| MESSAGE_ID      | int(10) unsigned | NO   | PRI | NULL    | auto_increment |
| NEXT_MESSAGE_ID | int(10) unsigned | YES  | MUL | NULL    |                |
| MESSAGE_TEXT    | varchar(255)     | NO   |     | NULL    |                |
| VERSION         | int(10) unsigned | NO   |     | 0       |                |
+-----------------+------------------+------+-----+---------+----------------+

このテーブルを作成するために実行するDDLは以下の通り:

CREATE TABLE `MESSAGES` (
  `MESSAGE_ID` int(10) unsigned NOT NULL auto_increment,
  `NEXT_MESSAGE_ID` int(10) unsigned default NULL,
  `MESSAGE_TEXT` varchar(255) character set ujis NOT NULL default '',
  `VERSION` int(10) unsigned NOT NULL default '0',
  PRIMARY KEY  (`MESSAGE_ID`),
  KEY `MESSAGES_FKIndex1` (`NEXT_MESSAGE_ID`),
  CONSTRAINT `MESSAGES_ibfk_1` FOREIGN KEY (`NEXT_MESSAGE_ID`) \
REFERENCES `MESSAGES` (`MESSAGE_ID`) ON DELETE NO ACTION
ON UPDATE NO ACTION
) ENGINE=InnoDB DEFAULT CHARSET=latin1 ;

Messages.hbm.xml

Hibernateの設定ファイルは以下の通り。VERSIONのカラムの指定が、

 <version name="version" type="int">
   <column name="VERSION" not-null="true">
   </column>
 </version>

となっています。

ちなみにこのファイルはHibernate Toolsによって作成されました。

<?xml version="1.0"?>
<!DOCTYPE hibernate-mapping PUBLIC "-//Hibernate/Hibernate Mapping DTD 3.0//EN"
"http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd">
<!-- Generated 2006/06/19 0:06:47 by Hibernate Tools 3.1.0.beta5 -->
<hibernate-mapping>
  <class name="nu.mine.kino.models.Messages" table="MESSAGES" catalog="test">
    <comment></comment>
    <id name="messageId" type="int">
      <column name="MESSAGE_ID" />
      <generator class="increment" />
    </id>
    <version name="version" type="int">
      <column name="VERSION" not-null="true">
        <comment></comment>
      </column>
    </version>
    <many-to-one name="messages" class="nu.mine.kino.models.Messages" fetch="select">
      <column name="NEXT_MESSAGE_ID">
        <comment></comment>
      </column>
    </many-to-one>
    <property name="messageText" type="string">
      <column name="MESSAGE_TEXT" not-null="true">
        <comment></comment>
      </column>
    </property>
    <set name="messageses" inverse="true">
      <key>
        <column name="NEXT_MESSAGE_ID">
          <comment></comment>
        </column>
      </key>
      <one-to-many class="nu.mine.kino.models.Messages" />
    </set>
  </class>
</hibernate-mapping>

やってみる

以下のように、複数のトランザクションを別スレッドで稼働させてUpdateがどうなるかを見てみました。

new Thread(new Runnable() {
    public void run() {
        Session session = sessionFactory.openSession();
        Transaction tx = session.beginTransaction();
        System.out.println("トラ1開始");
        Messages message = (Messages) session.load(Messages.class,new Integer(1));
        System.out.println(message.getMessageText());
        message.setMessageText("Thread1:" + System.currentTimeMillis());
        waitt(3000);
        tx.commit();
        session.close();
        System.out.println("トラ1終了");

    }
}).start();

new Thread(new Runnable() {
    public void run() {
        waitt(500);
        Session session = sessionFactory.openSession();
        Transaction tx = session.beginTransaction();
        System.out.println("トラ2開始");
        Messages message = (Messages) session.load(Messages.class,new Integer(1));
        System.out.println(message.getMessageText());
        waitt(1000);
        message.setMessageText("Thread2:" + System.currentTimeMillis());
        tx.commit();
        session.close();
        System.out.println("トラ2終了");
    }
}).start();

このように

トラ1開始 select  -> トラ2開始 select  ->
トラ2update & commit -> トラ1update & commit

としたときに、トラ1のコミットがどうなるかを見てみました。以下が実行結果です。

Start.
[main] INFO  org.hibernate.cfg.Environment - Hibernate 3.1
[main] INFO  org.hibernate.cfg.Configuration - Reading mappings from resource: nu/mine/kino/models/Messages.hbm.xml
[main] INFO  org.hibernate.cfg.HbmBinder - Mapping class: nu.mine.kino.models.Messages -> MESSAGES
[main] INFO  org.hibernate.cfg.HbmBinder - Mapping collection: nu.mine.kino.models.Messages.messageses -> MESSAGES
[main] INFO  org.hibernate.cfg.SettingsFactory - RDBMS: MySQL, version: 5.0.22-standard-log
[main] INFO  org.hibernate.dialect.Dialect - Using dialect: org.hibernate.dialect.MySQLInnoDBDialect
End.
トラ1開始
[Thread-0] DEBUG org.hibernate.SQL - select messages0_.MESSAGE_ID as MESSAGE1_0_0_, messages0_.VERSION as VERSION0_0_, 
messages0_.NEXT_MESSAGE_ID as NEXT3_0_0_, messages0_.MESSAGE_TEXT as MESSAGE4_0_0_ from test.MESSAGES messages0_
where messages0_.MESSAGE_ID=?
Thread2:1151250136968
トラ2開始
[Thread-1] DEBUG org.hibernate.SQL - select messages0_.MESSAGE_ID as MESSAGE1_0_0_, messages0_.VERSION as VERSION0_0_,
messages0_.NEXT_MESSAGE_ID as NEXT3_0_0_, messages0_.MESSAGE_TEXT as MESSAGE4_0_0_ from test.MESSAGES messages0_
where messages0_.MESSAGE_ID=?
Thread2:1151250136968
[Thread-1] DEBUG org.hibernate.SQL - update test.MESSAGES set VERSION=?, NEXT_MESSAGE_ID=?, MESSAGE_TEXT=?
where MESSAGE_ID=? and VERSION=?
トラ2終了
[Thread-0] DEBUG org.hibernate.SQL - update test.MESSAGES set VERSION=?, NEXT_MESSAGE_ID=?, MESSAGE_TEXT=?
where MESSAGE_ID=? and VERSION=?
Exception in thread "Thread-0" org.hibernate.StaleObjectStateException: Row was updated or deleted by another
transaction (or unsaved-value mapping was incorrect): [nu.mine.kino.models.Messages#1]
	at LockSample$1.run(LockSample.java:38)
	at java.lang.Thread.run(Unknown Source)

トラ1のスレッドが更新かけてcommitしようとしたときに

Exception in thread "Thread-0" org.hibernate.StaleObjectStateException:
Row was updated or deleted by another transaction

という例外が発生しました。トラ2の処理で自動的にVERSIONカラムがインクリメントされて、トラ1のコミット時にそれを例外で通知してくれるということがわかりました。


この記事は

選択肢 投票
おもしろかった 57  
そうでもない 9  

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

現在のアクセス:38977


添付ファイル: fileMESSAGES.png 1914件 [詳細]

トップ   編集 凍結 差分 バックアップ 添付 複製 名前変更 リロード   新規 一覧 単語検索 最終更新   ヘルプ   最終更新のRSS
Last-modified: 2022-10-06 (木) 11:49:54 (568d)