排他制御

排他制御とは、共有資源(データやファイル)に対して複数のアクセスが見込まれる場合に、同時アクセスにより不整合が発生することを防ぐため、
あるトランザクションが共有資源(データやファイル)にアクセスしている時は、
他トランザクションからはアクセスできないようにして直列に処理されるように制御することである。


排他制御の方式

排他制御の実現方式はいくつか存在するが、ここでは代表的な楽観ロック(楽観的排他制御)と悲観ロック(悲観的排他制御)を記載する。

楽観ロック(楽観的排他制御)

楽観ロックとは、滅多なことでは他者との同時更新は起きないであろう、という楽観的な前提の排他制御のことである。
データそのものに対してロックは行わずに、更新対象のデータがデータ取得時と同じ状態であることを確認してから更新することで、
データの整合性を保証する方式である。

楽観ロックを使用する場合は、更新対象のデータがデータ取得時と同じ状態であることを判断するために、
バージョンを管理するためのカラム(Versionカラム)を用意する。

更新時の条件として、データ取得時のバージョンとデータ更新時のバージョンを同じとすることで、データの整合性を保証することができる。


更新対象のデータがデータ取得時と同じ状態であることを判断するためのカラムを、ロックキーと呼ぶ。
ロックキーは、バージョンカラム以外にも更新日時等のタイムスタンプを用いることもできる。
ただし、タイムスタンプを秒単位までしか保有していない場合、同一秒に複数の操作を行われた時に楽観ロックの判定ができなくなる。

また、より精度の高いミリ秒まで保有していたとしても同様のことが言える。
つまり、タイムスタンプではこの懸念は払拭できないため、ロックキーはバージョンカラムを利用した方が無難である。

また、検知のタイミングが業務終了時での検知となるため、画面入力に時間がかかる業務の場合は最初からやり直しになってしまい、
結果として時間のロスとなってしまう。


悲観ロック(悲観的排他制御)

他者が同じデータに頻繁に変更を加えるであろう、という悲観的な前提の排他制御のことである。
更新対象のデータを取得する際にロックをかけることで、他のトランザクションから更新されないようにする方式である。

悲観ロックを使用する場合は、トランザクション開始直後に更新対象となるレコードのロックを取得する。
ロックされたレコードは、トランザクションがコミットまたはロールバックされるまで、他のトランザクションから更新されないため、
データの整合性を保証することができる。


データのロックは、SELECT ... FOR UPDATE文を利用して実現されるのが一般的である。
悲観ロックでは、ロックの解放漏れがあると、いつまで経っても他者が操作できないということに繋がるため、
データ更新後はロックの解放を必ず行う。
また、確実にロックを解放するのは難しいという特性も持っている。
例えば処理中に、以下のようなこと等が発生すると、場合によってはロックしたままの状態となってしまう。

  • Webアプリケーションにおいて、ブラウザの[×]ボタンが押下された。
  • Webブラウザが強制シャットダウンした。
  • 操作しているPCを強制シャットダウンした。


これら全てをアプリケーションの設計で制御するのは困難なため、
悲観ロックを採用する場合は、以下のような仕組みを設ける等、何らかの対応策を用意しておく必要がある。

  • 管理者であればロックを解放できる機能を設ける。
  • ロックしているユーザであれば、ロックを再取得できるようにする。
  • ロックしているユーザのセッションタイムアウトのタイミングで、そのユーザがロックしている全てのデータのロックを解放する。
  • 長時間ロックされているデータはロック解放するような機能を設ける。



排他制御方式の比較

楽観ロック 悲観ロック
思想 業務開始から終了まで
他者による更新は滅多に起きないという前提
業務開始から終了まで
他者による更新は頻繁に起きるという前提
競合検知のタイミング 業務終了時に検知 業務開始時に検知
開発コスト 比較的低い 高い
どちらを使用するか ・同一業務を複数人では実施しない場合
・競合があまり発生せず、発生してもやり直せば済む場合
・時間があまり掛からない場合
・同一業務を複数人で実施する場合
・競合が発生したらやり直せない場合
・時間が掛かる場合



排他制御の長さ

排他制御している時間が長ければ長いほど、システムの利便性が下がってしまう。
そのため、不都合や不整合が発生しない範囲で可能な限り短くすることが鉄則である。


バッチ処理とオンライン処理の排他制御

オンライン処理だけでなくバッチ処理もあるシステムの場合、
オンライン処理でのデータ更新とバッチ処理でのデータ更新が同時に行われるケースも考える必要がある。
また、オンライン処理とバッチ処理の排他制御は難易度が高いため、以下の順で方針を検討する。

  • 方針1 オンライン処理とバッチ処理で競合が発生しないよう処理を分ける。
  • 方針2 (方針1が駄目なら)バッチ処理でも排他制御を行う。


方針1 オンライン処理とバッチ処理で競合が発生しないよう処理を分ける
  1. 時間帯で処理を分ける
    オンライン処理とバッチ処理の時間帯を決めておくことで、競合を発生させないようにする方式である。
    例えば、深夜帯はバッチ処理を行い、深夜帯以外はオンライン処理を行う。
    ただ、この方式は24時間稼働が要件のシステムでは適用できない。


  1. データの種類で処理を分ける
    テーブルにステータスカラムのようなものを設けて、オンライン処理の対象データとバッチ処理の対象データを分けることで、
    そもそも競合を発生させないようにする方式である。


方針2 バッチ処理でも排他制御を行う

オンライン処理とバッチ処理で競合が発生しないよう処理を分けることができない場合、
バッチ処理でも排他制御を行う必要がある。
この時、排他制御方式は大きく分けて3つ存在する。

  1. オンライン処理のロック解除を待ってから排他制御を行う(オンライン優先)
    オンライン処理を優先させたい場合はこの方式を採用する。
    ただし、バッチの開始が遅れる分、終了も遅くなる。

  2. オンライン処理のロック解除を待たずバッチ処理で強制的にロックを解除する(バッチ優先)
    バッチ優先にしたい場合はこの方式を採用する。
    この場合、バッチ処理は時間通り開始することができるが、オンライン処理では排他エラーが発生してしまう。

  3. バッチ処理でロックできたもののみ処理対象とする
    バッチでロックが取得できたもののみ処理対象とする方式である。
    対象データを全て取得して(ここではロックしない)、取得したデータを1件1件ロックしながらデータを処理する。
    ロックが取得できなかったものは処理スキップして、次回のバッチ処理対象に回す等で対応する。


どの方式も間違いではないため、業務要件を鑑みてしっかりと検討したうえで一番マッチする方式を採用する。