Подтвердить что ты не робот

Вставить строку, если она не существует, приводит к состоянию гонки?

Я использую простой RSS-ридер на основе Интернета, используя python (не очень релевантный) и Postgresql (если это необходимо 9.2). Схема базы данных выглядит следующим образом (на основе формата RSS):

CREATE TABLE feed_channel
(
    id SERIAL PRIMARY KEY,
    name TEXT,
    link TEXT NOT NULL,
    title TEXT
);
CREATE TABLE feed_content
(
    id SERIAL PRIMARY KEY,
    channel INTEGER REFERENCES feed_channel(id) ON DELETE CASCADE ON UPDATE CASCADE,
    guid TEXT UNIQUE NOT NULL,
    title TEXT,
    link TEXT,
    description TEXT,
    pubdate TIMESTAMP
);

Когда я создаю новый канал (а также запрос для обновленной информации о фиде), я запрашиваю канал, вставляю его данные в таблицу feed_channel, выбирает вновь вставленный идентификатор или существующий, чтобы избежать дублирования, - а затем добавляйте данные фида к таблицу feed_content. Типичным сценарием будет:

  • Запросить URL-адрес канала подачи, заголовки каналов захвата и весь текущий контент
  • Вставьте заголовки каналов в канал feed_channel, если он не существует... если он уже существует, возьмите существующий идентификатор
  • Для каждого элемента подачи вставьте в таблицу feed_content ссылку на сохраненный идентификатор канала

Это стандартная "вставка, если она еще не существует, но возвращает соответствующий идентификатор". Чтобы решить эту проблему, я выполнил следующую хранимую процедуру:

CREATE OR REPLACE FUNCTION channel_insert(
  p_link feed_channel.link%TYPE,
  p_title feed_channel.title%TYPE
) RETURNS feed_channel.id%TYPE AS $$
  DECLARE
    v_id feed_channel.id%TYPE;
  BEGIN
    SELECT id
    INTO v_id
    FROM feed_channel
    WHERE link=p_link AND title=p_title
    LIMIT 1;

    IF v_id IS NULL THEN
      INSERT INTO feed_channel(name,link,title)
      VALUES (DEFAULT,p_link,p_title)
      RETURNING id INTO v_id;
    END IF;

    RETURN v_id;

  END;
$$ LANGUAGE plpgsql;

Затем это называется "select channel_insert (ссылка, название)"; из моего приложения вставить, если оно еще не существует, и затем вернуть идентификатор соответствующей строки независимо от того, была ли она вставлена ​​или просто найдена (шаг 2 в списке выше).

Это отлично работает!

Однако я недавно начал задаваться вопросом, что произойдет, если эта процедура будет выполнена дважды в то же время с теми же аргументами. Предположим следующее:

  • Пользователь 1 пытается добавить новый канал и тем самым выполнить channel_insert
  • Через несколько минут пользователь 2 пытается добавить тот же канал, а также выполнить channel_insert
  • Пользователь 1 проверяет наличие существующих строк, но до завершения вставки проверка User 2 завершается и говорит, что нет существующих строк.

Будет ли это потенциальное состояние гонки в PostgreSQL? Каков наилучший способ решить эту проблему, чтобы избежать таких сценариев? Возможно ли сделать всю хранимую процедуру атомарно, т.е. Что ее можно выполнить только один раз в одно и то же время?

Один из вариантов, который я пытался сделать, - это сделать поля Unique, а затем попытаться вставить сначала, а если исключение, выберите существующее вместо этого... Это сработало, однако, поле SERIAL увеличивалось бы для каждой попытки, оставляя много пробелов в последовательности. Я не знаю, будет ли это проблемой в долгосрочной перспективе (возможно, нет), но вроде бы раздражает. Возможно, это предпочтительное решение?

Спасибо за любую обратную связь. Этот уровень магии PostgreSQL превосходит меня, поэтому любая обратная связь будет оценена.

4b9b3361

Ответ 1

Здесь существует неизбежная "гонка", так как две сессии не могут "видеть" друг друга неустановленными строками. В конфликте сеанс мог только откат (возможно, в точку сохранения) и повторить попытку. Это обычно означает: ссылаясь на другую только что вставленную строку, вместо создания частного дубликата.

Здесь есть проблема моделирования данных: у feed_channel есть много ключей-кандидатов, и каскадное правило из feed_content могло бы осилить много строк feed_content (я полагаю, что content- > channel равен 1:: M; больше чем одна строка содержимого может ссылаться на один и тот же канал)

Наконец, таблица feed_channel не менее нуждается в естественном ключе {link, title}. Вот где вставка/не существует. (и вся цель этой функции)

Я немного очистил функцию. Конструкция IF не нужна, делая INSERT WHERE NOT EXISTS first работает так же хорошо, и, возможно, даже лучше.

DROP SCHEMA tmp CASCADE;
CREATE SCHEMA tmp ;
SET search_path=tmp;

CREATE TABLE feed_channel
    ( id SERIAL PRIMARY KEY
    , name TEXT
    , link TEXT NOT NULL
    , title TEXT NOT NULL -- part of PK :: must be not nullable
    , CONSTRAINT feed_channel_nat UNIQUE (link,title) -- the natural key
);

CREATE TABLE feed_content
    ( id SERIAL PRIMARY KEY
    , channel INTEGER REFERENCES feed_channel(id) ON DELETE CASCADE ON UPDATE CASCADE
    , guid TEXT UNIQUE NOT NULL -- yet another primary key
    , title TEXT --
    , link TEXT  -- title && link appear to be yet another candidate key
    , description TEXT
    , pubdate TIMESTAMP
    );

-- NOTE: omitted original function channel_insert() for brevity
CREATE OR REPLACE FUNCTION channel_insert_wp(
  p_link feed_channel.link%TYPE,
  p_title feed_channel.title%TYPE
) RETURNS feed_channel.id%TYPE AS $body$
   DECLARE
    v_id feed_channel.id%TYPE;
  BEGIN
      INSERT INTO feed_channel(link,title)
      SELECT p_link,p_title
      WHERE NOT EXISTS ( SELECT *
        FROM feed_channel nx
        WHERE nx.link= p_link
        AND nx.title= p_title
        )
        ;
    SELECT id INTO v_id
    FROM feed_channel ex
    WHERE ex.link= p_link
    AND ex.title= p_title
        ;

    RETURN v_id;

  END;
$body$ LANGUAGE plpgsql;

SELECT channel_insert('Bogus_link', 'Bogus_title');
SELECT channel_insert_wp('Bogus_link2', 'Bogus_title2');

SELECT * FROM feed_channel;

Результаты:

DROP SCHEMA
CREATE SCHEMA
SET
NOTICE:  CREATE TABLE will create implicit sequence "feed_channel_id_seq" for serial column "feed_channel.id"
NOTICE:  CREATE TABLE / PRIMARY KEY will create implicit index "feed_channel_pkey" for table "feed_channel"
NOTICE:  CREATE TABLE / UNIQUE will create implicit index "feed_channel_nat" for table "feed_channel"
CREATE TABLE
NOTICE:  CREATE TABLE will create implicit sequence "feed_content_id_seq" for serial column "feed_content.id"
NOTICE:  CREATE TABLE / PRIMARY KEY will create implicit index "feed_content_pkey" for table "feed_content"
NOTICE:  CREATE TABLE / UNIQUE will create implicit index "feed_content_guid_key" for table "feed_content"
CREATE TABLE
NOTICE:  type reference feed_channel.link%TYPE converted to text
NOTICE:  type reference feed_channel.title%TYPE converted to text
NOTICE:  type reference feed_channel.id%TYPE converted to integer
CREATE FUNCTION
NOTICE:  type reference feed_channel.link%TYPE converted to text
NOTICE:  type reference feed_channel.title%TYPE converted to text
NOTICE:  type reference feed_channel.id%TYPE converted to integer
CREATE FUNCTION
 channel_insert 
----------------
              1
(1 row)

 channel_insert_wp 
-------------------
                 2
(1 row)

 id | name |    link     |    title     
----+------+-------------+--------------
  1 |      | Bogus_link  | Bogus_title
  2 |      | Bogus_link2 | Bogus_title2
(2 rows)

Ответ 2

Будет ли это потенциальное состояние гонки в PostgreSQL?

Да, и на самом деле это будет в любом движке базы данных.

Каков наилучший способ решить эту проблему, чтобы избежать таких сценариев?

Это загруженный вопрос и потребует глубокого знания использования базы данных несколькими пользователями. Тем не менее, я собираюсь дать вам несколько вариантов. Короче говоря, единственный вариант, который у вас есть, - это LOCK таблица во время этого процесса, но , как вы заблокируете эту таблицу, будет зависеть от , как база данных используется в течение дня.

Начнем с базового LOCK:

LOCK TABLE feed_channel

Это заблокирует таблицу, используя опцию блокировки ACCESS EXCLUSIVE.

Конфликты с блокировками всех режимов (ACCESS SHARE, ROW SHARE, ROW EXCLUSIVE, SHARE UPDATE EXCLUSIVE, SHARE, SHARE ROW EXCLUSIVE, EXCLUSIVE и ACCESS EXCLUSIVE). Этот режим гарантирует, что держатель является единственной транзакцией, обращающейся к таблице в любом случае.

Теперь это самый ограничительный доступ к блокировке и, безусловно, разрешит состояние гонки, но может быть не совсем то, что вы хотите. Это то, что вам придется решать. Итак, хотя это очистить, вам нужно будет LOCK таблицы, это не очистить.

Что вы решили решить?

  • Как вы хотите LOCK таблицу? Изучите параметры блокировки на этой ссылке, чтобы сделать ваше определение.
  • Где вы хотите LOCK таблицу? Или, другими словами, вы хотите LOCK в верхней функции (которую я думаю, что вы делаете на основе возможного состояния гонки), или просто хотите LOCK прямо перед INSERT?

Можно ли сделать всю хранимую процедуру атомарно, т.е. что она может выполняться только один раз в одно и то же время?

Нет, код может быть выполнен любым, кто подключен к базе данных.


Я надеюсь, что это помогло вам.

Ответ 3

Ваша основная проблема заключается в том, что serial не делает хороший первичный ключ для таблицы feed_channel. Первичный ключ должен быть (link, title) или просто (link), если title может быть null. Тогда любая попытка вставить существующий канал вызовет ошибку первичного ключа.

BTW v_id будет null всякий раз, когда title равно null:

WHERE link=p_link AND title=p_title