web-dev-qa-db-ja.com

ON CONFLICT DO NOTHINGにもかかわらず複数行INSERTによるデッドロック

セットアップ

次のような一括挿入関数set_interactions(arg_rows text)があります。

with inserts as (
    insert into interaction (
        thing_id,
        associate_id, created_time)
    select t->>'thing_id', t->>'associate_id', now() from
    json_array_elements(arg_rows::json)  t
    ON CONFLICT (thing_id, associate_id) DO NOTHING
    RETURNING thing_id, associate_id
) select into insert_count count(*) from inserts;

-- Followed by an insert in an unrelated table that has two triggers, neither of which touch any of the tables here (also not by any of their triggers, etc.)

(「偽の行の更新」のトリックなしに、実際の挿入の数を取得する必要があるため、このようにラップします。)

テーブルinteractionには:

  1. ただ1つの制約:複数列の主キー(thing_id、associate_id)
  2. インデックスなし
  3. トリガーは1つだけ:挿入後、行ごとに。

トリガーはこれを行います:

DECLARE associateId text;

BEGIN

-- Go out and get the associate_id for this thing_id
BEGIN
    SELECT thing.associate_id INTO STRICT associateId FROM thing WHERE thing.id = NEW.thing_id;
    EXCEPTION
    WHEN NO_DATA_FOUND THEN
        RAISE EXCEPTION 'Could not map the thing to an associate!';
    WHEN TOO_MANY_ROWS THEN
        RAISE EXCEPTION 'Could not map the thing to a SINGLE associate!'; -- thing PK should prevent this
END;

-- We don't want to add an association between an associate interacting with their own things
IF associateId != NEW.associate_id THEN

    -- Insert the new association, if it doesn't yet exist
    INSERT INTO associations ("thing_owner", "associate")
    VALUES (associateId, NEW.associate_id)
    ON CONFLICT DO NOTHING;

END IF;

RETURN NULL;

END;

interactionsassociationsはどちらも、上記のステートメントに表示されている数を超える列はありません。

問題

アプリケーションがset_interactions()を呼び出すと、PostgreSQL 9.6.5からdeadlock detectedエラーが発生することがあります。 1〜100行に相当するデータを並べ替えずに呼び出す場合があります。 「競合する」バッチは、(バッチレベル全体または競合する行ごとに)同一の入力を持つ場合とそうでない場合があります。

エラーの詳細:

deadlock detected
while inserting index Tuple (37605,46) in relation "associations"
SQL statement INSERT INTO associations ("thing_owner", "associate")
    VALUES (associateId, NEW.associate_id)
    ON CONFLICT DO NOTHING;
PL/pgSQL function aud.addfriendship() line 19 at SQL statement
SQL statement "with inserts as (
        insert into interaction (
            thing_id,
            associate_id, created_time)
        select t->>'thing_id', t->>'associate_id', now() from
        json_array_elements(arg_rows::json)  t
        ON CONFLICT (thing_id, associate_id) DO NOTHING
        RETURNING thing_id, associate_id
    ) select                  count(*) from inserts"
PL/pgSQL function setinteractions(text) line 7 at SQL statement
Process 31370 waits for ShareLock on transaction 111519214; blocked by process 31418.
Process 31418 waits for ShareLock on transaction 111519211; blocked by process 31370.
error: deadlock detected

私が試したこと

たぶん、関数が1回の呼び出しで重複したデータで呼び出されている場合があると思いました。そうではありません:その代わりに、保証されたエラーON CONFLICT DO UPDATE command cannot affect row a second timeが発生します。

set_interactions()への1,000回の呼び出しを一度に試みても、同じパラメーターで、またはthing_idassociate_idの値を持つ(ペア内で異なる)同じ行のペアでさえ、デッドロックを再現できませんが、他の値もなので、PostgreSQLに到達する前に、なんらかの方法で最適化されません(関数がvolatileとマークされているため、データベースによって最適化されるべきではありません)。これは、シングルスレッドバックエンドからのものです。しかし同時に、アプリケーション自体は、デッドロックが発生している本番環境でそのようなバックエンドを1つだけ実行します。本番データベースのフルコピーに対してこれらの1,000回の呼び出しを実行し、2番目のバックエンドからの負荷がかかっている状態で、さらにinteractionsから選択する非常に長時間実行されているクエリを介してpgAdminから実行しようとしました。彼らは文句なしに成功します。

https://rcoh.svbtle.com/postgres-unique-constraints-can-cause-deadlock 一意のインデックスに依存しないようにすることについて言及している(私が理解しているように、これはPKの意味です) )重複を挿入する場合。しかし、それはON CONFLICT DO UPDATEの前であり、この問題を解決すると考えていました。

このクエリはどのように「ランダム」にデッドロックし、どのように修正できますか? (また、なぜ上記の方法でそれを再現できないのですか?)

5
Kev

_ON CONFLICT_句は、重複キーエラーを防止できます。同じキーを入力したり、同じ行を更新したりする同時トランザクションには依然として摩擦がある可能性があります。したがって、デッドロックに対する保険ではありません。

最も重要なのは、入力行への一貫した順序と_ORDER BY_を追加することです。注文が強制されるようにするために、結果を具体化するCTEを使用します。 (Ithinkサブクエリでも機能する必要があります。念のためです。)そうしないと、一意のインデックスに同じインデックスのタプルを入力しようとする相互に絡み合った挿入により、デッドロックが発生する可能性があります。あなたが観察した。 マニュアル:

デッドロックに対する最善の防御策は、データベースを使用するすべてのアプリケーションが複数のオブジェクトのロックを一貫した順序で確実に取得することで、デッドロックを回避することです。

また、set_interactions()はPL/pgSQL関数であるため、これはよりシンプルで安価です。

_WITH data AS (
   SELECT t->>'thing_id' AS t_id, t->>'associate_id' AS a_id
   -- Or, if not type text, cast right away:
   -- SELECT (t->>'thing_id')::int AS t_id, (t->>'associate_id')::int AS a_id
   FROM   json_array_elements(arg_rows::json) t
   ORDER  BY 1, 2  -- deterministic, stable order (!!)
   )
INSERT INTO interaction (thing_id, associate_id, created_time)
SELECT t_id, a_id, now()
FROM   data
ON     CONFLICT (thing_id, associate_id) DO NOTHING;

GET DIAGNOSTICS insert_count = ROW_COUNT;
_

別のCTE、RETURNINGおよび別のcount(*)は必要ありません。もっと:

トリガー機能も膨らんで見えます。エラーをキャッチせず、トランザクション全体をいずれかの方法でロールバックする例外を発生させるだけなので、ネストされたブロックは必要ありません。そして例外も無意味です。

  • _NO_DATA_FOUND_の最初のEXCEPTIONは、参照整合性を適用するFK制約がある適切な多対多の設計では決して発生しません。

  • 2つ目も無意味です-あなたは同じくらい疑っています:

    -PKはこれを防ぐ必要があります

トリガー機能は、要約すると次のとおりです。

_BEGIN
   -- Insert the new association, if it doesn't yet exist
   INSERT INTO associations (thing_owner, associate)
   SELECT t.associate_id, NEW.associate_id
   FROM   thing t
   WHERE  t.id = NEW.thing_id          --     -- PK guarantees 0 or 1 result
   AND    t.associate_id <> NEW.associate_id  -- exclude association to self
   ON     CONFLICT DO NOTHING;

   RETURN NULL;
END
_

トリガーと関数set_interactions()を完全に削除して、このクエリを実行するだけで、質問で確認できる便利なことをすべて実行できます。

_WITH data AS (
   SELECT (t->>'thing_id')::int AS t_id, (t->>'associate_id')::int AS a_id  -- asuming int
   FROM   json_array_elements(arg_rows::json) t
   ORDER  BY 1, 2  -- (!!)
   )
 , ins_inter AS (
   INSERT INTO interaction (thing_id, associate_id, created_time)
   SELECT t_id, a_id, now()
   FROM   data
   ON     CONFLICT (thing_id, associate_id) DO NOTHING
   RETURNING thing_id, associate_id
   )
 , ins_ass AS (
   INSERT INTO associations (thing_owner, associate)
   SELECT t.associate_id, i.associate_id
   FROM   ins_inter i
   JOIN   thing     t ON t.id = i.thing_id
                     AND t.associate_id <> i.associate_id  -- exclude association to self
   ON     CONFLICT DO NOTHING
   )
SELECT count(*) FROM ins_inter;
_

Now、デッドロックが発生する可能性はもうありません。もちろん、他のすべてのトランザクションも同じテーブルに同時に書き込む可能性があるため、同じ行の順序に固執する必要があります。

それが不可能な場合でも、_SKIP LOCKED_を検討している場合は、以下を参照してください。

6