標準としてのトランザクション分離レベルは一応把握してたけど、MySQLやPostgreSQLとか、実装によって結構事情が異なっててそのあたりあまり理解できてなかったのでPostgreSQLにおいてのトランザクション分離レベルを学び直した。
※参考にしたのはこのあたり
postgeSQLのトランザクション分離レベルについて
www.postgresql.jp
トランザクションで発生する問題(ダーティーリードやファントムリードの具体的なサンプルなど)
postd.cc
結論「どういう時にどうすればいいか?」
色々学び直した結果、PostgreSQLでは「どういう時にどうすればいいか?」を検討したので書く。
- 基本的にはデフォルト(read committed)指定で良い。
- 不整合が起こる可能性がある場合にrepeatable readを明示的に指定する。
- repeatable readを明示的に指定する場合、クエリ実行失敗に備えての再実行処理を組み込む事を検討する
「基本的にはデフォルト(read committed)指定で良い」について
トランザクション分離レベルは高く設定(serialized方向)すれば、不整合が起きず安心だがパフォーマンスが落ちる。
ほとんどのトランザクションは、重複するレコードを同時に更新せず、不整合が起きる危険性は無いので基本的にはデフォルト設定(read committed)で問題ない。
一部の「不整合が発生する可能性があるトランザクション」にのみ明示的により高い分離レベルを指定すればいい。(下記項目に示す)
「不整合が起こる可能性がある場合にrepeatable readを明示的に指定する」について
read committedで「不整合が起こる可能性がある場合」とは「non repeatable read」か「phantom read」が起こる可能性がある場合である。
具体的なサンプルは
SQLトランザクション分離 実践ガイド | POSTD を見るとわかりやすかった。
この場合、このトランザクションは明示的にrepeatable readを明示的に指定すればいい。 ※PostgreSQLの場合、repeatable readにすれば「non repeatable read」も「phantom read」も起こらない。
ただしこの場合「クエリ実行失敗に備えての再実行処理を組み込む事を検討する」必要がある。(下記項目に示す)
「repeatable readを明示的に指定する場合、クエリ実行失敗に備えての再実行処理を組み込む事を検討する」について
PostgreSQLの「repeatable read」は実行失敗する可能性がある。
例えば、repeatable read指定で、
- T1がid=1のレコードをselectする。
- T2がid=1のレコードに対してupdateを実行してcommitする。
- T1がid=1のレコードをselectする。(1の実行時点と同じ値が返ってくる(repeatable readが実現している))
- T1がid=1のレコードに対してupdateを実行すると、エラーが発生する。(楽観的ロックにより2で実行されたupdateを検知しエラーが発生する)
この場合、自動的に再実行すれば処理を完了させられる可能性がある為、コード側に再実行機構を入れておく必要がある。
※この前まで勘違いしていて、上記2の処理ではT1がcommitするまでwaitがかかる(悲観的ロックがかかる)と思っていた。
ちなみにMySQL(innodb)の場合はネクストキーロックによってphantom readを防いでおり、上記の場合はT1がcommitするまでwaitがかかると思われる(検証はしてない)
(補足)for updateについて
select for updateを実行すると、取得したレコードに対して更新ロック(悲観的ロック)をかける。
悲観的ロックなので別トランザクションはトランザクション終了まで更新を待たされることになる。
「あるレコード値を取得してそれを元に計算し、DBに書き戻す」ような処理を行いたいときに行う