はじめに
業務でDB更新処理の際に悲観ロックをかけ忘れた実装をレビュー時に指摘され、この実装をした原因がロックに対しての理解度が足りていないからだなと思い、改めてしっかり理解しようとした。
特に実装忘れなので、その重要性(必ずないといけないこと)をちゃんと理解するのが最低限の目的。
また、悲観ロックと楽観ロックがあるのは知っているが、使い分け方があいまいなので、そこの理解も目的。
参考
目的
排他制御の目的はトランザクション開始と終了時にデータの整合性が保つこと。
RDBMSで言えばACID特性の「一貫性(Consistency)」を担保することが目的。
※尚、筆者はRDBMS(MySQL)しか触ったことがないため、RDBMS前提で話が進みます。
方法
主にはよく聞く2つの方法がある
楽観ロック
基本「複数人によるデータの同時更新は起きないっしょ」という前提でのロックの方法。
実際にデータ自体にロックは行わない。
データの「更新開始時の状態」と「更新終了時の状態」が同じであるかを確認して疑似的にロックする。
具体的にはその「状態」を管理するカラムを用意して更新開始時と更新終了時にその状態が一致していれば更新してOK、一致していなければ楽観ロックエラー、てな感じ。
なので、万が一同時タイミングで複数人による更新処理が実行されたら、一番早く更新処理をした人の内容がDBに反映され、残りの方は楽観ロックエラーになり、更新処理ができないことになる。
悲観ロック
楽観ロックは「複数人によるデータの同時更新はめちゃくちゃ起こる!」という前提のロック方法。
更新対象のレコード取得時にロックすることで、他社からの更新処理を受け付けなくなる。そもそもレコードの読み取りが不可。
なので必然的にロックの対象はレコード単位となる。
ロックの解除はトランザクションのコミットかロールバックによって実行される。
ロックの解除の実装が漏れると常にロック状態になってしまうため、いつまでも他者がレコードを参照できない、という困りごとが起こる。
※但し、ロック処理中に「ブラウザを落とす」「強制シャットダウンする」などするとユーザー操作によるロックしたままの状態になってしまう。
これは「管理者権限でロック開放できるようにする」など別途対応が必要
Laravelおける排他制御
楽観ロック
自作する
デフォルトでは楽観ロックの機能は備わっていないので、記事の様に自作して対応します。
記事内ではedited_at
というカラムを使ってレコードの状態を管理していますが、version
みたいなintを持つカラムでもいいかもですね。
edited_at
の代わりにupdated_at
でもいい気がするけど。
ライブラリ導入
ライブラリも1つの手かと。MITですし。
ただ、スター数が少ない、更新が2019/1に止まっているのが懸念ですね。
コチラはReadme見る限り、
use OptimisticLocking;
というtraitをモデルに持たせることで、lock_version
というプロパティをモデルに持たせてこれで楽観ロックを管理する形だと思います。
(使ったことはないので保証は出来ません。)
悲観ロック
lockForUpdateを使う
悲観ロックは非常にシンプルでLaravelが用意しています。
更新する対象モデル(レコード)取得に使う感じ。
DB::table('users')
->where('votes', '>', 100)
->lockForUpdate()
->get();
Use::lockForUpdate->findOrFail(1);
おわりに
文字に起こして記事をしっかり読むと理解が深まりました。
(もうLaravelのlockForUpdateをトランザクション外で実装するような愚行はしないと思います。。。)
共有ロックをするsheredLockのメソッドがあることも今回知れました。
余談ですが、Laracastに「悲観ロック VS 楽観ロック」という記事もあったので共有します。
ざっくり結論としてはロック時間が長い高負荷な処理ついては楽観ロックだよね、って感じです。
最後までお読みいただきありがとうございました。