Средство UPDATE WHERE Значение IN В подзапросе, у которого есть GROUP BY, поэтому нет проблем с Race-Condition (-ами)?

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

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

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

Запрос у меня такой:

   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

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

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

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

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

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

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


UPDATE: Thanks SO MUCH Chris Travers!

That ol' line about "Forrest for the Trees" comes to mind! :>

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

   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 . Особенно для очередей по существу атомных строк, которые необходимо обрабатывать, не блокируя другую обработку. Но увы. Может быть, когда-нибудь ...? < a href = "http://www.postgresql.org/message-id/[email protected]om" rel = "nofollow noreferrer"> Или "скоро"? :-)

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


UPDATE 2: SO, after re-re-re-reading this and thinking about it, again "Forrest for the Trees" moment. I can simply do like this:

   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

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

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

A CAVEATE: If one was to have workers pulling a limited (like LIMIT 1) number of rows and/or items must be grabbed in a certain order (e.g.: FIFO, either ORDER BY and/or by function like Min(id)), there can be cases of starved workers: a worker waits and waits, and when the row(s) they were waiting for unblocks, turns out none of them meet its final criteria. There are a number of ways to try to get around this, like having workers jumping around via OFFSET, but most are either complex or slow. (Usually both. BONUS!)

MY functionailty expects multiple rows returned, or none is A-OK - nothing to do for now; sleep for a bit and recheck, so this isn't a problem for me. It may be for you. If so, you'll want to consider a...

NON-BLOCKING VERSION: I found a great article working with this very problem, turns out, and it introduced me to Pg's Advisory Locks. (This one was quite informative, too.)

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

   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

ПРИМЕЧАНИЕ:

  • It's up to the applications to do/respect Advisory Locks, so this is no pancea, but nor is it a placebo. Again, SKIP LOCKED would be very handy because of this.
  • pg_try_advisory_lock, since v 8.2, does not auto-unlock, (thus) may (MUST) be explicitly unlocked
  • pg_try_advisory_xact_lock, since v 9.1, auto-unlocks at end of transaction, may NOT be explicitly unlocked
  • I HAVE NOT TESTED THIS YET! I'll edit/update when I have...
1
Незначительная точка: если trans_nbr иногда имеет значение NULL, вы, вероятно, получите лучшие результаты с помощью COUNT (*) по сравнению с COUNT (trans_nbr) , поскольку первая, я считаю, может быть лучше оптимизирована ,
добавлено автор Flimzy, источник
Вы думали о тестировании этого в двух сеансах SQL?
добавлено автор Mike Sherrill 'Cat Recall&, источник
@ MikeSherrill'Catcall ': Да, я подумал об этом. Начал работу над некоторыми тестовыми данными/запросами, но получил вызов для других задач. (Я пересматриваю это сейчас, потому что кто-то был достаточно любезен, чтобы предложить решение. :-) 1. Логически проблема, которую я упоминаю, «теоретически возможна», проблема-вид или нет. 2. Даже при тестировании подобные проблемы параллелизма трудно реплицировать. Таким образом, «на самом деле это происходит» не всегда возможно. И без подробного знания источника ... (Да, FLOSS, но этот тип кода трудно [для меня;]] пробираться через ... esp, когда я не работаю над ним 24/7
добавлено автор pythonlarry, источник
@Flimzy: Я давно давал вам реквизит в другом месте, но хотел явно выкрикнуть здесь: СПАСИБО за то, что указали это! :>
добавлено автор pythonlarry, источник

1 ответы

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

   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
1
добавлено
pgsql – PostgreSQL
pgsql – PostgreSQL
2 429 участник(ов)

Чат про PostgreSQL

DBA - русскоговорящее сообщество
DBA - русскоговорящее сообщество
1 345 участник(ов)

Общаемся и обсуждаем темы, посвященные DBA, PostgreSQL, Redis, MongoDB, MySQL, neo4j, riak и т.д. См. также: @devops_ru, @kubernetes_ru, @docker_ru, @nodejs_ru Рекомендуем сразу отключить уведомления, чтобы пребывание здесь было полезным и комфортным.

SqlCom.ru - Стиль жизни SQL
SqlCom.ru - Стиль жизни SQL
908 участник(ов)

Правила чата - https://t.me/sqlcom/88269 @sqlcom - основной канал (только MS SQL) @sql_ninja - второй канал (SQL вопросы начального уровня и свободное общение) @Gopnegbot - Викторина по SQL Server (наберите в привате /quiz). Предложения в @sql_ninja

SQL_Ninja
SQL_Ninja
340 участник(ов)

Правила чата - https://t.me/sqlcom/88269 @sqlcom - основной канал (только SQL) @sql_ninja - второй канал (SQL вопросы начального уровня и свободное общение) @Gopnegbot - Викторина по SQL Server (наберите в привате /quiz)