Без одновременного доступа к записи
Материализация выбора в CTE и присоединение к нему в FROMпредложении UPDATE.
WITH cte AS (
SELECT server_ip -- pk column or any (set of) unique column(s)
FROM server_info
WHERE status = 'standby'
LIMIT 1 -- arbitrary pick (cheapest)
)
UPDATE server_info s
SET status = 'active'
FROM cte
WHERE s.server_ip = cte.server_ip
RETURNING server_ip;
Изначально у меня был простой подзапрос, но он может обойтись без LIMITопределенных планов запросов, как указал Фейке :
Планировщик может выбрать создание плана, который выполняет вложенный цикл над LIMITingподзапросом, что приводит к более UPDATEsчем LIMIT, например:
Update on buganalysis [...] rows=5
-> Nested Loop
-> Seq Scan on buganalysis
-> Subquery Scan on sub [...] loops=11
-> Limit [...] rows=2
-> LockRows
-> Sort
-> Seq Scan on buganalysis
Воспроизведение тестового примера
Чтобы исправить вышесказанное, нужно было заключить LIMITподзапрос в собственный CTE, поскольку CTE материализован, и он не будет возвращать разные результаты на разных итерациях вложенного цикла.
Или используйте слабо коррелированный подзапрос для простого случая сLIMIT 1. Проще, быстрее:
UPDATE server_info
SET status = 'active'
WHERE server_ip = (
SELECT server_ip
FROM server_info
WHERE status = 'standby'
LIMIT 1
)
RETURNING server_ip;
С одновременным доступом к записи
Предполагая уровень изоляции по умолчаниюREAD COMMITTED для всего этого. Более строгие уровни изоляции ( REPEATABLE READи SERIALIZABLE) могут по-прежнему приводить к ошибкам сериализации. Видеть:
При одновременной загрузке записи добавьте FOR UPDATE SKIP LOCKEDблокировку строки, чтобы избежать условий гонки. SKIP LOCKEDбыл добавлен в Postgres 9.5 , более старые версии см. ниже. Руководство:
При этом SKIP LOCKEDлюбые выбранные строки, которые не могут быть немедленно заблокированы, пропускаются. Пропуск заблокированных строк обеспечивает несогласованное представление данных, поэтому это не подходит для работы общего назначения, но может использоваться для предотвращения конфликта блокировок, когда несколько потребителей получают доступ к таблице, подобной очереди.
UPDATE server_info
SET status = 'active'
WHERE server_ip = (
SELECT server_ip
FROM server_info
WHERE status = 'standby'
LIMIT 1
FOR UPDATE SKIP LOCKED
)
RETURNING server_ip;
Если не осталось соответствующей незаблокированной строки, в этом запросе ничего не происходит (строка не обновляется), и вы получите пустой результат. Для некритических операций это означает, что вы сделали.
Тем не менее, параллельные транзакции могут иметь заблокированные строки, но затем не завершить обновление ( ROLLBACKили по другим причинам). Чтобы быть уверенным, запустите финальную проверку:
SELECT NOT EXISTS (
SELECT 1
FROM server_info
WHERE status = 'standby'
);
SELECTтакже видит заблокированные строки. Если не возвращается true, одна или несколько строк все еще обрабатываются, и транзакции могут быть откатаны. (Или тем временем были добавлены новые строки.) Подождите немного, затем выполните цикл двух шагов: ( UPDATEпока вы не вернете строку; SELECT...), пока не получите true.
Связанный:
Без SKIP LOCKEDв PostgreSQL 9.4 или старше
UPDATE server_info
SET status = 'active'
WHERE server_ip = (
SELECT server_ip
FROM server_info
WHERE status = 'standby'
LIMIT 1
FOR UPDATE
)
RETURNING server_ip;
Параллельные транзакции, пытающиеся заблокировать одну и ту же строку, блокируются до тех пор, пока первая из них не снимет свою блокировку.
Если первый откатывался, следующая транзакция захватывает блокировку и продолжается в обычном режиме; другие в очереди продолжают ждать.
Если первый зафиксирован, WHEREусловие переоценивается, и если оно TRUEбольше statusне изменяется ( изменилось), CTE (несколько неожиданно) не возвращает строки. Ничего не произошло. Это желаемое поведение, когда все транзакции хотят обновить одну и ту же строку .
Но не тогда , когда каждая транзакция хочет обновить на следующую строку . А поскольку мы просто хотим обновить произвольную (или случайную ) строку , ждать вообще не имеет смысла.
Мы можем разблокировать ситуацию с помощью консультативных блокировок :
UPDATE server_info
SET status = 'active'
WHERE server_ip = (
SELECT server_ip
FROM server_info
WHERE status = 'standby'
AND pg_try_advisory_xact_lock(id)
LIMIT 1
FOR UPDATE
)
RETURNING server_ip;
Таким образом, следующая не заблокированная строка будет обновлена. Каждая транзакция получает новую строку для работы. Мне помогала чешская Postgres Wiki для этого трюка.
idбыть любым уникальным bigintстолбцом (или любым типом с неявным приведением типа int4или int2).
Если консультативные блокировки используются для нескольких таблиц в вашей базе данных одновременно, устраните неоднозначность с pg_try_advisory_xact_lock(tableoid::int, id)- idбыть уникальным integerздесь.
Поскольку tableoidэто bigintколичество, оно теоретически может переполниться integer. Если вы достаточно параноик, используйте (tableoid::bigint % 2147483648)::intвместо этого - оставив теоретическое «столкновение хешей» для действительно параноиков ...
Кроме того, Postgres имеет право тестировать WHEREусловия в любом порядке. Он может тестировать pg_try_advisory_xact_lock()и получать блокировку раньше status = 'standby' , что может привести к дополнительным консультативным блокировкам на несвязанных строках, где status = 'standby'это не так. Связанный вопрос по SO:
Как правило, вы можете просто игнорировать это. Чтобы гарантировать блокировку только подходящих строк, вы можете вложить предикаты в CTE, как описано выше, или подзапрос с OFFSET 0хаком (предотвращает встраивание) . Пример:
Или (дешевле для последовательных сканирований) вложите условия в CASEутверждение вроде:
WHERE CASE WHEN status = 'standby' THEN pg_try_advisory_xact_lock(id) END
ОднакоCASE трюк будет также держать Postgres использовать индекс на status. Если такой индекс доступен, для начала вам не нужно дополнительное вложение: в проверке индекса будут блокироваться только подходящие строки.
Поскольку вы не можете быть уверены, что индекс используется в каждом вызове, вы можете просто:
WHERE status = 'standby'
AND CASE WHEN status = 'standby' THEN pg_try_advisory_xact_lock(id) END
CASEЛогически лишний, но это сервера обсуждаемая цели.
Если команда является частью длинной транзакции, рассмотрите блокировки на уровне сеанса, которые могут быть (и должны быть) сняты вручную. Таким образом, вы можете разблокировать, как только закончите с заблокированным рядом: pg_try_advisory_lock()иpg_advisory_unlock() . Руководство:
После получения на уровне сеанса консультативная блокировка удерживается до тех пор, пока явно не будет снята или сеанс не завершится.
Связанный: