Работа с параллелизмом PostgreSQL с помощью Rails find_or_create

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

game = Game.find_or_create_by(
    status: Game::STATUS[:waiting],
    category_id: params[:category_id],
    private: 0
) do |g|
  is_new = true
  g.user = current_user
end

Я не могу четко понять, в чем дело, но, вероятно, дело в разных процессах Unicorn, которые используют разные соединения с базой данных, поэтому транзакции могут выполняться параллельно.

Если это так, мне нужен правильный способ избежать этого, возможно, мне следует использовать транзакции Rails или блокировки Postgres, но мне действительно нужен пример использования.

Спасибо.


person Alexander Zinchuk    schedule 22.05.2014    source источник
comment
Я полагаю, что ваша проблема связана с текущим_пользователем, но сначала измените find_or_create_by на create_by, поскольку is_new имеет значение true.   -  person Hitham S. AlQadheeb    schedule 22.05.2014
comment
Добавить и индексировать базу данных, чтобы избежать дублирования записей?   -  person manu29.d    schedule 22.05.2014
comment
manu29.d, на самом деле я не могу использовать УНИКАЛЬНЫЙ индекс для [status, category_id, private]: потому что мне нужна уникальная запись только тогда, когда ее статус равен 0 (ОЖИДАНИЕ). Для других статусов у меня могут быть записи, дублирующие эти столбцы.   -  person Alexander Zinchuk    schedule 22.05.2014
comment
Hitham S. AlQadheeb, я не понял, что вы имеете в виду. В чем проблема с current_user и что такое create_by? is_new используется в следующем коде, чтобы знать, что запись только что создана.   -  person Alexander Zinchuk    schedule 22.05.2014


Ответы (1)


Это может происходить при высоком уровне параллелизма.

Согласно документам rails, будут выполняться следующие запросы:

SELECT * FROM games WHERE status = 'waiting' AND ... LIMIT 1;
INSERT INTO games (status, ...) VALUES ('waiting', ...);

Второй запускается только тогда, когда первый не вернул строку.

Возможно, если два (или более) соединения запустят первый запрос в течение нескольких микросекунд, несколько процессов создадут несколько записей. Чтобы предотвратить это, вы можете использовать ACCESS EXCLUSIVE блокировку для этой таблицы. или используйте собственную рекомендательную блокировку.

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

ИЗМЕНИТЬ:

ACCESS EXCLUSIVE блокировку можно получить с помощью LOCKкоманды.

Консультативную блокировку можно получить с помощью pg_advisory_lock(id)функции.

Оба требуют запуска произвольных команд SQL.


Другой способ - использовать пользовательские запросы с:

  1. вставить, только если он не существует (и вернуться со всеми полями)
  2. выбирать, только если не вставлен

Что-то вроде:

INSERT INTO games (status, category_id, private)
     SELECT 'waiting', 2, 0
      WHERE NOT EXISTS (
             SELECT 1
               FROM games
              WHERE status = 'waiting'
                AND category_id = 2
                AND private = 0
       )
  RETURNING *;

-- only select, when this not inserted anything

SELECT *
  FROM games
 WHERE status = 'waiting'
   AND category_id = 2
   AND private = 0
person pozs    schedule 22.05.2014
comment
Спасибо. Не могли бы вы привести небольшой пример того, как реализовать эти типы блокировок в моем коде rails? Разве транзакции rails не будут полезны в моем случае? - person Alexander Zinchuk; 22.05.2014