web-dev-qa-db-fra.com

Blocage avec INSERT à plusieurs lignes malgré ON CONFLICT DO NOTHING

Installer

J'ai une fonction d'insertion en bloc set_interactions(arg_rows text) qui ressemble à ceci:

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.)

(Je l'enroule de cette façon car j'ai besoin d'obtenir un nombre d'insertions réelles, sans l'astuce "fausses mises à jour de lignes".)

La table interaction a:

  1. Une seule contrainte: une clé primaire à plusieurs colonnes (thing_id, Associate_id)
  2. Pas d'indices
  3. Un seul déclencheur: après insertion, pour chaque ligne.

Le déclencheur fait ceci:

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;

interactions et associations n'ont pas plus de colonnes que vous ne le voyez dans les instructions ci-dessus.

Problème

Parfois, j'obtiens une erreur deadlock detected De PostgreSQL 9.6.5 lorsque l'application appelle set_interactions(). Il peut l'appeler avec 1 à 100 lignes de données, non triées; les lots "en conflit" peuvent ou non avoir une entrée identique (au niveau du lot entier ou pour chaque ligne en conflit).

Détails de l'erreur:

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

Ce que j'ai essayé

Je pensais que peut-être la fonction était parfois appelée avec des données en double en un seul appel. Pas le cas: cela entraîne à la place une erreur garantie, ON CONFLICT DO UPDATE command cannot affect row a second time.

Je ne suis pas en mesure de reproduire le blocage, même en essayant 1 000 appels à set_interactions() à la fois avec des paramètres identiques, ou même avec des paires de lignes identiques ayant (différentes dans la paire) thing_id Et associate_id Mais d'autres valeurs aussi, donc elles ne sont pas optimisées avant de frapper PostgreSQL (elles ne devraient pas non plus être optimisées par la base de données, car la fonction est marquée volatile.) provient d'une extrémité arrière à filetage unique; mais en même temps, l'application elle-même n'exécute qu'un seul back-end en production, où l'impasse se produit. J'ai même essayé d'exécuter ces 1 000 appels sur une copie complète de la base de données de production, et même sous la charge d'un second back-end, et en plus de pgAdmin via une requête très longue qui sélectionne parmi interactions. Ils réussissent sans se plaindre.

https://rcoh.svbtle.com/postgres-unique-constraints-can-cause-deadlock mentionne essayer d'éviter de s'appuyer sur un index unique (ce à quoi correspond le PK, si je comprends bien) ) lors de l'insertion de doublons. Cependant, c'était avant ON CONFLICT DO UPDATE, Ce qui, je pense, résoudrait ce problème.

Comment cette requête est-elle bloquée de manière "aléatoire" et comment puis-je la résoudre? (Aussi, pourquoi ne puis-je pas le reproduire avec la méthode ci-dessus?)

5
Kev

La clause ON CONFLICT Peut empêcher les erreurs clé en double. Il peut toujours y avoir des frictions avec des transactions simultanées essayant d'entrer les mêmes clés ou de mettre à jour les mêmes lignes. Ce n'est donc pas une assurance contre les blocages.

Plus important encore, ajoutez un ordre cohérent pour saisir les lignes avec ORDER BY. Pour m'assurer que la commande est exécutée, j'utilise un CTE, qui matérialise le résultat. (I pensez cela devrait aussi fonctionner avec une sous-requête; juste pour être sûr.) Sinon, des insertions mutuellement enchevêtrées essayant d'entrer des tuples d'index identiques dans l'index unique peuvent conduire à l'impasse que vous avez observée. Le manuel:

La meilleure défense contre les blocages est généralement de les éviter en étant certain que toutes les applications utilisant une base de données acquièrent des verrous sur plusieurs objets dans un ordre cohérent.

De plus, puisque set_interactions() est une fonction PL/pgSQL, c'est plus simple et moins cher:

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;

Pas besoin d'un autre CTE, RETURNING et un autre count(*). Plus:

La fonction de déclenchement semble également gonflée. Pas besoin d'un bloc imbriqué, car vous n'attrapez pas d'erreurs, ne soulevant que des exceptions qui annulent la transaction entière dans tous les cas. Et les exceptions sont également inutiles.

La fonction de déclenchement se résume à:

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

Vous pouvez supprimer complètement le déclencheur et la fonction set_interactions() et simplement exécuter cette requête, en faisant tout ce que je peux voir dans la question:

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;

Maintenant, je ne vois plus aucune chance de blocage. Bien sûr, toutes les autres transactions pouvant également écrire simultanément dans la même table doivent respecter le même ordre de lignes.

Si ce n'est pas possible et que vous envisagez toujours SKIP LOCKED, Consultez:

6
Erwin Brandstetter