web-dev-qa-db-ja.com

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

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

CREATE TABLE test (id INT PRIMARY KEY);

といくつかの行:

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

私のお気に入りのJDBCクエリツール(ExecuteQuery)を使用して、このテーブルが存在するデータベースに2つのセッションウィンドウを接続します。どちらもトランザクション対応です(つまり、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の挿入は正常に機能します。

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

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

36
DaveyBob

MatとErwinはどちらも正しいので、私は別の答えを追加して、コメントに収まらない方法で彼らが言ったことをさらに拡張しています。彼らの答えはすべての人を満足させるようには見えないので、PostgreSQL開発者に相談するべきであるという提案がありました。私もその一人なので、詳しく説明します。

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

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

 S1-1実行(1行削除)

S1がまだロールバックする可能性があるため、古い行はそのまま残りますが、S1は行にロックを保持するため、行を変更しようとする他のセッションはS1がコミットするかロールバックするかを確認するために待機します。テーブルのreadsは、SELECT FOR UPDATEまたはSELECT FOR SHAREでロックしようとしない限り、古い行を表示できます。

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

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

 S1-2ラン(1行挿入)

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

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

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

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

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

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

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

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

もちろん、その行はすでに存在しています。これは、画像の中で最も驚くべき部分ではありません。

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

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

標準では、SERIALIZABLEトランザクションがデフォルトであることを要求していますが、ほとんどの製品は、より緩やかなトランザクション分離レベルでパフォーマンスに影響を与えるため、これを行いません。一部の製品は、SERIALIZABLEが選択されている場合、真にシリアル化可能なトランザクションさえ提供しません-特にOracleおよび9.1より前のバージョンのPostgreSQL。ただし、真にSERIALIZABLEトランザクションを使用することが、競合状態による予期しない影響を回避する唯一の方法であり、SERIALIZABLEトランザクションは常に競合状態を回避するためにブロックするか、競合状態の発生を回避するために一部のトランザクションをロールバックする必要があります。 SERIALIZABLEトランザクションの最も一般的な実装は、ブロッキングとシリアル化の両方の失敗(デッドロックの形)を伴う厳密な2フェーズロック(S2PL)です。

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

40
kgrittn

PostgreSQL 9.2の read-committed分離レベル の説明によると、これは仕様によるものだと思います。

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

S1に挿入した行は、S2DELETEの開始時にまだ存在していませんでした。そのため、S2の削除では表示されません(1)上記。 (S1が削除したものは、S2DELETEによって無視されます(2)。

したがって、S2では、削除は何もしません。ただし、挿入が発生すると、その挿入が実行されますS1の挿入を参照してください。

コミット読み取りモードは、その瞬間までにコミットされたすべてのトランザクションを含む新しいスナップショットを使用する各コマンドを開始するため、同じトランザクション内の後続のコマンドは、コミットされた同時トランザクションの影響を常に確認します。。上記の問題点は、単一のコマンドがデータベースの完全に一貫したビューを見るかどうかです。

そのため、S2による挿入の試行は、制約違反で失敗します。

反復可能読み取りまたはserializableを使用してそのドキュメントを読み続けても問題は完全に解決されません-2番目のセッションはシリアル化で失敗します削除時のエラー。

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

21
Mat

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

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

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

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

11

[〜#〜] deferrable [〜#〜] 主キーを使用して、再試行してください。

0
Frank Heikens