Означает ОБНОВИТЬ, ГДЕ Значение IN в подзапросе с GROUP BY, поэтому нет проблем с условиями гонки? - PullRequest
1 голос
/ 28 октября 2011

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

Упрощенный сценарий - у меня есть процесс, который должен захватывать любые записи, где есть несколько записей определенного типа. Я хотел бы сделать систему / процесс (ы) потоком / мультипроцессором / реентрантом / умным словом дня безопасным; если тот же процесс запускается и вводит условие гонки, пытающееся захватить ряды интересов, я хотел бы, чтобы были явные победители / проигравшие: успех для одного, ошибка для другого; на самом деле, я предпочел бы плавный, тихий, изящный «провал» для второго в том, что он просто НЕ ВИДЕТ тех, которые были бы захвачены первым экземпляром.

Таким образом, моя дилемма.

У меня такой запрос:

   UPDATE my_table
      SET processing_by = our_id_info -- unique to this worker
    WHERE trans_nbr IN (
                          SELECT trans_nbr
                            FROM my_table
                        GROUP BY trans_nbr
                          HAVING COUNT(trans_nbr) > 1
                           LIMIT our_limit_to_have_single_process_grab
                       )
RETURNING row_id

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

Я думал о добавлении « FOR UPDATE ON my_table» в конце подзапроса, но это не сработает; не может иметь это И "GROUP BY" (что необходимо для определения СЧЕТА trans_nbr's). (Так как это приведет к блокировке любых-тоже-rans в ожидании нашего обновления, это было бы предпочтительным решением, так как тогда и то, и другое избежало бы ошибки, вызванной условием гонки [два процесса получают один и тот же ряд {s}], и позволяло те другие процессы, которые блаженно не подозревают и просто получают строки, которые больше не включают те, которые захватил первый процесс. Увы.)

Я думал о блокировке таблицы, но (по крайней мере, в Postgres) блокировки таблицы снимаются только после COMMIT; в целях тестирования я не хочу COMMIT, поэтому во время тестирования (да, перед живым тестированием на действующей базе данных ПОСЛЕ тестирования на тестовой базе данных) не пойдет этот путь. (Кроме того, даже в режиме реального времени это может привести к недопустимому снижению производительности при достаточном количестве пользователей / процессов.)

Я думал о том, чтобы сделать обновление зависимым от того, какое значение processing_by было для нашего подзапроса, но, опять же, это не сработает: если в подзапросе, это нарушит GROUP BY / HAVING условие (как сейчас будут подсчитываться подгруппы trans_nbr / processing_by, а это не то, что мне нужно).

Я ожидаю, что какой-то пронзительный пункт в «Правильном направлении» будет насмехаться над мной, задавая такой очевидный вопрос, но для меня это было неочевидно (очевидно; о), и, уверяю вас, я изучал это буквально часами. *

Большое спасибо за любые подсказки, не говоря уже о решениях!


ОБНОВЛЕНИЕ: Большое спасибо Крис Траверс !

Эта старая строка о " Форрест для деревьев " приходит на ум! :>

Вот модифицированная версия запроса, учитывающая это предложение и добавляющая еще одну «двойную проверку». Это должно быть ЕДИНЫМ.

   UPDATE my_table
      SET processing_by = our_id_info -- unique to this worker
    WHERE trans_nbr IN (
                SELECT trans_nbr
                  FROM my_table
                 WHERE trans_nbr IN (
                           SELECT trans_nbr
                             FROM my_table
                         GROUP BY trans_nbr
                           HAVING COUNT(*) > 1 -- Thanks for the suggestion, Flimzy
                            LIMIT our_limit_to_have_single_process_grab
                                    )
                   AND processing_by IS NULL
                       /* Or some other logic that says "not currently being
                          processed".  This way, we ALSO verify we're not
                          grabbing one that might have been UPDATEd/grabbed
                          during our sub-SELECT, while it was being
                          blocked/waiting.

                          This COULD go in our UPDATE/top-level, but unnecessary
                          rows could be locked by this lower-level in that case.
                       */
            FOR UPDATE /* Will block/wait for rows this finds to be unlocked by
                          any prior transaction that had a lock on them.

                          NOTE: Which _could_ allow the prior trans to change
                                our desired rows in the mean time, thus the
                                secondary WHERE clause.
                       */
                       )
RETURNING row_id

Я бы хотел, чтобы у Postgres была SKIP LOCKED -подобная функция. Особенно для очередей с атомарными строками, которые нужно обрабатывать, не блокируя другую обработку. Но, увы. Может быть, когда-нибудь ...? Или "скоро"? : -)

На данный момент можно добавить NOWAIT , чтобы НЕ блокироваться никакими другими транзакциями, однако имейте в виду, что это просто возвращает обратно с ошибкой - вам придется продолжать попытки запроса пока это не удастся (или сдаться). Без NOWAIT запрос блокируется до тех пор, пока другие транзакции не снимают свои блокировки или не истечет время ожидания запроса.


ОБНОВЛЕНИЕ 2: ТАК, после повторного перечитывания этого и обдумывания, снова момент "Форрест для деревьев". Я могу просто сделать так:

   UPDATE my_table
      SET processing_by = our_id_info -- unique to this worker
    WHERE trans_nbr IN (
                        -- This query MAY pull ones we don't want to mess with (already "grabbed")
                          SELECT trans_nbr
                            FROM my_table
                        GROUP BY trans_nbr
                          HAVING COUNT(*) > 1
                           LIMIT our_limit_to_have_single_process_grab
                             AND processing_by IS NULL -- only "ungrabbed" ones (at this point)
                       )
      AND processing_by IS NULL -- But THIS will drop out any "bogus" ones that changed between subquery and here
RETURNING row_id

Выполните транзакцию, чтобы освободить НАШИ блокировки, и ваш дядя Боба.

SKIP LOCKED все равно будет супер-круто.

A CAVEATE: Если один из них былчтобы рабочие, тянущие за ограниченное (например, LIMIT 1) количество строк и / или элементов, должны быть захвачены в определенном порядке (например, FIFO, ORDER BY и / или с помощью функции, такой как Min (id)), могут быть случаиголодные работники: рабочий ждет и ждет, и когда ряд (ы), которого они ждали, разблокируется, оказывается, что ни один из них не отвечает его окончательным критериям.Есть несколько способов попытаться обойти это, например, заставить рабочих прыгать через OFFSET, но большинство из них либо сложные, либо медленные.(Обычно оба. БОНУС!)

MY functionailty ожидает, что возвращено несколько строк, или ни одна из них не является A-OK - на данный момент ничего не делать;поспать немного и перепроверить , так что это не проблема для меня.Это может быть для вас.Если это так, вы можете рассмотреть ...

НЕБЛОКИРУЮЩУЮ ВЕРСИЮ: Я нашел замечательную статью , работающую с этой самой проблемой, оказывается,и это познакомило меня с Консультативными замками Pg .( Этот тоже был довольно информативным.)

Итак, неблокирующее решение моей собственной проблемы должно выглядеть так:

   UPDATE my_table
      SET processing_by = our_id_info -- unique to this worker
    WHERE trans_nbr IN (
            -- This query MAY pull ones we don't want to mess with (already "grabbed")
              SELECT trans_nbr
                FROM my_table AS inner_my_table_1
            GROUP BY trans_nbr
              HAVING Count(*) > 1
                 AND Count(*) in ( -- For MY query, since I'm grouping-by, I want "all or none" of trans_nbr rows
                       SELECT Count(*)
                         FROM my_table AS inner_my_table_2
                        WHERE inner_my_table_2.trans_nbr = inner_my_table_1.trans_nbr
                          AND pg_try_advisory_xact_lock(id) -- INT that will uniquely ID this row
                                 )
/* Note also that this will still lock all non-locked rows with this
   trans_nbr, even though we won't use them unless we can grab ALL of the
   rows with same trans_nbr... the rest of our query should be made
   quick-enough to accept this reality and not tie up the server unduly.

   See linked info for more-simple queries not doing group-by's.
*/
               LIMIT our_limit_to_have_single_process_grab
                 AND processing_by IS NULL -- only "ungrabbed" ones (at this point)
                       )
      AND processing_by IS NULL -- But THIS will drop out any "bogus" ones that changed between subquery and here
RETURNING row_id

ПРИМЕЧАНИЯ:

  • Это зависит от приложений, чтобы делать / уважать консультативные блокировки, так что это не панча, но и не плацебо.Опять же, SKIP LOCKED будет очень удобен из-за этого.
  • pg_try_advisory_lock , поскольку v 8.2, не разблокируется автоматически, (таким образом) может ( MUST ) бытьявно разблокировано
  • pg_try_advisory_xact_lock , поскольку v 9.1, автоматическая разблокировка в конце транзакции, НЕ МОЖЕТ быть разблокировано явно
  • Я НЕ ИСПЫТАЛ ЭТО ДА! Я буду редактировать / обновлять, когда у меня будет ...

1 Ответ

1 голос
/ 23 марта 2013

Как насчет дополнительного подзапроса для блокировки?

   UPDATE my_table
      SET processing_by = our_id_info -- unique to this instance
    WHERE trans_nbr IN (
                    SELECT trans_nbr
                      FROM my_table
                     WHERE trans_nbr IN (
                                 SELECT trans_nbr
                                   FROM my_table
                               GROUP BY trans_nbr
                                 HAVING COUNT(trans_nbr) > 1
                                  LIMIT our_limit_to_have_single_process_grab
                                 )
                        FOR UPDATE
                       )
RETURNING row_id
...