データの参照範囲は各ステートメントで変化するので、リードコミッティドトランザクションを使用して、データ保全性に関するビジネスルールを強化するのは非常に難しいことです。また、書き込み競合が生じる場合、単一のステートメントでさえステートメントのスナップショットに限定されないかもしれません。
リピータブルリードトランザクションは実行全体にわたってデータの安定した参照範囲を持ちますが、MVCCスナップショットをデータ完全性チェックに使用することによる、読み取り/書き込み競合として知られるものを含む、微妙な問題があります。 1つのトランザクションがデータを書き、同時に実行するトランザクションが、同じデータ(書き込みの前に、あるいはその書き込みの後にも)を読むことを試みる場合、それは別のトランザクションの働きを見ることができません。 その後、読み手は、どれが最初にスタートしたか、あるいは、どれが最初にコミットしたかにかかわらず最初に実行したように見えます。 そのままいけば問題はありませんが、読み手がさらにデータを書けば、同時に実行したトランザクションがそれを読んだ場合、上で述べたトランザクションのどちらかの前に走ったように見えるトランザクションとなってしまいます。 最後に実行したように見えるトランザクションが実際には最初にコミットしていた場合、トランザクションの実行順のグラフには循環が容易に出現します。 そのような循環が出現する時、完全性のチェックはなにかしらの支援がなければ正しく動作しません。
13.2.3. シリアライザブル分離レベルにより述べたように、シリアライザブルトランザクションは、危険なパターンの読み取り/書き込み競合のための非ブロッキング監視を加えたリピータブルリードトランザクションです。 明白に実行順が循環を引き起こすパターンが検知された場合、含まれていたトランザクションのうちの1つは循環を断ち切るためにロールバックされます。
シリアライザブルトランザクション分離レベルが、データの一貫性を必要とするすべての書き込みおよびすべての読み取りに使用される場合、一貫性を確実にするために必要なことは他にはありません。 一貫性を保証するためにシリアライザブルトランザクションを使用するよう書かれている他の環境からのソフトウェアは、PostgreSQLでこの点に関して「正しく動く」べきです。
この技術を使用した場合、アプリケーションソフトウェアが直列化失敗でロールバックしたトランザクションを自動的に再試行するようなフレームワークを備えている場合、アプリケーションプログラマにとって不必要な負担を生み出さないようにするでしょう。
default_transaction_isolation
をserializable
にセットすることはよい考えかもしれません。
他のトランザクション分離レベルは使用されないことを保証する処置を講ずる、そうでなければ、不注意に完全位チェックを失わないよう、トリガーでトランザクション分離レベルのチェックをすることも賢明でしょう。
実行に関する提言は13.2.3. シリアライザブル分離レベルを参照してください。
シリアライザブルトランザクションを使用する整合性保護レベルは、まだホットスタンバイモード(26.5. ホットスタンバイ)には拡張されていません。そのために、ホットスタンバイを使用する場合は、マスタにおけるリピータブルリードと明示的なロック処理の利用が望まれるかもしれません。
非シリアライザブルの書き込みが可能な場合、
ある行の現時点の有効性を確実なものとし、同時更新を避けるためには、SELECT FOR UPDATE
文やSELECT FOR SHARE
文、適切なLOCK TABLE
文を使用する必要があります
(SELECT FOR UPDATE
文およびSELECT FOR SHARE
文は返ってきた行のみを同時に起こる更新からロックし、LOCK TABLE
はテーブル全体をロックします)。
これはPostgreSQLに他の環境からアプリケーションを移植する時に考慮されなければなりません
他の環境から切り替えた場合のさらなる注意点としては、同時実行トランザクションが選択された行を更新しないか削除しないということをSELECT FOR UPDATE
が保証しないという事実です。
PostgreSQLでそれをするためには、値を変更する必要がなくても、実際に行を更新しなければなりません。
SELECT FOR UPDATE
は、他のトランザクションが同じロックを獲得すること、または、ロックされた行に影響するUPDATE
またはDELETE
を実行することを一時的にブロックします。
しかしトランザクションがコミットするかロールバックして一度このロックを獲得すると、ロックが獲得されている間に、行の実際のUPDATE
が行なわれなかった場合、ブロックされたトランザクションは、競合した操作を続けることになります。
非シリアライザブルMVCCにおいては全体的な有効性チェックに特別な考慮を払わなければなりません。
例えば銀行のアプリケーションで、1つのテーブルにある全ての貸方の合計が、別のテーブルにある借方の合計と同じであることを、二つのテーブルが常に更新されているときに、チェックする必要があるとします。
2つの連続するSELECT SUM(...)
コマンドの結果を比べると、2番目の問い合わせは、おそらく最初の問い合わせによってカウントされなかったトランザクションの結果を含んでいるため、リードコミッティドモードでは信頼のおける処理を実行できないことがわかります。
1つのリピータブルリードトランザクションで2つの合計を出力すると、リピータブルリードトランザクションが開始される前にコミットされたトランザクション結果のみの正確な状況を得ることができます。
しかし、その結果がもたらされた時点でもなお妥当であるかどうかは、実際には疑わしいかもしれません。
整合性チェックを行う前にリピータブルリードトランザクション自身が変更を行った場合、そのチェックの有効性はさらに疑わしくなります。
これにより、トランザクション開始後に行われる変更の全てだけでなく、何か別のものが含まれるためです。
このような場合、注意深い人であれば、現状を確実に把握するためにチェックに必要な全てのテーブルをロックするでしょう。
SHARE
モード(もしくはそれ以上)のロックにより、現在のトランザクションでの変更を除き、ロックされたテーブルにコミットされていない変更が存在しないことが保証されます。
同時に、明示的なロック処理を使用して、同時に変更が実行されるのを防ごうとする場合、リードコミッティドモードを使用するか、または、リピータブルリードモードの場合は、問い合わせを実行する前にロックを獲得するよう留意してください。
リピータブルリードトランザクションにおいて獲得されたロックは、テーブルに変更をかける他のトランザクションが現在実行されていないことを保証します。
しかし、トランザクションが参照しているスナップショットが、ロックの獲得より前に取得されたものであれば、そのスナップショットは現時点においてコミットされている変更より前のテーブルのものである可能性があります。
リピータブルリードトランザクションのスナップショットは、実際にはその最初の問い合わせもしくはデータ変更コマンド(SELECT
、INSERT
、UPDATE
、またはDELETE
)が開始された時点で取得されます。
したがって、スナップショットを取得する前に、明示的にロックを獲得することが可能です。