例えば「ユーザー名」など重複を許さない項目はDBへ登録クエリを投げる前にバリデーションで重複チェックするが、普通にやっていると「重複チェックをパスしたのに、DB登録実行時点では重複が発生している」となるケースが存在する。
「重複チェック」と「DB登録実行」が同一トランザクション内に記載されていても起こる。
※ファントムリードが起こるために発生する。
二人(A君、B君)が別の画面から同時に同じユーザー名で登録を実行した場合
A君:重複チェック => OK(重複は存在しない) B君:重複チェック => OK(重複は存在しない) A君:DB登録実行 => OK(重複は存在しない) B君:DB登録実行 => NG(A君の登録したユーザー名と重複)
この状況を防ぐには、「重複チェック」の際に対象テーブルに対してテーブルロックをかける必要がある。 そうすれば、イメージ通りの挙動になる。(が、非推奨) (また、isolation levelをserializableにすることで下記イメージで実行されることを期待したが違った。 参照 => トランザクション分離レベル Serializableでも同時実行でエラーがでる可能性がある - やる気がストロングZERO )
A君:重複チェック => OK(重複は存在しない)※ここでテーブルロックをかける B君:重複チェック => テーブルロック開放待ち A君:DB登録実行 => OK(重複は存在しない)※テーブルロックが開放される B君:重複チェック => NG(A君の登録したユーザー名と重複)
ただしこれだと、あまり起こらないケースへの対応のために常時のパフォーマンスが犠牲になっている。
潔癖に「バリデーションでキッチリ不整合を拾いたい」といろいろ考えたが、 このケースでは単純に「DB登録を失敗させて、再度ユーザーに操作を促す」が正解だと思った。
(2020/12/14)追記:
アプリケーション側での対応を検討した。