Без одновременного доступа к записи
Материализация выбора в 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()
. Руководство:
После получения на уровне сеанса консультативная блокировка удерживается до тех пор, пока явно не будет снята или сеанс не завершится.
Связанный: