ロックって何だっけ、ということで整理しようと思ったので書いていきます。「ジャカジャーーーン!」のあれではなく、データベースにおけるロックですね。雑魚エンジニアによる記事なので、修正等見つけたら是非コメントください。
ロックには2種類の概念がある
データベースには「共有・占有ロック」と「楽観的・悲観的ロック」という2種類の異なる次元の概念があります。前者はデータベースの行や表に対するアクセス制御をする「機能」的なもので、後者はどうやってロックをするかという「方針」的なものになります。
共有・占有ロックについて
READロックとかWRITEロックとか言われることもあります。考え方としては、これからREADするからデータは弄るなよーって時に掛けるのが共有ロック(READロック)、これからWRITEするから見るのも許さんぞーってのが占有ロック(WRITEロック)です。
共有ロックを掛けると、「自他共にREADは出来るが、WRITEが出来ない」という状態になります。占有ロックを掛けると、「自分はREAD・WRITEできるけど、他からはどちらも出来ない」という状態になります。
恐怖のデッドロックを起こしてしまう直接的原因となるのがこちらの概念ですね。共有ロック同士でデッドロックが起きることはありませんが、共有・占有 or 占有・占有パターンで発生する可能性があります。SELECT文と他のDML分とでロックが起きるパターンというのは、SELECT文でも共有・占有ロックを掛けることがあるからです。
楽観的・悲観的ロックについて
僕の場合はこちらをきちんと理解していませんでした。「排他制御(楽観ロック・悲観ロック)の基礎」という記事がすごくわかりやすかったです。簡単に言うと、「①データの確認(SELECT) → ②データの更新(UPDATE)」というフローがあるときに、①の時点でロックを掛ける場合を悲観的ロック、掛けない場合は楽観的ロックということになります(UPDATEの直前にはもちろんロックが掛かります。)。
楽観的ロックをする場合にはレコード単位のバージョン管理を行う必要があります。つまりテーブルにSEQUENCEとかVERSIONとかいう数値の列項目を持たせるということです。レコードを更新するたびにバージョン管理の項目値を+1していくことで、SELECTとUPDATEの間に、他の人がレコードを更新したか否かを判定することが出来るという仕組みです。
そして、悲観的ロックをする場合にはSELECT ... FOR UPDATE のようなクエリを投げます。このクエリを投げた時点で、対象の範囲に共有ロックが掛かり、他から更新することが出来なくなります。逆に言えば、トランザクションを解除するまではずっとロックがかかるので、タイムアウトを設定しておかないと、ずっと処理が止まったまま、なんてことも発生する可能性が出てきます。
楽観的ロックと悲観的ロックの使い分け
-
悲観的ロックの場合
複数人が同じレコードを同時に更新する可能性が高く、一回の更新件数が少なく、処理が速い場合に使用されるようです。銀行のオンラインシステムなどによく使われるとのこと。ただし、何らかの理由で広い範囲を長時間ロックしてしまうと、他の処理も固まってしまうので、注意が必要ですね。 -
楽観的ロックの場合
SELECT文を発行してから実際の更新までに時間が掛かり、複数人が同時に更新する可能性が高くない場合に使うようです。マスタ更新でよく使われるようですね。SELECT FOR UPDATEを発行すると共有ロックが掛かります。もし同じプログラムからSELECT FOR UPDATEを発行すると、ロック待ちが発生してしまいます。これがフローとして、「①データ確認(SELECT FOR UPDATE)→ 画面操作 → 更新(UPDATE)」だとすると、タイムアウトの時間が設定されていなければ、画面操作が終わるまで待たされることになります。だから更新の直前までロックを掛けない楽観的ロックという方法をとるわけですね。私が昔やっていた販売管理のシステムもほとんどが楽観的ロックで実装されていたように思います。
とはいえ、一概に言えず、その時の用途に応じて使い分けが必要だと思われます。