2015-09-03 2 views
2

я испытываю состояние гонки в ActiveRecord с PostgreSQL, где я читаю значение, то увеличивая его и вставить новую запись:PostgreSQL и ActiveRecord подвыборка для гонки условия

num = Foo.where(bar_id: 42).maximum(:number) 
Foo.create!({ 
    bar_id: 42, 
    number: num + 1 
}) 

в масштабе, несколько потоков будет одновременно прочитайте, затем введите то же значение number. Обертка этого в транзакции не фиксирует условие гонки, потому что SELECT не блокирует таблицу. Я не могу использовать автоматическое приращение, потому что number не уникален, он уникален только при заданном bar_id. Я вижу 3 возможных исправлений: (? Блокировку на уровне строк)

  • Явное Используйте Postgres блокировки
  • Используйте уникальное ограничение и повторить на сбой
  • Override сохранить использовать подзапрос (Тьфу!) , IE

    INSERT INTO foo (bar_id, number) VALUES (42, (SELECT MAX(number) + 1 FROM foo WHERE bar_id = 42));

Все эти решения, кажется, что я бы реализовав большую часть из ActiveRecord::Base#save! Есть более простой способ?

UPDATE: Я думал, что я нашел ответ с Foo.lock(true).where(bar_id: 42).maximum(:number) но использует SELECT FOR UDPATE, который не допускается по совокупности запросов

UPDATE 2: Я просто информированных нашим DBA, что даже если бы мы могли делать INSERT INTO foo (bar_id, number) VALUES (42, (SELECT MAX(number) + 1 FROM foo WHERE bar_id = 42));, что ничего не исправить, так как SELECT, работает в другом замке, чем INSERT

+0

Если бы он был рассчитан «на лету», он со временем изменился бы, а также подвергся одному и тому же состоянию гонки –

ответ

2

варианты:

  • Run в SERIALIZABLE изоляции. Взаимозависимые транзакции будут прерваны при фиксации как имеющие отказ от сериализации. Вы получите много спама ошибок, и вы будете делать много попыток, но он будет работать надежно.

  • Определите ограничение UNIQUE и повторите попытку, как вы отметили. Те же проблемы, что и выше.

  • Если есть родительский объект, вы можете SELECT ... FOR UPDATE родительский объект перед выполнением запроса max. В этом случае вы получите SELECT 1 FROM bar WHERE bar_id = $1 FOR UPDATE. Вы используете bar как замок для всех foo с этим bar_id. Затем вы можете знать, что это безопасно для продолжения, если каждый запрос, который выполняет ваш счетчик, делает это надежно. Это может работать очень хорошо.

    Это по-прежнему представляет собой совокупный запрос для каждого вызова, который (для каждой опции) не нужен, но, по крайней мере, он не спамает журнал ошибок, как вышеупомянутые параметры.

  • Использовать счетчик. Это то, что я сделал бы.Либо в bar, или в боковом столе, как bar_foo_counter, приобретают идентификатор строки, используя

    UPDATE bar_foo_counter SET counter = counter + 1 
    WHERE bar_id = $1 RETURNING counter 
    

    или менее эффективный вариант, если структура не может справиться с RETURNING:

    SELECT counter FROM bar_foo_counter 
    WHERE bar_id = $1 FOR UPDATE; 
    
    UPDATE bar_foo_counter SET counter = $1; 
    

    Затем в той же транзакции, используйте сгенерированную строку счетчика для number. Когда вы совершаете фиксацию, строка таблицы счетчиков для этого bar_id разблокируется для следующего запроса. Если вы откажетесь, изменение будет отменено.

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

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