Wyobraź sobie, że pracujesz w biurze, gdzie wszystkie drzwi są zablokowane, bo ktoś zapomniał kluczy w środku pokoju. Tak właśnie działają blokady w PostgreSQL. Jeśli zapytanie albo transakcja zablokowała jakiś zasób, inne operacje próbujące dostać się do tego samego zasobu muszą poczekać, aż ta pierwsza się skończy. Taka sytuacja może powodować opóźnienia, konflikty i w skrajnym przypadku zatrzymać cały system.
Kiedy pojawiają się blokady?
Blokady (Locks) w PostgreSQL są używane do zarządzania współbieżnym dostępem do danych. Pojawiają się:
- Podczas operacji zapisu:
UPDATE,DELETE,INSERT. - Gdy transakcje trzymają zasób dłużej niż to potrzebne.
- Przy konfliktach między różnymi transakcjami, które chcą ten sam zasób.
Prawdziwa baza danych to "pole bitwy" o zasoby i nawet jeśli myślisz, że wszystko działa idealnie, jedna nieostrożna transakcja może wszystko "zablokować", jak nieudany merge w Git.
Narzędzia do analizy blokad: pg_locks
pg_locks to systemowy widok PostgreSQL pokazujący aktualne blokady trzymane i oczekiwane przez transakcje. Odpowiada na pytanie: "Kto trzyma blokadę, a kto czeka?"
Najważniejsze pola pg_locks:
locktype: typ blokady (np.relation,transaction,page,tuple).database: identyfikator bazy danych.relation: identyfikator tabeli (jeśli blokada dotyczy tabeli).mode: tryb blokady (np.RowExclusiveLock,AccessShareLock).granted: flaga pokazująca, czy blokada została przyznana (true), czy transakcja jeszcze na nią czeka (false).
Uwaga: PostgreSQL stosuje tzw. "hierarchiczny tryb blokad". To znaczy, że różne operacje mogą nakładać mniej restrykcyjne blokady (np. AccessShareLock do czytania danych) albo bardziej restrykcyjne (ExclusiveLock do zmiany struktury tabeli).
Przykład: podgląd wszystkich aktualnych blokad
SELECT *
FROM pg_locks;
Ale jeśli po prostu wyświetlisz całą zawartość pg_locks, będzie za dużo szumu. Spróbujmy czegoś bardziej sensownego!
Przykład: blokady, które jeszcze nie zostały przyznane (czyli transakcje czekają)
SELECT pid, locktype, relation::regclass AS table_name, mode, granted
FROM pg_locks
WHERE NOT granted;
Co tu się dzieje?
- Filtrujemy rekordy, gdzie
granted = false, czyli blokada jeszcze nie została przyznana. relation::regclasszamienia identyfikator tabeli na jej nazwę, żeby było czytelniej.
Wynik może wyglądać mniej więcej tak:
| pid | locktype | table_name | mode | granted |
|---|---|---|---|---|
| 1234 | relation | students | RowExclusiveLock | false |
| 4321 | relation | courses | RowShareLock | false |
Te zapytania pomogą ci zidentyfikować, która tabela/zasób jest blokowana i która transakcja może być powodem.
Analiza konfliktów: pg_blocking_pids()
Blokady to jeszcze pół biedy, ale co zrobić, jeśli jedna transakcja blokuje drugą? PostgreSQL daje wygodny sposób na znalezienie "winowajcy" przez funkcję pg_blocking_pids().
Funkcja pg_blocking_pids() zwraca listę identyfikatorów procesów (pid), które blokują wykonanie aktualnej transakcji.
Przykład: szukanie transakcji, które blokują inne
SELECT pid, pg_blocking_pids(pid) AS blocking_pids
FROM pg_stat_activity
WHERE cardinality(pg_blocking_pids(pid)) > 0;
Co tu się dzieje?
- Używamy widoku
pg_stat_activity, żeby wyciągnąć aktywne procesy w systemie. - Funkcja
pg_blocking_pids(pid)zwraca listę blokujących procesów dla każdegopid. Jeśli lista nie jest pusta (długość większa od 0), to znaczy, że proces jest zablokowany.
Przykład wyniku:
| pid | blocking_pids |
|---|---|
| 4567 | {1234, 5678} |
| 6789 | {4321} |
Transakcja z pid = 4567 jest zablokowana przez procesy 1234 i 5678. Mamy naszych "winowajców".
Zakończenie blokujących procesów
Kiedy już znajdziesz blokujące procesy, możesz je zatrzymać przez funkcję pg_terminate_backend():
SELECT pg_terminate_backend(1234); -- "Zabijamy" proces 1234
Ale uważaj! Wymuszone zatrzymanie procesu może spowodować rollback danych w tej transakcji. Używaj tego narzędzia jak "atomowego przycisku" tylko w ostateczności.
Zastosowanie w praktyce: scenariusz analizy blokad
Załóżmy, że mamy bazę danych dla uniwersytetu z tabelami students i enrollments. Kilka transakcji próbuje jednocześnie zapisywać dane do tabeli enrollments i pojawiają się blokady.
- Identyfikacja blokad:
SELECT pid, locktype, relation::regclass AS table_name, mode, granted
FROM pg_locks
WHERE NOT granted;
- Określenie blokujących procesów:
SELECT pid, pg_blocking_pids(pid) AS blocking_pids
FROM pg_stat_activity
WHERE cardinality(pg_blocking_pids(pid)) > 0;
- Usuwanie blokad:
Wymuszamy zakończenie jednego z konfliktujących procesów:
SELECT pg_terminate_backend(1234); -- Kończymy proces 1234
Uwaga: Zanim "zabijesz" proces, spróbuj zrozumieć, dlaczego pojawiła się blokada. Może warto przejrzeć logikę transakcji.
Typowe błędy i jak ich unikać
Blokady często pojawiają się przez złe zarządzanie transakcjami. Na przykład:
Błąd: jedna transakcja trzyma blokadę zbyt długo, nie wykonując żadnych operacji (stan "idle in transaction").
Rozwiązanie: pilnuj stanu transakcji przez pg_stat_activity i kończ "zawieszone" transakcje.
SELECT pid, state, query
FROM pg_stat_activity
WHERE state = 'idle in transaction';
Błąd: zapomniałeś użyć indeksów w zapytaniach, co spowodowało blokady na całych tabelach.
Rozwiązanie: optymalizuj zapytania, dodając indeksy do często używanych warunków.
Wyświetlanie tabeli "kto na kogo czeka"
Dla wygody diagnostyki możesz zbudować drzewo zależności pokazujące, która transakcja blokuje którą:
WITH RECURSIVE blocking_tree AS (
SELECT pid, pg_blocking_pids(pid) AS blocked_by
FROM pg_stat_activity
WHERE cardinality(pg_blocking_pids(pid)) > 0
UNION ALL
SELECT a.pid, pg_blocking_pids(a.pid)
FROM pg_stat_activity a
JOIN blocking_tree b ON a.pid = ANY(b.blocked_by)
)
SELECT pid, blocked_by FROM blocking_tree;
Wynik:
| pid | blocked_by |
|---|---|
| 4567 | {1234} |
| 1234 | {5678} |
| 5678 | {} |
Widać tu, że proces 5678 blokuje proces 1234, a ten z kolei blokuje 4567.
GO TO FULL VERSION