W tej lekcji skupimy się na typowych błędach, które pojawiają się przy pracy z transakcjami, i na tym, jak ich unikać. Serio, nawet największy SQL-ninja czasem zapomni o COMMIT! Damy ci kilka tipów, jak sprawić, żeby błędy w transakcjach były rzadkością.
Niestety (albo i na szczęście), bazy danych to nie magiczny zamek, gdzie wszystko zawsze działa idealnie. Błędy w transakcjach to dość częsty temat, szczególnie dla początkujących devów. Rozłóżmy je na czynniki pierwsze.
Zapomniana komenda COMMIT albo ROLLBACK
Zapomnieć zakończyć transakcję to prawdziwa "klasyka gatunku". Wyobraź sobie knajpę, gdzie zamawiasz żarcie, ale kelner zapomina przynieść rachunek. W świecie PostgreSQL to znaczy, że baza "wisi" w stanie transakcji, trzyma zasoby i blokuje inne operacje.
Przykład błędu:
BEGIN;
UPDATE accounts SET balance = balance - 100 WHERE account_id = 1;
-- Ups! Zapomnieliśmy dodać COMMIT albo ROLLBACK.
Kiedy transakcja "wisi", blokada zasobu może rozlać się na całą tabelę. Jeśli admin bazy wie, że jest lipa, może ją zakończyć "na twardo". Ale lepiej do tego nie dopuszczać.
Jak tego uniknąć?
- Zawsze kończ transakcję jawnie:
COMMITalboROLLBACK. - Korzystaj z narzędzi klienckich, które automatycznie przypominają o wiszących transakcjach.
- Jeśli transakcja nie jest zakończona, a restartujesz apkę, baza sama zrobi
ROLLBACK, ale to nie zawsze wygodne dla stanu systemu.
Użycie złego poziomu izolacji
Wybór poziomu izolacji może się wydawać nudną formalnością, ale to klucz do unikania anomalii. Na przykład, jeśli używasz READ UNCOMMITTED do ważnej operacji finansowej, możesz czytać "brudne" dane, które potem zostaną cofnięte.
Przykład:
SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
BEGIN;
-- Czytamy dane, które zmienia inna transakcja
SELECT balance FROM accounts WHERE account_id = 1;
-- Inna transakcja zrobi ROLLBACK i twoje dane będą nieważne.
Jak tego uniknąć?
- Określ, jak ważne są dane dla twojej apki.
- Używaj
READ COMMITTEDw większości przypadków, żeby uniknąć "brudnego" czytania. - Stosuj wyższe poziomy izolacji
REPEATABLE READ,SERIALIZABLEtam, gdzie musisz mieć pewność, że nie będzie zmian ani phantomów.
Konflikt transakcji i blokady
Czasem dwie albo więcej transakcji próbuje zmienić te same dane. Wtedy PostgreSQL blokuje jedną z nich, aż druga się skończy. To może prowadzić do deadlocka (wzajemnej blokady).
Przykład błędu:
-- Pierwsza transakcja
BEGIN;
UPDATE accounts SET balance = balance - 100 WHERE account_id = 1;
-- Druga transakcja
BEGIN;
UPDATE accounts SET balance = balance + 100 WHERE account_id = 1;
-- Czeka na pierwszą transakcję...
Jeśli obie transakcje trzymają zasoby, których potrzebują nawzajem, mamy deadlock. PostgreSQL wykryje deadlocka, zakończy jedną z transakcji z błędem i wywali komunikat:
ERROR: deadlock detected
Jak tego uniknąć?
- Trzymaj się ustalonej kolejności operacji w transakcjach.
- Minimalizuj czas trwania transakcji, żeby zmniejszyć szansę na blokadę.
- Używaj poziomu izolacji
SERIALIZABLEtylko wtedy, gdy naprawdę musisz.
Błąd z SAVEPOINT
SAVEPOINT to świetne narzędzie do częściowego cofania, ale złe użycie może wprowadzić zamieszanie. Na przykład, jeśli zapomnisz zwolnić savepointa (RELEASE SAVEPOINT), możesz mieć niepotrzebne blokady albo błędy.
Przykład błędu:
BEGIN;
SAVEPOINT my_savepoint;
UPDATE accounts SET balance = balance - 100 WHERE account_id = 1;
ROLLBACK TO SAVEPOINT my_savepoint;
-- Zapomnieliśmy zwolnić SAVEPOINT!
Jak tego uniknąć?
- Upewnij się, że usuwasz
SAVEPOINT, jeśli już go nie potrzebujesz. - Nie twórz za dużo savepointów, żeby nie komplikować zapytań.
Niezgodność transakcji z systemami zewnętrznymi
Wyobraź sobie, że transakcja w PostgreSQL próbuje gadać z zewnętrznymi systemami: wysyła powiadomienia, update'uje API itd. Jeśli coś pójdzie nie tak z zewnętrznym systemem, cofnięcie zmian będzie trudne.
Przykład:
BEGIN;
UPDATE accounts SET balance = balance - 100 WHERE account_id = 1;
-- Nie można wysłać powiadomienia: email-server nie odpowiada.
COMMIT; -- Zmiany zapisane, ale powiadomienie nie poszło.
Jak tego uniknąć?
- Jeśli możesz, izoluj operacje na zewnętrznych systemach.
- Używaj tabel pośrednich albo kolejek zadań, żeby koordynować akcje z zewnętrznymi systemami.
Błędy przez duże transakcje
Duże transakcje, które mają mnóstwo operacji, są bardziej podatne na błędy: blokady, timeouty i deadlocki.
Przykład:
BEGIN;
-- Kilka tysięcy operacji update
UPDATE orders SET status = 'completed' WHERE delivery_date < CURRENT_DATE;
COMMIT; -- Może zająć sporo czasu.
Jak tego uniknąć?
- Dziel duże transakcje na kilka mniejszych.
- Używaj batchy do update'owania danych.
- Minimalizuj ilość danych zmienianych w jednej transakcji.
Zapomniana obsługa błędów
Nie wszystkie SQL-zapytania w transakcji muszą się udać. Na przykład, jeśli jedna z operacji rzuci błąd, cała transakcja pójdzie do kosza.
Przykład:
BEGIN;
UPDATE accounts SET balance = balance - 100 WHERE account_id = -1; -- Błąd: account_id nie istnieje.
UPDATE accounts SET balance = balance + 100 WHERE account_id = 2;
COMMIT; -- Nie wykona się przez błąd.
Jak tego uniknąć?
- Zawsze sprawdzaj wynik każdej operacji.
- Używaj obsługi błędów w zapytaniach albo kodzie klienckim.
Złe rozumienie działania ROLLBACK
Wielu devów myśli, że ROLLBACK cofa zmiany i przywraca wszystko do stanu początkowego. Ale ROLLBACK działa tylko w ramach bieżącej transakcji.
Przykład nieporozumienia:
UPDATE accounts SET balance = balance - 100 WHERE account_id = 1;
ROLLBACK; -- Błąd! To nie działa, bo operacja nie była w transakcji.
Jak tego uniknąć?
- Pamiętaj:
BEGINto twój ziomek, bez niegoROLLBACKjest bezsilny. - Zawsze wrzucaj krytyczne operacje w transakcje.
GO TO FULL VERSION