Bez równoczesnego dostępu do zapisu
Zmaterializuj zaznaczenie w CTE i dołącz do niego w FROM
klauzuli 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;
Pierwotnie miałem tutaj proste podzapytanie, ale może to pomijaćLIMIT
niektóre plany zapytań, jak zauważył Feike :
Planista może wybrać wygenerowanie planu, który wykonuje zagnieżdżoną pętlę nad LIMITing
podzapytaniem, powodując więcej UPDATEs
niż LIMIT
np .:
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
Odtwarzanie przypadku testowego
Sposobem na naprawienie powyższego było zawinięcie LIMIT
podzapytania we własne CTE, ponieważ CTE jest zmaterializowane, nie zwraca różnych wyników dla różnych iteracji zagnieżdżonej pętli.
Lub użyj słabo skorelowanego podzapytania dla prostego przypadku zLIMIT
1
. Prostsze, szybsze:
UPDATE server_info
SET status = 'active'
WHERE server_ip = (
SELECT server_ip
FROM server_info
WHERE status = 'standby'
LIMIT 1
)
RETURNING server_ip;
Z jednoczesnym dostępem do zapisu
Zakładając domyślny poziom izolacjiREAD COMMITTED
dla tego wszystkiego. Zaostrzone poziomy izolacji ( REPEATABLE READ
i SERIALIZABLE
) mogą nadal powodować błędy serializacji. Widzieć:
Przy jednoczesnym obciążeniu zapisu dodaj, FOR UPDATE SKIP LOCKED
aby zablokować wiersz, aby uniknąć warunków wyścigu. SKIP LOCKED
został dodany w Postgres 9.5 , dla starszych wersji patrz poniżej. Instrukcja:
Za pomocą SKIP LOCKED
, wszystkie wybrane wiersze, których nie można natychmiast zablokować, są pomijane. Pomijanie zablokowanych wierszy zapewnia niespójny widok danych, więc nie jest to odpowiednie do prac ogólnego przeznaczenia, ale można go użyć, aby uniknąć rywalizacji o blokadę, gdy wielu konsumentów uzyskuje dostęp do tabeli podobnej do kolejki.
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;
Jeśli nie ma kwalifikującego się, odblokowanego wiersza, w tym zapytaniu nic się nie dzieje (żaden wiersz nie jest aktualizowany), a wynik jest pusty. W przypadku bezkrytycznych operacji oznacza to, że skończyłeś.
Jednak współbieżne transakcje mogą mieć zablokowane wiersze, ale nie kończą aktualizacji ( ROLLBACK
lub z innych powodów). Aby mieć pewność, wykonaj ostatnią kontrolę:
SELECT NOT EXISTS (
SELECT 1
FROM server_info
WHERE status = 'standby'
);
SELECT
widzi również zablokowane rzędy. Wile, które nie zwraca true
, jeden lub więcej wierszy jest nadal przetwarzanych, a transakcje można nadal wycofywać. (W międzyczasie dodano nowe wiersze.) Poczekaj chwilę, a następnie zapętl dwa kroki: ( UPDATE
dopóki nie odzyskasz żadnego wiersza; SELECT
...), aż dostaniesz true
.
Związane z:
Bez SKIP LOCKED
w PostgreSQL 9.4 lub starszym
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;
Równoczesne transakcje próbujące zablokować ten sam wiersz są blokowane, dopóki pierwszy nie zwolni blokady.
Jeśli pierwszy został wycofany, następna transakcja bierze blokadę i przebiega normalnie; inni w kolejce czekają.
Jeśli pierwszy zatwierdzony, WHERE
warunek jest ponownie oceniany, a jeśli już nie TRUE
jest ( status
zmienił się), CTE (nieco zaskakująco) nie zwraca wiersza. Nic się nie dzieje. Jest to pożądane zachowanie, gdy wszystkie transakcje chcą zaktualizować ten sam wiersz .
Ale nie przy każdej transakcji chce zaktualizować do następnego wiersza . A ponieważ chcemy po prostu zaktualizować dowolny (lub losowy ) wiersz , nie ma sensu w ogóle czekać.
Możemy odblokować sytuację za pomocą blokad doradczych :
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;
W ten sposób następny niezablokowany jeszcze wiersz zostanie zaktualizowany. Każda transakcja otrzymuje nowy wiersz do pracy. Miałem pomoc z Czech Postgres Wiki w tej sztuczce.
id
będący dowolną unikalną bigint
kolumną (lub dowolnym typem z niejawną obsadą, taką jak int4
lub int2
).
Jeśli blokady doradcze są używane jednocześnie dla wielu tabel w bazie danych, należy jednoznacznie ustalić, że pg_try_advisory_xact_lock(tableoid::int, id)
- id
jest integer
tutaj wyjątkowy .
Ponieważ tableoid
jest to bigint
ilość, teoretycznie może się przepełnić integer
. Jeśli jesteś wystarczająco paranoikiem, użyj (tableoid::bigint % 2147483648)::int
zamiast tego - pozostawiając teoretyczną „kolizję skrótu” dla prawdziwie paranoicznej ...
Ponadto Postgres może testować WHERE
warunki w dowolnej kolejności. To mogło testować pg_try_advisory_xact_lock()
i uzyskać blokadę przed status = 'standby'
, co może wiązać się z dodatkowymi zamkami doradczych na niepowiązanych wierszy, w których status = 'standby'
nie jest to prawdą. Powiązane pytanie dotyczące SO:
Zazwyczaj możesz to zignorować. Aby zagwarantować, że tylko kwalifikujące się wiersze są zablokowane, możesz zagnieździć predykat (y) w CTE jak wyżej lub podkwerendę z OFFSET 0
hackem (zapobiega wstawianiu) . Przykład:
Lub (tańsze w przypadku skanowania sekwencyjnego) zagnieżdżają warunki w CASE
instrukcji, takiej jak:
WHERE CASE WHEN status = 'standby' THEN pg_try_advisory_xact_lock(id) END
JednakCASE
Sztuką byłoby również zachować Postgresa z użyciem indeksu status
. Jeśli taki indeks jest dostępny, na początku nie potrzebujesz dodatkowego zagnieżdżania: tylko skanowane wiersze zostaną zablokowane.
Ponieważ nie możesz mieć pewności, że indeks jest używany przy każdym połączeniu, możesz po prostu:
WHERE status = 'standby'
AND CASE WHEN status = 'standby' THEN pg_try_advisory_xact_lock(id) END
CASE
Jest logicznie zbędny, ale serwery omawiany cel.
Jeśli polecenie jest częścią długiej transakcji, rozważ blokady na poziomie sesji, które można (i trzeba) zwolnić ręcznie. Możesz więc odblokować, jak tylko skończysz z zablokowanym rzędem: pg_try_advisory_lock()
ipg_advisory_unlock()
. Instrukcja:
Po uzyskaniu na poziomie sesji blokada doradcza jest utrzymywana do momentu jawnego zwolnienia lub zakończenia sesji.
Związane z: