Home > 保険システム開発室NEWS > Webアプリケーションの排他制御

Webアプリケーションの排他制御

2008-12-28 11:15

Webに限ったことではありませんが、オンラインシステムにて排他制御をしたい場合はよくあります。 例えばWebオンライン上で複数のユーザが同じデータを更新することが出来るシステムは、なんらかの排他制御の仕組みを取り入れた方がよいでしょう。

今回は私たちがWebシステムを開発してきた中でのこのようなケースで有効な排他に関するテクニックをいくつかご紹介したいと思います。

前提

Webシステムで最も排他制御が必要な対象リソースはデータベースです。ここで申し上げる排他制御を行う対象はデータベースに存在するデータとします。

楽観的ロック

楽観的ロックとはデータそのものに対してはロックをせずに、更新するときにアプリケーションの方でデータ取得時と同じ状態であることを確認してから更新する仕組みです。

例えばWebオンライン画面にて下図のようにデータベースからデータを取得した後「編集」→「確認」→「更新」のように複数画面の遷移を伴う一連の処理では、この楽観的ロックを行います。

具体的には、左図の「編集」でデータベースからデータを取得する際に、そのデータの最終更新日も取得しておきます。

そして「更新」する際にデータベースに存在するデータの最終更新日を比較し、異なっていたら編集中に他のユーザが先に更新したと見なしてその更新は無効である旨をユーザに伝えます。

最終更新日を比較する方法で手っ取り早いのは、更新する条件に最終更新日を含めてしまうことです。SQLで表現すると以下のようになります。

UPDATE table SET field1 = 'date1', field2 = 'date2', last_update = SYSDATE
  WHERE id = 1000 and last_update = '2008/12/25 23:41:11'

このように更新する条件に最終更新日を含めてしまえば更新した際に更新件数が0件であった場合は、他のユーザが先に更新(もしくは削除)したと判断できます。

また上記のように手動で実装しなくても、自動的に楽観的ロックを行ってくれるデータベース関連のAPIやミドルウェアも存在するので、そちらを利用する方法もあります。

悲観的ロック

悲観的ロックとはデータを取得する際にロックをかけ、他のプロセスから更新を許さないという仕組みです。

例えば一度のトランザクションで、関連する複数のテーブルのデータを抽出し、その抽出結果を基にして更新する場合があります。その場合、複数のテーブルからデータを抽出し、各テーブルに対して更新が終わるまでは、それらのデータのまとまりは他のプロセスから抽出と更新がされていないと保障される必要があります。

具体的な例を挙げます。左図のような伝票テーブルとそれに紐付く明細テーブルを更新するアプリケーションがあったとします。

伝票テーブルの情報はWebオンライン上の複数の画面から同時に更新される可能性があり、併せてそれに紐付く明細テーブルのデータも自由に追加・更新・削除できる仕様とします。

また、伝票テーブルには明細の合計金額を保持する項目があるとします。つまり、明細の情報が変更されたり増減した場合は、伝票テーブルのレコードの合計金額もそれに合わせて更新する必要があるとします(このテーブル設計の良し悪しはまた別の話ということで。。)。
つまり、伝票テーブルと明細テーブルは常に整合性を取って更新する必要があるわけです。

更に厄介なことに、これらのデータはWebオンライン画面からだけではなく、日中に定期的に実行されるバッチ処理でも抽出・更新される可能性があるとします。

このような場合に、悲観的ロックを行います。

特にバッチ処理では先に申し上げた楽観的ロックを行うのは適切ではありません。オンライン画面での楽観的ロックであれば、競合を検出したエラー時にユーザに通知し再入力を促すことは出来ますが、バッチ処理ではエラーとなった場合は再実行をするなりの面倒な考慮が必要となるからです。

具体的な悲観的ロックの方法は、右図の通りアプリケーションが伝票または明細テーブルを処理する際に、Webもバッチ処理でも最初に伝票テーブルの対象レコードのロックを取得してから処理を行うというルールを決めます。

重要なのは更新する直前にロックをするのではなく、そのトランザクション内で伝票データを抽出する前に、伝票テーブルの対象レコードのロックを取得することです。

悲観的ロックを実現するには、データベースのロック機構をそのまま利用します。例えば、Oracleの場合はFOR UPDATE句でありSQL ServerやDB2の場合は、更新ロック(Uロック)等が適当です。
以下はOracleのFOR UPDATE句でロックを取得する例です。

SELECT * FROM table WHERE id = 1000 FOR UPDATE

先の楽観的ロックの例に出た左図のような「編集」→「確認」→「更新」のように遷移するWebオンライン画面のシステムで、バッチ処理も存在する場合は、オンラインでの「更新」の際に悲観的ロックも併用するようにします。
またバッチ処理では常に悲観的ロックを使用して更新します。

このようにWebでもバッチでも必ず伝票テーブルの対象レコードのロックを取得してから処理をする、というルールを徹底すれば正常に排他制御が働き不整合データができることはありません。

ただし、悲観的ロックではデッドロックに注意する必要があります。 先ほどからの例で言うと、一度のトランザクションで複数の伝票データを更新する可能性がある場合は注意が必要です。

具体的な例として左図の通り、①でWebオンラインが伝票テーブルのレコード1に対してロックを取得した後、②でバッチ処理が伝票テーブルのレコード3に対してロックを取得したとします。 ③でWebオンラインが同一トランザクションで伝票テーブルのレコード3に対してロックを取得しようとするのと、④でバッチ処理が伝票テーブルのレコード1に対してロックを取得しようとするのが重なり、デッドロックに陥る可能性があるということです。

デッドロックを防止する有効な手段としては、ロックを取得する際に順番付けのルールを決めるというのがあります。上記の例でいうと、伝票のレコード番号が小さいものからロックを取得するというルールを決めれば、バッチ処理は伝票テーブルのレコード3からではなくレコード1から取得しなければならず、デッドロックが発生しません。

上記のルールが徹底されればそれに越したことはありませんが、私たちがよくやる方法はロックを取得する際にタイムアウトを設けてしまうことです。こうすれば、上記のような状態に陥ってもタイムアウトが発生してデッドロックにはなりません。
以下はOracleのFOR UPDATE句でロックを取得する際に8秒のタイムアウトを設けた例です。

SELECT * FROM table WHERE id = 1000 FOR UPDATE WAIT 8

ただし、バッチ処理から行うロックにはタイムアウトは設けません。先に申し上げた通りバッチ処理でエラーとなった場合は再実行をするなりの面倒な考慮が必要であり、オンラインのように安易にエラーを発生させる事は出来ません。
よって私たちは、バッチ処理からのロックはタイムアウトを設けずに、オンラインからのロックだけにタイムアウトを設けます。つまりバッチ処理を最優先させるということです。
ただし、バッチ処理は複数同時に実行されることは無いというのが前提です。

まとめ

以上となりますが、私たちが排他制御を行う際のポイントをまとめると下記の通りとなります。

  • 複数のユーザが同じデータを同時に更新可能なWebオンラインでは楽観的ロックを行う
  • Webオンラインと同時にバッチ処理で更新される可能性がある場合は悲観的ロックも併用する
  • 悲観的ロックでは対象データの主となるテーブルのレコードのロックを取得する
  • 同一トランザクションで複数のデータに対して悲観的ロックを取得する場合はデッドロックに注意する

Posted by T.S

このページの上部へ