ロック処理の詳細

最終更新日: 2022年10月7日
R8 | R9

悲観ロックと楽観ロック

Wagbyではモデル単位でロック方式を指定することができます。標準は悲観ロックです。

補足

コラム「【Wagbyのこだわり】データのロック」にて、その必要性やロック方式の違いなどを詳しく解説しています。ぜひご一読ください。

悲観ロックと楽観ロックのそれぞれの特徴は次の通りです。

悲観ロックの特徴
楽観ロックの特徴

ロックの実装方法

楽観ロックの実装

Wagbyが利用する Hibernate というミドルウェアが提供する機能を用いています。ただしタイムスタンプ方式ではなく、バージョン方式のみを採用しています。(タイムスタンプ方式は厳密性に欠けると判断しました。)

悲観ロックの実装

Wagby独自のロックマネージャを用います。ロック取得時のキーはメモリに保持されています。特別な内部テーブル jfclockobject でロックキーを管理させることもできます。この場合、外部システムから「どのレコードがロックされているか」を知ることができます。[詳細は次節で説明します。]

データベースの SELECT FOR UPDATE 構文について

いくつかのデータベースには "SELECT ... FOR UPDATE" 構文が提供されています。しかし Wagby のロックは、この機能を使っていません。その理由を説明します。

"SELECT ... FOR UPDATE" はトランザクション内でのみ有効なロックです。 しかし本節で説明しているロック処理は、ユーザの操作を伴うもので、次のような動作が求められるものです。

  1. 更新画面を開く。(ここでデータを SELECT する。)
  2. ユーザがデータを編集する。
  3. 保存ボタン押下でデータを更新する。(データを UPDATE する。)

ここで 1. と 3. ではトランザクションが異なります。このように複数のトランザクションをまたがるような処理は一般に「ロングトランザクション」として扱います。(*1)

ロングトランザクションでは、"SELECT ... FOR UPDATE" を使うことができません。この構文は画面処理をまたぐのではなく、ある完結した業務処理ロジックの中で使うことを想定したものです。(*2)

Wagbyではこのようなロングトランザクションでも一貫性を保つため、楽観ロックならびに(独自のロックマネージャを使った)悲観ロックの両方に対応しています。

*1 ロングトランザクションは、データベースの"トランザクション"とは異なるものです。
*2「保存ボタンを押下した直後に "SELECT ... FOR UPDATE" を実行して...」という考え方もありますが、これはロストアップデートを防ぐことができず、厳密な悲観ロックとはいえません。

(悲観)ロックテーブル

Wagbyの「悲観ロック」は内部でロックマネージャをもっています。このロックマネージャはメモリ内にロックを保持します。この情報は外部から知ることはできません。

このロック情報を(リレーショナルデータベースの)テーブルに書き込むオプションを用意しています。これによって、外部プログラムから「どのデータがロックされているか」を知ることができるため、ロックされているデータは更新しないといった制御を行うことができます。

さらに、外部プログラム側からロックをかけることで、Wagby側からも更新ができないように制御することもできます。

定義方法

「環境 > カスタマイズ > 詳細 > ロック情報をデータベースのテーブルに格納する」を有効にします。

ロック情報をデータベースのテーブルに格納する

テーブルの作成

ビルド後に init_db.bat を行うことで外部データベースに jfclockobject テーブルが作成されます。

または手動で jfclockobject テーブルだけを(外部データベースに)作成することもできます。コマンドを実行するカレントディレクトリを wagbyapp\bin とします。

R9.0.3以降

java -jar InitLoader.jar create -t jfclockobject -c ..\webapps\wagby\WEB-INF\export\conf\initdb.xml -systempropfile ..\conf\catalina.properties
  

R9.0.2まで

java -jar InitLoader.jar create -t jfclockobject -c ..\webapps\wagby\WEB-INF\export\conf\initdb.xml
  

ロック情報を検索する

システム管理者でログオンし、管理処理タブにある「ロック情報検索」画面を開くと、図5のように現在、ロックされているデータが表示されます。

ロック情報を検索する

jfclockobjectテーブル

設定を有効にすると、利用するリレーショナルデータベース内に jfclockobject テーブルが用意されます。

項目名 主キー 説明
modelname ロック対象のモデル名(英語)
pkey ロック対象データの主キーの値。複合キーの場合は"$SEP$"を区切り文字として値を連結する。
lockForAll 数値型。"1" が個別データのロック。"2" がモデル全体のロックを意味する。
userid ロックを取得した(juserアカウントの)userid値。
username ロックを取得した(juserアカウントの)username値。
machinename ロックを取得したホストマシンの値。クラスタリング、オートスケール運用で利用する。外部プログラムからロックを取得する場合、Wagbyアプリケーションが使用していない値を設定する。
sessionid ロックを取得したユーザのセッションID値。

あるデータの更新画面を開くタイミングで、jfclockobject テーブルを検索し、該当データに関するロックが存在するかどうかを確認します。すでにロックが存在していた場合、更新画面を開くことができません。存在しなかった場合、同テーブルにロック情報を書き込みます。更新処理が終了したタイミングで、ロック情報を削除します。

外部からロックを取得する

外部プログラムから jfclockobject テーブルにロック情報(レコード)を追加することで、Wagbyの画面から編集できないようにすることができます。(*3)

jfclockobject テーブルに用意されたすべての列を埋める必要があります。いずれか一つでも、未設定 (null) であった場合、ロックは取得されません。

項目 modelname, pkey, lockForAll は対象データを指し示すために必要です。
項目 userid, username, sessionid は「誰がロックを取得したか」という情報を記録するために必要です。

なお項目 sessionid は Web アプリケーションサーバが発行するセッションIDが格納されます。しかし外部プログラムからこの値を設定する場合、セッションID を生成することは困難です。例えば Web アプリケーションサーバに Tomcat を利用した場合、セッションIDは32文字のランダム文字列です。原則として、ユニークな文字列であれば動作します。

外部プログラムから値を設定する場合は、何らかのルールを決めると良いでしょう。例えばこの値を次のようにします。

$HOSTNAME$APPLICATIONNAME$SEQUENCEVALUE

このようなルールに基づいて擬似的なセッションID文字列をセットすることで、ユニーク性を保ちつつ、誰がロックしたかも視認しやすくなります。

(*3) 事前に同テーブルにロック対象データがないか確認してからレコードを追加するようにしてください。

アプリケーション起動時にロック情報を削除する

R9.0.3より、アプリケーション起動時にjfclockobjectテーブルのロック情報を削除するようになりました。削除した件数をINFOレベルでログ出力します。(削除件数が0の場合はログ出力されません。)

この仕様に伴い、項目 machinename にはWagbyアプリケーションが使用していない値を設定するようにしてください。

これは Wagby アプリケーション起動時にそのアプリケーションの machiename に対応する jfclocktable 内のレコードを削除する仕様となっているためです。

※ クラスターやオートスケール構成ではない運用で「ロック情報をデータベースのテーブルに格納する」を有効にした場合、Wagbyアプリケーションから取得したロックはmachinenameカラムにlocalhostが設定されます。よって外部プログラムからロックを取得する場合はlocalhost以外を設定するようにしてください。これによってWagbyアプリケーションが再起動されるような場合でも、外部システムから取得したロックは維持されます。

ロックキーのカスタマイズ

Wagbyのロックマネージャの仕組みを理解し、ロックキーをカスタマイズすることができます。(悲観ロック利用時。楽観ロックの場合は、本内容は適用されません。)

Wagby の悲観ロックでは、Wagby内部のロックマネージャを用います。ユーザーがデータの更新画面を開いたタイミングで対象データをロックします。

このとき、対象データのモデル名を「ロック名」とし、主キー項目を「ロック項目」として扱います。ロック名とロック項目の値から「ロックキー」を生成し、これをロックマネージャで管理しています。

ロックキーのカスタマイズとは、このロック名やロック項目を任意に設定できるようにすることです。 さらに、ひとつの更新画面で複数のロックキーを扱えるようにします。

この機能を利用すると、トランザクション処理のためのカスタマイズコードを記述するときに対象データのロック管理を柔軟に制御することができるようになります。

定義方法

「モデル項目詳細定義>詳細>データベースの詳細>ロック名」を指定します。

図3 モデル項目詳細定義

標準は未指定(空白)となっています。対象となるモデルのすべての項目で未指定の場合は、モデル名をロック名とし、主キー項目をロックキーとします。これが通常の動作となります。

値を記載すると、この項目がロックキーとして用いられます。また記載した値がロック名となります。

サンプル

次のモデルを例に示します。

モデル a

項目主キー
ida1
ida2

モデル b

項目主キー
idb1
idb2

また、モデルaおよびモデルbのデータが次のようになっているとします。

モデルaのデータ:2つのデータ

項目 ida1項目 ida2
110001001
210011002

モデルbのデータ:2つのデータ

項目 idb1項目 idb2
110001001
210011002

例1:モデル a のデータを更新する際に、モデル b のデータもロックする。

1000番のモデル a のデータを編集すると、1000番のモデル b のデータもロックする。

モデル a 項目 ida1 のモデル項目定義シートの「モデルの関連性 - ロック名」に「b」と指定します。これにより、モデル a のデータを編集する場合でも、ロック名はモデル b と同じになるため、モデル b のデータもロックされることとなります。

例2:モデル a のデータを更新する際に、ida2の値でモデル b もロックする。

1000番のモデル a のデータを編集する場合、ida2は1001なので、1001番のモデル b のデータもロックする。

モデル a 項目 ida2 のモデル項目定義シートの「モデルの関連性 - ロック名」に「b」と指定します。 この設定により、モデル a のデータを更新する場合、ロック名 b ロックキー ida2 の値となるロックを行います。

例3:下記のような複合キーのあるモデル c がある場合に、モデル a を更新する際に、ロックキーとして ida1 と ida2 を使い、モデル c をロックする。

モデル c

項目主キー
idc1
idc2

モデル a 項目 ida1 のモデル項目定義シートの「モデルの関連性 - ロック名」に「c」と指定します。

モデル a 項目 ida2 のモデル項目定義シートの「モデルの関連性 - ロック名」に「c」と指定します。

一つのモデルにて同じロック名が指定されている場合は、複合キーとして扱います。 ida1:1000,ida2:1001のモデル a のデータを更新する場合、idc1:1000,idc2:1001のモデル c のデータをロックします。

例4:モデル a を更新する際に、モデル b とモデル c のデータをロックする。

モデル a 項目 ida1 のモデル項目定義シートの「モデルの関連性 - ロック名」に「b c」と指定します。

モデル a 項目 ida2 のモデル項目定義シートの「モデルの関連性 - ロック名」に「c」と指定します。

スペース区切りで複数のロック名を指定することができます。

ida1:1000,ida2:1001のモデル a のデータを更新する場合、idb1:1000のモデル bのデータと、idc1:1000,idc2:1001のモデル c のデータをロックします。

繰り返しコンテナ項目の値を使う場合

繰り返しコンテナ内の項目を使ったロック名の指定を行うことができます。繰り返しコンテナの数に合わせて、ロックを行います。

注意

[注意] 更新画面では繰り返しコンテナの追加/削除/挿入は行わないようにしてください。(ボタン表示制御で、SCREENTYPE 関数を利用して更新画面の場合にこれらのボタンを非表示にする、といった対応を行なってください。

一つの繰り返し項目、繰り返しコンテナ内の項目に指定した場合

この場合は、n 個のロックオブジェクトが作成されます。

例: 下記のようにcont1/c1contentにロック名が指定されたモデルがあるとします。

項目主キーロック名
id
cont1
cont1/c1id
cont1/c1contenttest1

この場合、ロック名をtest1、ロックデータキーにcont1/c1contentを用いてロックを作成します。(*4)

具体的に説明します。このモデルに対応する下記のデータがあるとします。

<test1>
    <id>1000</id>
    <cont1><c1id>1</c1id><c1content>a</c1content></cont1>
    <cont1><c1id>2</c1id><c1content>b</c1content></cont1>
  </test1>
  

この場合、次の2つのロックオブジェクトが内部で用意されます。

ロック名:test1 ロックキー:a
ロック名:test1 ロックキー:b

(*4) cont1のすべてのデータに対応するロックオブジェクトを用意します。

一つの項目と、一つの繰り返しコンテナ内の項目に指定した場合

この場合は、n 個の複合キーとなるロックオブジェクトが作成されます。

例: 下記のようにidとcont1/c1contentにロック名が指定されたモデルがあるとします。

項目主キーロック名
idtest1
cont1
cont1/c1id
cont1/c1contenttest1

この場合、ロック名をtest1、ロックデータキーにidとcont2/c2contentを用いてロックを作成します。(*5)

具体的に説明します。このモデルに対応する下記のデータがあるとします。

<test1>
    <id>1000</id>
    <cont1><c1id>1</c1id><c1content>a</c1content></cont1>
    <cont1><c1id>2</c1id><c1content>b</c1content></cont1>
  </test1>
  

この場合、次の2つのロックオブジェクトが内部で用意されます。

ロック名:test1 ロックキー:1000$a
ロック名:test1 ロックキー:1000$b

(5) 通常項目と繰り返し項目等に同一のロック名を指定した場合、id項目の値と、cont2のすべての項目値を組み合わせてロックオブジェクトを用意します。

複数の繰り返し項目、繰り返しコンテナ内の項目を指定した場合

この場合は、n*m 個の複合キーとなるロックオブジェクトが作成されます。

例: 下記のようにcont1/c1contentとcont2/c2content項目にロック名が指定されたモデルがあるとします。

項目主キーロック名
id
cont1
cont1/c1id
cont1/c1contenttest1
cont2
cont2/c2id
cont2/c2contenttest1

この場合、ロック名をtest1、ロックデータキーにcont1/c1contentとcont2/c2contentを用いてロックを作成します。(*6)

具体的に説明します。このモデルに対応する下記のデータがあるとします。

<test1>
    <id>1000</id>
    <cont1><c1id>1</c1id><c1content>a</c1content></cont1>
    <cont1><c1id>2</c1id><c1content>b</c1content></cont1>
    <cont2><c2id>1</c2id><c2content>c</c2content></cont2>
    <cont2><c2id>2</c2id><c2content>d</c2content></cont2>
  </test1>
  

この場合、次の4つのロックオブジェクトが内部で用意されます。

ロック名:test1 ロックキー:a$c
ロック名:test1 ロックキー:a$d
ロック名:test1 ロックキー:b$c
ロック名:test1 ロックキー:b$d

(*6) 複数の繰り返し項目などに同一のロック名を指定した場合、cont1のすべてのデータと、cont2のすべてのデータを掛け合わせて総当たりでロックオブジェクトを用意します。

仕様・制約

  • ここで説明したロックキーのカスタマイズについてのお問い合わせは Premium Support の対象となります。
  • モデル参照(チェックボックス)にロック名を指定することはできません。
  • 複数のモデルでロック名を共用する場合に、ロックキーとなる項目の型が一致する必要があります。上記、例1の設定では、モデル a 項目 ida1 とモデル b 項目 idb1 の項目の型が一致しています。
  • 複合キーとして扱われた場合は、設定されている順番と項目の型が一致する必要があります。例3の設定では、項目 ida1 と idc1、項目 ida2 と idc2 の項目の型がそれぞれ一致しています。
  • ロックキーに指定した項目は更新画面、一覧更新画面で読み込み専用項目となります。
  • ロックは更新時のみ行われます。新規登録やコピー登録の際には行われません。(*7)
(*7) Wagbyは新規登録時には排他制御は行いません。カスタマイズを行う場合でも登録と更新が競合しない設計方針となるよう、留意ください。

セッションタイムアウトによるロック解除

悲観ロックでは、あるユーザによって取得されたロックは、このユーザがログアウトするまで有効です。ここでユーザがブラウザの操作を何もしなかった場合、セッションタイムアウトによって強制ログオフされます。このタイミングでロックも解除されます。

このセッションタイムアウトによるロック解除には、若干のタイムラグがあります。これは Tomcat の仕様です。

Tomcatのセッションタイムアウトはバックグラウンドプロセスで実行されますが、このプロセスの実行間隔がbackgroundProcessorDelayで指定されます。また、バックグラウンドプロセスが実行される際に何回に1回セッション無効化(session expires)の処理を行うかをprocessExpiresFrequencyで指定されます。

それぞれのデフォルト値はbackgroundProcessorDelayが10(秒)、processExpiresFrequencyが6となっているため、6*10=60秒毎にsession expiresの処理が実行されます。よってタイミングによっては最大で60秒遅れることとなります。

タイムラグについてのカスタマイズ

wagbyapp/webapps/wagby/META-INF/context.xmlを編集します。次の例は毎秒session expiresの処理が実行し、最大で1秒しか遅れないようにしたものです。

<Context path="/wagby"
        privileged="true" useHttpOnly="true" backgroundProcessorDelay="1">
  <Manager className="org.apache.catalina.session.StandardManager" pathname="" processExpiresFrequency="1"/>
  

しかしこの分、実行時に負荷がかかります。通常、この程度のロック解除の遅れは運用上の問題にならないと判断し、Wagby ではこの値は Tomcat 標準設定値を用いています。何らかの理由で修正したい場合は、上記ファイルを直接、編集してください。

関連するページ