PostgreSQLでの同時DELETE / INSERTのロックの問題


35

これは非常に簡単ですが、PG(v9.0)の機能に困惑しています。簡単な表から始めます。

CREATE TABLE test (id INT PRIMARY KEY);

数行:

INSERT INTO TEST VALUES (1);
INSERT INTO TEST VALUES (2);

お気に入りのJDBCクエリツール(ExecuteQuery)を使用して、2つのセッションウィンドウをこのテーブルが存在するdbに接続します。両方ともトランザクション対応です(つまり、auto-commit = false)。それらをS1およびS2と呼びましょう。

それぞれに同じコードのビット:

1:DELETE FROM test WHERE id=1;
2:INSERT INTO test VALUES (1);
3:COMMIT;

次に、これをスローモーションで実行し、ウィンドウで1つずつ実行します。

S1-1 runs (1 row deleted)
S2-1 runs (but is blocked since S1 has a write lock)
S1-2 runs (1 row inserted)
S1-3 runs, releasing the write lock
S2-1 runs, now that it can get the lock. But reports 0 rows deleted. HUH???
S2-2 runs, reports a unique key constraint violation

現在、これはSQLServerで正常に機能します。S2が削除を行うと、1行が削除されたことを報告します。そして、S2の挿入は正常に機能します。

PostgreSQLはその行が存在するテーブルのインデックスをロックしているのに対し、SQLServerは実際のキー値をロックしていると思われます。

私は正しいですか?これを機能させることはできますか?

回答:


39

マットとアーウィンはどちらも正しいです。コメントに収まらない方法で彼らが言ったことをさらに拡張するために、私は別の答えを追加するだけです。彼らの答えはすべての人を満足させるものではないようであり、PostgreSQL開発者に相談するべきだという提案があったので、私もその1つです。

ここで重要な点は、SQL標準では、READ COMMITTEDトランザクション分離レベルで実行されているトランザクション内で、コミットされていないトランザクションの作業が表示されてはならないという制限があることです。ときにコミットされたトランザクションの作業が見えるようになり、実装依存です。あなたが指摘しているのは、2つの製品がそれを実装するために選択した方法の違いです。どちらの実装も標準の要件に違反していません。

PostgreSQL内で起こることの詳細は次のとおりです。

S1-1の実行(1行削除)

S1はまだロールバックする可能性があるため、古い行はそのまま残りますが、S1は行のロックを保持するため、行を変更しようとする他のセッションはS1がコミットまたはロールバックするかどうかを待機します。いずれかが読み彼らがそれをロックしようとしない限り、まだ、古い行を見ることができるテーブルのSELECT FOR UPDATESELECT FOR SHARE

S2-1が実行されます(ただし、S1には書き込みロックがあるためブロックされます)

S2は、S1の結果を確認するために待機する必要があります。S1がコミットではなくロールバックする場合、S2は行を削除します。S1がロールバックする前に新しいバージョンを挿入した場合、新しいバージョンは他のトランザクションの観点からは存在せず、古いバージョンは他のトランザクションの観点から削除されないことに注意してください。

S1-2の実行(1行挿入)

この行は、古い行から独立しています。id = 1の行の更新があった場合、古いバージョンと新しいバージョンが関連付けられ、S2はブロックが解除されたときに行の更新されたバージョンを削除できます。新しい行がたまたま過去に存在したいくつかの行と同じ値を持つことは、その行の更新されたバージョンと同じにはなりません。

S1-3が実行され、書き込みロックが解除されます

したがって、S1の変更は保持されます。1行がなくなりました。1行追加されました。

S2-1が実行され、ロックを取得できるようになりました。ただし、0行が削除されたと報告されます。え?

内部で行われることは、行のあるバージョンから、同じ行の次のバージョンが更新された場合、その行へのポインターがあるということです。行が削除された場合、次のバージョンはありません。場合READ COMMITTEDトランザクションが書き込み競合上のブロックから目覚めさせ、それが最後までその更新鎖を、以下、行が削除されておらず、クエリの選択基準をまだ満たしている場合、処理されます。この行は削除されているため、S2のクエリは続行します。

S2は、テーブルのスキャン中に新しい行に到達する場合と到達しない場合があります。存在する場合は、S2のDELETEステートメントが開始された後に新しい行が作成されたことがわかるため、そこに表示される行セットの一部ではありません。

PostgreSQLが新しいスナップショットを使用してS2のDELETEステートメント全体を最初から再起動する場合、SQL Serverと同じように動作します。PostgreSQLコミュニティは、パフォーマンス上の理由からそうすることを選択していません。この単純なケースでは、パフォーマンスの違いに気付くことはありませDELETEんが、ブロックされたときに1000万行が入っていた場合、間違いなく気付くでしょう。高速バージョンは依然として標準の要件に準拠しているため、PostgreSQLがパフォーマンスを選択した場合、トレードオフがあります。

S2-2が実行され、一意キー制約違反が報告されます

もちろん、行はすでに存在します。これは写真の最も驚くべき部分です。

ここには驚くべき動作がいくつかありますが、すべてがSQL標準に準拠しており、標準に従って「実装固有」であるものの範囲内です。他の実装の動作がすべての実装に存在すると想定している場合、確かに驚くかもしれませんが、PostgreSQLはREAD COMMITTED分離レベルでのシリアル化の失敗を避けるために非常に努力し、それを達成するために他の製品とは異なる動作を許可します。

今、私は個人的にREAD COMMITTEDどの製品の実装でトランザクション分離レベルの大ファンではありません。これらはすべて、競合状態がトランザクションの観点から驚くべき動作を作成することを可能にします。誰かが1つの製品で許可されている奇妙な動作に慣れると、「正常」であり、別の製品で選択されたトレードオフが奇妙であると考える傾向があります。しかし、すべての製品は、実際にとして実装されていないモードに対して何らかのトレードオフを行う必要がありますSERIALIZABLE。PostgreSQL開発者が線を引くことを選択したのは、READ COMMITTEDブロッキングを最小限に抑えることです(読み取りは書き込みをブロックせず、書き込みは読み取りをブロックしません)。そして、シリアル化の失敗の可能性を最小限にします。

標準では、SERIALIZABLEトランザクションをデフォルトにする必要がありますが、ほとんどの製品は、より緩やかなトランザクション分離レベルでパフォーマンスヒットを引き起こすため、これを行いません。一部の製品SERIALIZABLEは、選択されたときに真にシリアル化可能なトランザクションさえ提供しません。最も顕著なのは、Oracleおよび9.1より前のPostgreSQLのバージョンです。しかし、真のSERIALIZABLEトランザクションを使用することは、競合状態からの驚くべき影響を回避する唯一の方法であり、SERIALIZABLEトランザクションは常に競合状態を回避するためにブロックするか、競合状態の発生を回避するために一部のトランザクションをロールバックする必要があります。SERIALIZABLEトランザクションの最も一般的な実装は、ブロッキングとシリアル化の両方の障害(デッドロックの形で)が発生するStrict Two-Phase Locking(S2PL)です。

完全な開示:MITのダンポートと協力して、シリアライズ可能なスナップショット分離と呼ばれる新しい手法を使用して、PostgreSQLバージョン9.1に真にシリアライズ可能なトランザクションを追加しました。


この作業を行うための本当に安価な(安っぽい?)方法は、2つのDELETEの後にINSERTを発行することだろうかと思います。私の限られた(2スレッド)テストでは問題なく動作しましたが、それを多くのスレッドに当てはまるかどうかを確認するためにさらにテストする必要があります。
-DaveyBob

READ COMMITTEDトランザクションを使用している限り、競合状態が発生します。最初のDELETE開始後、2番目のDELETE開始前に別のトランザクションが新しい行を挿入するとどうなりますか?SERIALIZABLE競合状態をクローズする2つの主な方法よりも厳密でないトランザクションでは、競合の促進(ただし、行が削除されている場合は役に立ちません)と競合の具体化です。行を削除するたびに更新される「id」テーブルを使用するか、テーブルを明示的にロックすることにより、競合を実現できます。または、エラー時に再試行を使用します。
kgrittn

再試行します。貴重な洞察に感謝します!
-DaveyBob

21

PostgreSQL 9.2の読み取りコミットされた分離レベルの説明によると、これは設計によるものだと思います。

UPDATE、DELETE、SELECT FOR UPDATE、およびSELECT FOR SHAREコマンドは、ターゲット行の検索に関してはSELECTと同じように動作します。コマンド開始時刻1 でコミットされたターゲット行のみを検出します。ただし、このようなターゲット行は、検出されるまでに別の並行トランザクションによってすでに更新(または削除またはロック)されている場合があります。この場合、アップデーターは、最初の更新トランザクションがコミットまたはロールバックするまで待機します(まだ進行中の場合)。最初のアップデーターがロールバックすると、その効果は無効になり、2番目のアップデーターは最初に見つかった行の更新を続行できます。最初のアップデーターがコミットした場合、最初のアップデーターが削除した場合、2番目のアップデーターは行を無視します2そうでない場合、更新されたバージョンの行に操作を適用しようとします。

あなたが挿入行がS1時にまだ存在していなかったS2のがDELETE始まりました。したがって、上記のS21)の削除では表示されません。1 S1削除をすることによって無視さS2DELETE(によると2)。

したがってS2、削除では何も実行されません。インサートは、かかわらずやって来る、1のことをするとを参照S1の挿入:

Read Committedモードでは、その時点までにコミットされたすべてのトランザクションを含む新しいスナップショット各コマンドが開始されるため、同じトランザクション内の後続のコマンドは、いずれの場合でもコミットされた同時トランザクションの効果を確認します。上記の問題点は、単一のコマンドがデータベースの完全に一貫したビューを表示するかどうかです。

したがって、挿入の試みS2は制約違反で失敗します。

そのドキュメントを読み続ける、繰り返し可能な読み取り、またはシリアライズ可能であっても、問題を完全に解決することはできません。2番目のセッションは、削除時にシリアル化エラーで失敗します。

これにより、トランザクションを再試行できます。


ありがとう、マット。それが起こっていることのように見えますが、その論理には欠陥があるようです。READ_COMMITTED isoレベルでは、これら2つのステートメント tx内で成功する必要があるようです:DELETE FROMテストWHERE ID = 1 INSERT INTOテストVALUES(1)つまり、行を削除してから行を挿入すると、その挿入は成功するはずです。SQLServerはこれを正しく行います。現状では、両方のデータベースで動作しなければならない製品でこの状況に対処するのは非常に苦労しています。
-DaveyBob

11

@Matの素晴らしい回答に完全に同意します。コメントに収まらないので、私は別の答えを書きます。

コメントへの返信:DELETES2の特定の行バージョンに既にフックされています。これはその間にS1によって殺されるため、S2は成功したと見なします。一見すると明らかではありませんが、一連のイベントは事実上次のようになります。

   S1削除成功  
S2 DELETE(プロキシにより成功-S1からDELETE)  
   S1 は、その間に削除された値を事実上再挿入します  
S2 INSERTが一意キー制約違反で失敗する

それはすべて設計によるものです。SERIALIZABLE要件にトランザクションを使用し、シリアル化の失敗時に再試行する必要があります。


1

DEFERRABLE主キーを使用して、再試行してください。


ヒントをありがとう、しかしDEFERRABLEを使用してもまったく違いはありませんでした。ドキュメントは読み、それが持っている必要がありますように、しかししません。
-DaveyBob

-2

この問題にも直面しました。私たちのソリューションはselect ... for update前に追加 していdelete from ... whereます。分離レベルは、コミット読み取りでなければなりません。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.