web-dev-qa-db-ja.com

多対多の挿入/更新トリガー関数でデッドロックを防ぐ方法は?

私は多対多の挿入でデッドロックに問題があり、現時点では私のリーグからかなり離れています。

1秒あたり数千のレコードを受け取るTweetテーブルがあります。列の1つは、配列に0対多のURLを持つPostgreSQL _array[]::text[]_タイプです。 _{www.blah.com, www.blah2.com}_のように見えます。

Tweetテーブルのトリガーから達成しようとしていることは、_urls_starting_テーブルにエントリを作成し、_Tweet_x_url_starting_にTweet/url_starting関係を追加することです。

サイドノート:_url_starting_テーブルは、完全に解決されたURLパスが存在する_url_ending_テーブルにリンクされています。

私が直面している問題はデッドロックであり、他に何を試すべきかわかりません。
私は Erwin Brandstetter を学びました。 (あなたがそこにいるのなら...ありがとうございます!????)

  1. PostgreSQLに多対多の関係を実装する方法
  2. ON CONFLICT DO NOTHINGにもかかわらず複数行のINSERTによるデッドロック
  3. Postgres UPDATE…LIMIT 1 _(skip locked help)_

確定的で安定した注文とFOR UPDATE SKIP LOCKEDのORDER BYを追加してみましたが、どれが正しく行われているかはわかりません。

これが構造です。 PostgreSQL 10.5を使用しています

_CREATE TABLE Tweet(
    id integer NOT NULL GENERATED BY DEFAULT AS IDENTITY,
    Twitter_id text NOT NULL,
    created_at timestamp NOT NULL,
    content text NOT NULL,
    urls text[],
    CONSTRAINT Tweet_pk PRIMARY KEY (id)
);

CREATE TABLE url_starting(
    id integer NOT NULL GENERATED BY DEFAULT AS IDENTITY,
    url text NOT NULL,
    CONSTRAINT url_starting_pk PRIMARY KEY (id),
    CONSTRAINT url_starting_ak_1 UNIQUE (url)
);

CREATE TABLE Tweet_x_url_starting(
    id_Tweet integer NOT NULL,
    id_url_starting integer NOT NULL,
    CONSTRAINT Tweet_x_url_starting_pk PRIMARY KEY (id_Tweet,id_url_starting)

ALTER TABLE Tweet_x_url_starting ADD CONSTRAINT Tweet_fk FOREIGN KEY (id_Tweet)
REFERENCES Tweet (id) MATCH FULL
ON DELETE CASCADE ON UPDATE CASCADE;

ALTER TABLE Tweet_x_url_starting ADD CONSTRAINT url_starting_fk FOREIGN KEY (id_url_starting)
REFERENCES url_starting (id) MATCH FULL
ON DELETE CASCADE ON UPDATE CASCADE;
_

これはTweetテーブルトリガーです。

_CREATE TRIGGER create_Tweet_relationships
    AFTER INSERT OR UPDATE
    ON Tweet
    FOR EACH ROW
    EXECUTE PROCEDURE create_Tweet_relationships();
_

そして最後に、機能。

_CREATE FUNCTION create_Tweet_relationships ()
    RETURNS trigger
    LANGUAGE plpgsql
    VOLATILE 
    CALLED ON NULL INPUT
    SECURITY INVOKER
    COST 1
    AS $$
BEGIN
    IF (NEW.urls IS NOT NULL) AND cardinality(NEW.urls::TEXT[]) > 0 THEN
        WITH tmp_url AS (
          INSERT INTO url_starting (url)
          SELECT UNNEST(NEW.urls)
            ORDER BY 1
          ON CONFLICT (url) DO UPDATE
            SET url = EXCLUDED.url
          RETURNING id
        )
        INSERT INTO Tweet_x_url_starting (id_Tweet, id_url_starting)
        SELECT NEW.id, id
            FROM tmp_url
            ORDER BY 1, 2
            FOR UPDATE SKIP LOCKED
        ON CONFLICT DO NOTHING;
    END IF;

    RETURN NULL;
END
$$;
_

読んだものをやみくもに関数に投入しましたが、成功しませんでした。

エラーは次のようになります。

_deadlock detected
DETAIL:  Process 11281 waits for ShareLock on transaction 1317; blocked by process 11278.
Process 11278 waits for ShareLock on transaction 1316; blocked by process 11281.
HINT:  See server log for query details.
CONTEXT:  while inserting index Tuple (494,33) in relation "url_starting"
SQL statement "WITH tmp_url AS (
          INSERT INTO url_starting (url)
          SELECT UNNEST(NEW.urls)
      ORDER BY 1
          ON CONFLICT (url) DO UPDATE
              SET url = EXCLUDED.url
          RETURNING id
        )
        INSERT INTO Tweet_x_url_starting (id_Tweet, id_url_starting)
        SELECT NEW.id, id
            FROM tmp_url
    ORDER BY 1, 2
        FOR UPDATE SKIP LOCKED
        ON CONFLICT DO NOTHING"
PL/pgSQL function create_Tweet_relationships() line 12 at SQL statement

Error causing transaction rollback (deadlocks, serialization failures, etc).
_

デッドロックを停止するにはどうすればよいですか?ありがとう! ????

2
GollyJer

これは最終的に2つのものに煮詰められます。

  1. 挿入前に挿入されたデータがソートされていない場合、同時書き込みは最終的にデッドロックになります。私のトリガー関数内ではすべての挿入がソートされましたが、同時に追加されるすべてのURLをソートする方法がありませんでした。この問題を解決する唯一の方法は、1つのレベルをバックアップし、ツイートのバッチ全体で挿入/並べ替えを実行して、一度にすべてのURLにアクセスできるようにすることでした。

    詳細はこちら。 ???? PostgreSQLでON CONFLICTを指定してRETURNINGを使用する方法

    これを行うと、bigの違いが生じますが、問題は完全には修正されませんでした。 ????

  2. _ON CONFLICT_句は重複キーエラーを防止できますが、同時トランザクションが同じキー

    詳細はこちら。 ???? ON CONFLICT DO NOTHINGにもかかわらず複数行のINSERTによるデッドロック

    私の質問のエラーメッセージに示されているように、ON CONFLICT (column) DO UPDATEを実行すると、システムタプルインデックスctidに競合が発生しました。幸い、データを更新する必要がなかったので、クエリの_DO UPDATE_の部分は必要ありませんでした。

    これを修正すると100%デッドロックが停止しました! ????


ここに ????最後のクエリです pythonを使用して送信され、_psycopg2_execute_values()関数を使用)。

_WITH cte_data (Twitter_id, created_at, contents, search_hits, urls) AS (
    VALUES
    (NULL::text, NULL::timestamp, NULL::text, NULL::text[], NULL::text[]),
    %s
    OFFSET 1
)
, inserted_tweets AS (
    INSERT INTO Tweet (Twitter_id, created_at, contents, search_hits)
        SELECT Twitter_id, created_at, contents, search_hits
        FROM cte_data
        ORDER BY 1
    ON CONFLICT DO NOTHING
    RETURNING id, Twitter_id
    )
, inserted_tweets_with_urls AS (
    SELECT id, urls
    FROM inserted_tweets
    JOIN cte_data USING (Twitter_id)
)
, unique_urls AS (
    SELECT DISTINCT UNNEST(urls) url
    FROM cte_data
)
, new_urls AS (
    SELECT url
    FROM url_starting
    RIGHT JOIN unique_urls USING (url)
    WHERE id IS NULL
)
, inserted_urls AS (
    INSERT INTO url_starting (url)
        TABLE new_urls
        ORDER BY 1
    ON CONFLICT DO NOTHING
    RETURNING id, url
)
INSERT INTO Tweet_x_url_starting (id_Tweet, id_url_starting)
    SELECT it.id, iu.id
    FROM inserted_tweets_with_urls it
    JOIN inserted_urls iu
        ON (iu.url = ANY (it.urls))
    ORDER BY 1, 2
ON CONFLICT DO NOTHING;
_
1
GollyJer

これには、慎重な調査、完全な状況の知識、そして今よりも多くの時間を必要とする場合があります。あるいは、明らかな何かが欠けているのかもしれません。ここで他に何がうまくいかなくても、いくつかのことが際立っています:

  • FOR UPDATE SKIP LOCKEDを完全に削除します。どこで使用しても意味がありません。すでに排他ロックで行を保持しているCTEから選択している間は、意味がありません。また、クエリのこの段階で行をスキップしても意味がありません。

  • COST 1は誤解を招く恐れがあります。デフォルトはCOST 100であり、トリガー関数はCOST 5000の領域にあります。デフォルトのままにするか、より高く設定します。おそらくデッドロックとは無関係です。

  • AFTERトリガーは、単一のクエリ(複数のデータ変更CTEを使用)でワークフロー全体を書き換えるよりも、デッドロックの影響を受けやすい場合があります。

  • 暗闇の中でのショット:デッドロックは、FK制約がurl_startingでShareLockを取得しようとしたために発生しますが、同時実行トランザクションは、同様のShareLockを取得した後に同じ行を変更しようとします。余裕がある場合は、FK制約Tweet_x_url_startingを削除するのが手っ取り早い方法です。少なくとも試してみて、それが問題の一部であることを確認できます。

学習を続けたい場合は、関連性があると思われるもう1つを次に示します。

2