この記事では、SQLで並行処理を行う際に利用する「SELECT FOR UPDATE」という構文についての紹介記事です。
アプリケーションは複数の利用するユーザーが存在するため、同じデータへのアクセスがどうしても発生します。
そんな時にデータの整合性を保つ役割を持っています。
私も実務で初めて見た際は何をするものか理解できなかったのでなるべくわかりやすく紹介しようと思います。
この記事を読むメリット
- SELECT FOR UPDATEの基本の使い方が分かる
- 同時アクセスによるリスクがどんなものか分かる
データへの同時アクセスのリスク
まず最初は、どんなリスクがあるのか整理してみます。
「在庫管理」を行なっているアプリを想定します。
商品テーブル 「items」に、在庫数「stock」を管理しているとします。
idが1の商品を購入する際、次のような処理が走ります。
-- 残り在庫の確認
SELECT stock FROM items WHERE id = 1;
-- 在庫がある場合、減算して更新
UPDATE items SET stock = stock - 1 WHERE id = 1;
この処理では残りの在庫を確認して、在庫があったら更新の処理をするようにしています。
しかし、これが2人同時に行なっていたらどうでしょう。
- idが1の商品のstockは「1」で在庫が残り1の状態
- ユーザーAとユーザーBが同時に「select stock」を実行 → 残り在庫は「1」
- 両者とも「買える」と判断
- 「UPDATE」の更新処理が2回実行され、stockは「-1」となる
このようにstockがマイナスの数値になって不具合が起きてしまいます。
このようなことは複数ユーザを持つアプリケーションでは頻繁に発生する同時アクセスの事象です。
チケット購入や残高更新なども当てはまります。
同時アクセスにはこのようなリスクが潜んでいます。
そしてアプリケーションが大きくなればなるほどこのようなリスクも大きくなります。
SELECT FOR UPDATEによる解決方法
ここで登場するのが「SELECT FOR UPDATE」です。
先ほどのような同時アクセスによる競合を防いでくれます。
この構文を使うことで、対象の行に「排他ロック(書き込みロック)」をかけることができます。
先ほどの在庫管理の例を修正するとこのようになります。
BEGIN;
-- 在庫をロックしながら取得
SELECT stock FROM items WHERE id = 1 FOR UPDATE;
-- 在庫があれば減らす
UPDATE items SET stock = stock - 1 WHERE id = 1;
COMMIT;
「SELECT stock FROM items WHERE id = 1 FOR UPDATE;」の部分で対象の行に対してロックがかかり、他のトランザクションがその行にアクセスしてロックを取得しようとすると、「ロックが解除されるまで待たされる」ようになります。
このことによって同時にデータが更新されることを防ぐことになります。
他のトランザクションは、ロックがかかっているトランザクションの「COMMIT」が完了した時点で実行されるようになります。
SELECT FOR UPDATEの注意点
SELECT FOR UPDATEを使用する際には注意点もあります。
1つは「トランザクション内で使用する」ということです。
BEGIN;
SELECT stock FROM items WHERE id = 1 FOR UPDATE;
-- 更新処理
COMMIT;
このように「BEGIN」から「COMMIT」の中で使用しないと「FOR UPDATE」を使ってもロックがかかりません。
必ずトランザクション内で使用するようにしましょう。
2つ目は「不要なロックはしないようにする」です。
SELECT FOR UPDATEで排他ロックをしている間は他のユーザーの処理は止まってしまいます。
読み取り専用の処理にまでロックをかけると、アプリ全体のパフォーマンスが悪化します。あくまで、同時更新の危険性がある場面に限定して使うことが重要です。
楽観的ロックというものもある
先ほどまで紹介してきた「SELECT FOR UPDATE」ですが、これは「悲観的ロック」と言われます。
より厳密にデータの整合性をとるロック方法です。
一方で、「楽観的ロック」という方法もあります。
以下が楽観的ロックの簡単な例です。
-- 現在のデータ(version=3)を取得
SELECT stock, version FROM items WHERE id = 1;
-- 更新時にversionを条件に含める
UPDATE items
SET stock = stock - 1,
version = version + 1
WHERE id = 1 AND version = 3;
バージョンを使った方法になります。
WHEREでversionをチェックしているので、もし他の処理でversionが更新されていた場合、このUPDATEは失敗します。
失敗した際の再試行処理やエラー処理などを書かないといけないですが、こういうロックの方法もあります。
先ほどの悲観的ロックとの違いは整理するとこんな感じになります。
項目 | 悲観ロック | 楽観ロック |
---|---|---|
データ競合の頻度 | 高い | 低い |
パフォーマンス | 低くなりやすい | 高くなりやすい |
実装の複雑さ | シンプル | 再試行処理が必要 |
安全性 | 高い | 競合時失敗の考慮必要 |
ユースケース | 在庫、残高、予約 | プロフィール、設定 |
違いを理解して使い分ける必要があります。
まとめ
いかがだったでしょうか。
大きなアプリになるに従ってこのようなデータ操作にも慎重になる必要があります。
初めて見るものに関しては積極的に意味を調べて自分のものになるまで理解するようにしたいと思います。
見たことなかったという方の参考になれば幸いです。