DBの排他制御(ロック)の仕組みが理解できていなかったので、整理する。
当初の理解
前提
複数の投稿者によるブログサイト。
各ブログ詳細ページにはいいねボタンが設置されており、
ブログ詳細ページごとのいいね数と
投稿者ごとのいいね数が表示される仕組みとなっている。
テーブル構成は、以下のような構成。
- users(投稿者)テーブル
カラム:投稿者名、投稿者に対するいいね数 - blogs(ブログ内容)テーブル
カラム:ブログ内容、ブログに対するいいね数
やりたいこと
ブログ詳細にいいねされると、以下の2つの更新処理を実行したい
- blogsテーブルの「ブログに対するいいね数」をカウントアップ
- usersテーブルの「投稿者に対するいいね数」をカウントアップ
ただし、投稿者によるブログ更新や投稿者名変更が同一タイミングで実行される可能性があるため、排他制御を行いたい。
実装内容
blogsテーブルの対象レコードを更新されないように、行ロックをかけてから、カウントアップ処理をする。
その後、usersテーブルも行ロックして、カウントアップ処理をする。
行ロックをするには、トランザクションを発行しないといけないから、
2回トランザクションを発行する。
間違っていたポイント
トランザクションを2回発行したら、blogsテーブルの更新後にDB障害が発生した場合にデータの不整合が発生してしまう。
同一テーブルの同一レコードに対しての行ロックはされているが、処理全体のロックがかかっていない。
正しい理解
排他制御とは
同時アクセスが発生しても、直列に処理されるように制御し、不整合が発生させない仕組み。
排他制御の方式
楽観的排他制御(楽観ロック)と悲観的排他制御(悲観ロック)が存在する。
楽観ロックは、同時更新が発生した場合にエラーを返す制御。
同時更新の発生頻度が少ないケースで利用される。
悲観ロックは、更新対象のデータを取得する際にロックをかけて、他の更新処理ができないようにする制御。
同時更新の発生頻度が多いケースで利用される。
→今回のケースでは、頻繁に同時更新が発生する可能性があるため、悲観ロックを実施する
考察
更新対象が1テーブル、1レコードであれば、問題はなかった。
しかし、やりたいことは、2テーブル、2レコードを不整合なく更新したいという要望だったので、以下のどちらかの方法で直列で処理されるようにする。
- データをロックする順番を常に一緒にする
- ロック対象を1つにする
1つ目の案は、順番を常に一緒にすることで、他のトランザクションが発行されても、前のロック処理を追い越すことはできないので、直列の処理は担保される。
しかし、順番を間違うとデッドロックの危険性が出てくるので、却下。
2つ目の案は、ロック対象を親テーブルに固定する案。
トランザクションが終了するまでは、別のトランザクションはロックが取れないため、待機状態になってくれる。
今回は、2つ目の案の方がシンプルでわかりやすいと考え、採用。
投稿者がブログを書くという立ち位置から、users(投稿者)テーブルを親と考えた。
正しい実装方法
usersとblogsテーブルに対して、トランザクションを発行する
usersテーブルの対象レコードを行ロックする
blogsテーブルの対象レコードのカウントアップ処理を行う。
usersテーブルの対象レコードのカウントアップ処理を行う。
トランザクションを終了する。
投稿者によるブログ更新と投稿者名変更処理も、ロック対象をusersテーブルに変更する。
最後に
わかってしまえばシンプル。