web-dev-qa-db-ja.com

シリアル化可能な分離レベルでトランザクションにinsert-if-not-foundを実装するにはどうすればよいですか?

「見つからない場合に挿入」関数を正確に実装する方法を理解するのに苦労しています。以下を検討してください。

artistというテーブルがあり、2列、(name, id)ここで、nameは一意であり、idはシリアル主キーです。これは不自然な例ですが、私の問題を示しています。

    SESSION A                     SESSION B
1.                                SELECT id FROM artist
                                    WHERE name = 'Bob';
2.  INSERT INTO artist (name)
      VALUES ('Bob')
3.                                INSERT INTO artist (name)
                                    VALUES ('Bob')
4.   code that users 'Bob'
     (e.g., a FK to Bob's ID)
5.                                ??? Bob already exists, but we
                                  can't find it
4.  COMMIT

セッションBは、ボブと呼ばれるartistを見つけようとして開始されますが、失敗します。ただし、セッションAはボブを作成します。セッションBは、主キーに違反しているため失敗するボブというアーティストを挿入しようとします。しかし、ここにビットI しないでください取得-artistの選択に操作3を変更した場合、テーブルはまだ空です!これは、シリアル化可能な分離レベルを使用しているためですが、このケースをどのように処理できますか?

私が持っている唯一のオプションは、トランザクション全体を中止して再試行することです。これが事実である場合、アプリケーションを再試行する必要があることを示す、独自の「シリアライズできない」例外をスローする必要がありますか?私はすでにこの「検索または挿入」をplpgsql関数で望んでおり、ここでINSERTを実行し、それが失敗した場合はSELECTですが、競合する行を見つけることは不可能のようです...

5
ocharles

これはちょっとしたFAQです。 ON DUPLICATE KEY UPDATE(MySQL構文)、MERGE(SQL標準構文)、またはUPSERTを検索すると、さらに情報が得られます。意外と大変です。

私が今まで見た中で最高の記事は Depeszの「なぜupsertが複雑なのか」 です。 SO質問 挿入、更新の重複(postgresql) もありますが、これには提案はありますが、問題の説明と議論が欠けています。

簡単に言えば、そうです。

私が持っている唯一のオプションは、トランザクション全体を中止して再試行することです。

SERIALIZABLEトランザクションを使用する場合、トランザクションが失敗したときに再発行する必要があります。彼らはどちらでしょう。設計上-競合検出が大幅に改善されたため、Pg 9.1以降ではより頻繁に使用されます。アップサートのような操作は非常に競合が激しいため、かなりの再試行が発生する可能性があります。 READ COMMITTEDトランザクションでアップサートを実行できる場合は代わりに役立ちますが、いくつかの避けられない競合状態があるため、再試行する準備をしておく必要があります。

競合する行を挿入すると、トランザクションは一意の違反で失敗します。トランザクションからSQLSTATE 23505 unique_violationエラーが発生し、更新/挿入を試みていたことがわかっている場合は、再試行してください。 SQLSTATE 40001 serialization_failureを取得した場合は、再試行する必要もあります。

基本的に、PL/PgSQL関数内で再試行を行うことはできません(dblinkのようなダーティなハックがない場合)。アプリケーション側で行う必要があります。 PostgreSQLに自律型トランザクションを持つストアドプロシージャがあった場合、それは可能ですが、不可能です。 READ COMMITTEDモードでは、トランザクションの開始以降に行われた競合する挿入をチェックできますが、PL/PgSQL関数を開始するステートメントの後には開始できませんしたがって、READ COMMITTEDでも、「選択との競合の検出」アプローチは機能しません。

Depeszの記事を読んで、より適切で詳細な説明を読んでください。

5
Craig Ringer

私がこれで扱ったほとんどのアプリケーションでこれは可能ですが、優れたトランザクション管理ではほとんど発生しません。ユーザーとの通信中はトランザクションを開かないことを強くお勧めします。ほとんどの場合、これにより1秒未満のトランザクション時間が発生します。楽観的ロックはここでの友です。

トランザクションは次のようになります。

  • ユーザーAとBはボブを検索しましたが、ボブは見つかりません。
  • AとBの両方がボブを追加しようとします。
  • Bの追加が最初に到着し、コミットされます。
  • Aの追加は後で到着し、適切に処理されます(設計決定)。
  • AとBの両方がボブを見つけることができます。

AとBの両方が同時に追加を送信すると、競合状態になる可能性がありますが、実際にはほとんどありません。ワークフローによっては、この問題が発生するのは更新の方が多いです。この場合、最後に送信したユーザーは通常、他のユーザータイプエラーによってデータ更新を取得します。更新されたデータを取得した場合、必要に応じて更新を再試行できます。 2番目の更新が競合しない場合は、暗黙的にスキップするか、必要に応じてその変更を適用できます。

実行時間の長いトランザクションは、データの不整合を引き起こす可能性があります。

0
BillThor