https://zenn.dev/mpyw/articles/rdb-advisory-locks

以前, Qiita で以下の記事を投稿した。今回の議題に直接的な関係はないが,関連している部分があるため引用する。
MySQL/Postgres とも,
- MVCC アーキテクチャの恩恵で,
SELECTとUPDATEは基本的には競合しない。- 単一レコードのシンプルな
UPDATEでも排他ロックされ,排他ロック中のレコードへのUPDATEでの変更操作は トランザクション分離レベルによらず ブロックされる。UPDATE文に含まれるWHERE句での検索もブロックされ,これはブロックされないSELECTによる検索とは別扱いになる。- 但し
UPDATE文のWHERE句上で,更新対象をサブクエリのSELECTから自己参照している場合は例外。トランザクション分離レベルをREPEATABLE READ以上にして,競合エラーからの復帰処理を書かなければならない。Postgres に関しては,
REPEATABLE READ以上では, MySQL よりも積極的・予防的に競合エラーを起こすようになっている。上記のようにWHERE句に含まれるサブクエリのSELECTから自己参照が発生しない場合,READ COMMITTEDにしておくのが最適解。
両データベースとも,書き込み処理競合時, REPEATABLE READ ではデッドロックを含むエラーが発生する前提の設計になっており,リトライすることが求められる。一方でエラーを絶対に回避したい場合は,
分離レベルを下げ、ギャップロックを無効化することでデッドロックを回避できたものの、
SELECT...FOR UPDATE句の取得結果がNULLであった場合にロックがかけられない(ロックする行がない)
とあるように, 更新時は READ COMMITTED でロッキングリードしておくことで対応できるものの, 新規作成時には(先行者のコミット完了まで)ロックする行が存在しない ことで後続者が素通りしてしまう問題がある。
そこで,新規作成を考慮しなければならない操作対象のリソースの代わりに,存在が保証されている別のリソースをロックするルールにしよう,という戦略を取ることができる。これは アドバイザリーロック(勧告的ロック) と呼ばれている。
引用した記事では, users というユーザ情報を格納する汎用的なテーブルをアドバイザリーロックのために使用していた。ところがコメント欄でも指摘があるように,汎用的なテーブルをアドバイザリーロックに流用すると,アプリケーション実装者の
「とりあえず users テーブルをロックしておこう!」
という愚行により, 「特定ユーザに関連してはいるものの内容的には全く関係のない処理」を無駄に待機させてしまう ことが起こるかもしれない。そのため,以下のような対応を取らなければならない。
また,アドバイザリーロックのライフサイクルとして,以下の 2 つの場合があり得る。
これらに着目しながら,各方式を検討していく。