2013-12-10 5 views
3

Используя postgres 9.3, у меня есть таблица с именем regression_runs, которая хранит некоторые счетчики. Когда строка в этой таблице обновляется, вставлена ​​или удаляется, вызывается функция триггера для обновления строки в таблице nightly_runs, чтобы сохранить общее количество этих счетчиков для всех regression_runs с данным идентификатором. Подход, который я принял, довольно широко документирован. Моя проблема, однако, в том, что я столкнулся с тупиками, когда несколько процессов пытаются одновременно вставлять новые строки в таблицу regression_runs с тем же nightly_run_id.deadlock в функции триггера postgresql

В таблице regression_runs выглядит следующим образом:

regression=> \d regression_runs 
             Table "public.regression_runs" 
    Column  |   Type   |       Modifiers       
-----------------+--------------------------+-------------------------------------------------------------- 
id    | integer     | not null default nextval('regression_runs_id_seq'::regclass) 
username  | character varying(16) | not null 
nightly_run_id | integer     | 
nightly_run_pid | integer     | 
passes   | integer     | not null default 0 
failures  | integer     | not null default 0 
errors   | integer     | not null default 0 
skips   | integer     | not null default 0 
Indexes: 
    "regression_runs_pkey" PRIMARY KEY, btree (id) 
    "regression_runs_nightly_run_id_idx" btree (nightly_run_id) 
Foreign-key constraints: 
    "regression_runs_nightly_run_id_fkey" FOREIGN KEY (nightly_run_id) REFERENCES nightly_runs(id) ON UPDATE CASCADE ON DELETE CASCADE 
Triggers: 
    regression_run_update_trigger AFTER INSERT OR DELETE OR UPDATE ON regression_runs FOR EACH ROW EXECUTE PROCEDURE regression_run_update() 

В таблице nightly_runs выглядит следующим образом:

regression=> \d nightly_runs 
            Table "public.nightly_runs" 
    Column |   Type   |       Modifiers       
------------+--------------------------+----------------------------------------------------------- 
id   | integer     | not null default nextval('nightly_runs_id_seq'::regclass) 
passes  | integer     | not null default 0 
failures | integer     | not null default 0 
errors  | integer     | not null default 0 
skips  | integer     | not null default 0 
Indexes: 
    "nightly_runs_pkey" PRIMARY KEY, btree (id) 
Referenced by: 
    TABLE "regression_runs" CONSTRAINT "regression_runs_nightly_run_id_fkey" FOREIGN KEY (nightly_run_id) REFERENCES nightly_runs(id) ON UPDATE CASCADE ON DELETE CASCADE 

Функция триггера regression_run_update заключается в следующем:

CREATE OR REPLACE FUNCTION regression_run_update() RETURNS "trigger" 
    AS $$ 
     BEGIN 
     IF TG_OP = 'UPDATE' THEN 
       IF (NEW.nightly_run_id IS NOT NULL) and (NEW.nightly_run_id = OLD.nightly_run_id) THEN 
         UPDATE nightly_runs SET passes = passes + (NEW.passes - OLD.passes), failures = failures + (NEW.failures - OLD.failures), errors = errors + (NEW.errors - OLD.errors), skips = skips + (NEW.skips - OLD.skips) WHERE id = NEW.nightly_run_id; 
       ELSE 
         IF NEW.nightly_run_id IS NOT NULL THEN 
           UPDATE nightly_runs SET passes = passes + NEW.passes, failures = failures + NEW.failures, errors = errors + NEW.errors, skips = skips + NEW.skips WHERE id = NEW.nightly_run_id; 
         END IF; 
         IF OLD.nightly_run_id IS NOT NULL THEN 
           UPDATE nightly_runs SET passes = passes - OLD.passes, failures = failures - OLD.failures, errors = errors - OLD.errors, skips = skips - OLD.skips WHERE id = OLD.nightly_run_id; 
         END IF; 
       END IF; 
     ELSIF TG_OP = 'INSERT' THEN 
       IF NEW.nightly_run_id IS NOT NULL THEN 
         UPDATE nightly_runs SET passes = passes + NEW.passes, failures = failures + NEW.failures, errors = errors + NEW.errors, skips = skips + NEW.skips WHERE id = NEW.nightly_run_id; 
       END IF; 
     ELSIF TG_OP = 'DELETE' THEN 
       IF OLD.nightly_run_id IS NOT NULL THEN 
         UPDATE nightly_runs SET passes = passes - OLD.passes, failures = failures - OLD.failures, errors = errors - OLD.errors, skips = skips - OLD.skips WHERE id = OLD.nightly_run_id; 
       END IF; 
     END IF; 
     RETURN NEW; 
     END; 
$$ 
    LANGUAGE plpgsql; 

Что я вижу в файл журнала postgres:

ERROR: deadlock detected 
DETAIL: Process 20266 waits for ShareLock on transaction 7520; blocked by process 20263. 
     Process 20263 waits for ExclusiveLock on tuple (1,70) of relation 18469 of database 18354; blocked by process 20266. 
     Process 20266: insert into regression_runs (username, nightly_run_id, nightly_run_pid) values ('tbeadle', 135, 20262); 
     Process 20263: insert into regression_runs (username, nightly_run_id, nightly_run_pid) values ('tbeadle', 135, 20260); 
HINT: See server log for query details. 
CONTEXT: SQL statement "UPDATE nightly_runs SET passes = passes + NEW.passes, failures = failures + NEW.failures, errors = errors + NEW.errors, skips = skips + NEW.skips WHERE id = NEW.nightly_run_id" 
     PL/pgSQL function regression_run_update() line 16 at SQL statement 
STATEMENT: insert into regression_runs (username, nightly_run_id, nightly_run_pid) values ('tbeadle', 135, 20262); 

я могу воспроизвести проблему с помощью этого сценария:

#!/usr/bin/env python 

import os 
import multiprocessing 
import psycopg2 

class Foo(object): 
    def child(self): 
     pid = os.getpid() 
     conn = psycopg2.connect(
      'dbname=regression host=localhost user=regression') 
     cur = conn.cursor() 
     for i in xrange(100): 
      cur.execute(
       "insert into regression_runs " 
       "(username, nightly_run_id, nightly_run_pid) " 
       "values " 
       "('tbeadle', %s, %s);", (self.nid, pid)) 
      conn.commit() 
     return 

    def start(self): 
     conn = psycopg2.connect(
      'dbname=regression host=localhost user=regression') 
     cur = conn.cursor() 
     cur.execute('insert into nightly_runs default values returning id;') 
     row = cur.fetchone() 
     conn.commit() 
     self.nid = row[0] 
     procs = [] 
     for child in xrange(5): 
      procs.append(multiprocessing.Process(target=self.child)) 
     for proc in procs: 
      proc.start() 
     for proc in procs: 
      proc.join() 

Foo().start() 

Я не могу понять, почему тупиковый происходит или то, что я могу с этим поделать. Пожалуйста помоги!

+1

IMHO, обновление полей внутри триггера - плохая идея. Поскольку триггерные пушки часто пытаются записать в один ряд, он становится тупиковым. Мб нуждается в изменениях схемы. Для сложных случаев я создаю таблицу очереди буферов и отправляю ее по процедуре хранения. Конечно, используется внешний инструмент для управления очередью. – corvinusz

+0

@corvinusz: ерунда. Триггеры - идеальный инструмент для того, что делает OP. Он просто не знает о нескольких ошибках. –

ответ

2

Чаще всего возникают взаимоблокировки, поскольку обновления, связанные с OLD и NEW, не выполняются в последовательном порядке. Показательный пример:

IF TG_OP = 'UPDATE' THEN 
    IF (NEW.nightly_run_id IS NOT NULL) AND (NEW.nightly_run_id = OLD.nightly_run_id) THEN 
    -- stuff that seems fine 
    ELSE 
    IF NEW.nightly_run_id IS NOT NULL THEN 
     UPDATE nightly_runs ... WHERE id = NEW.nightly_run_id; -- lock 
    END IF; 
    IF OLD.nightly_run_id IS NOT NULL THEN 
     UPDATE nightly_runs ... WHERE id = OLD.nightly_run_id; -- lock 
    END IF; 

Представьте себе две сделки:

  • T1 получает блокировку на new.nightly_run_id = 1 и ожидает блокировку на old.nightly_run_id = 2
  • T2 получает блокировку на новый .nightly_run_id = 2 и ожидает блокировку на old.nightly_run_id = 1

... Deadlock

Принудительный порядок, чтобы избежать является такая ситуация:

IF OLD.nightly_run_id = NEW.nightly_run_id THEN 
    -- stuff that seems fine 
ELSIF OLD.nightly_run_id < NEW.nightly_run_id THEN 
    UPDATE nightly_runs ... WHERE id = OLD.nightly_run_id; 
    UPDATE nightly_runs ... WHERE id = NEW.nightly_run_id; 
ELSEIF NEW.nightly_run_id < OLD.nightly_run_id THEN 
    UPDATE nightly_runs ... WHERE id = NEW.nightly_run_id; 
    UPDATE nightly_runs ... WHERE id = OLD.nightly_run_id; 
ELSEIF OLD.nightly_run_id IS NOT NULL THEN 
    UPDATE nightly_runs ... WHERE id = OLD.nightly_run_id; 
ELSEIF NEW.nightly_run_id IS NOT NULL THEN 
    UPDATE nightly_runs ... WHERE id = NEW.nightly_run_id; 
END IF; 

Такое же изменение должно произойти для других триггеров, где это применимо. Бар других патологий в вашем коде, тупики должны уйти.

Смежные вопросы