Transakcje w bazie danych - ACID, COMMIT, ROLLBACK i izolacja

Tymoteusz Kowalski • 2 października 2026
Warunki ACID: atomowość, spójność, izolacja, trwałość. Kluczowe dla transakcji baz danych.

Spis treści

Gdy płatność zostaje pobrana, ale zamówienie nie zapisuje się w systemie, problem zwykle nie leży w pojedynczym zapytaniu SQL, lecz w źle obsłużonej transakcji. W tym artykule pokazuję, jak działają transakcje bazy danych, na czym polega model ACID, kiedy używać COMMIT, ROLLBACK i SAVEPOINT oraz jak ograniczać problemy z równoległym dostępem do danych.

Najważniejsze zasady bezpiecznej pracy z transakcjami

  • Transakcja łączy kilka operacji w jeden logiczny proces.
  • Model ACID chroni atomowość, spójność, izolację i trwałość danych.
  • COMMIT zapisuje zmiany na stałe, a ROLLBACK je wycofuje.
  • Poziom izolacji wpływa na wydajność i ryzyko konfliktów między zapytaniami.
  • Najczęstsze problemy to zakleszczenia, zbyt długie transakcje i przypadkowy autocommit.

Czym jest transakcja w bazie danych

Transakcja to zestaw operacji, który baza traktuje jako jedną logiczną całość. Może obejmować odczyt, kilka modyfikacji i sprawdzenie warunków. Jeżeli wszystkie kroki zakończą się poprawnie, zmiany zostają zatwierdzone. Gdy wystąpi błąd, system może wycofać wykonane operacje.

Najłatwiej zobaczyć to na przykładzie przelewu. System musi zmniejszyć saldo konta A i zwiększyć saldo konta B. Samo wykonanie pierwszego UPDATE nie wystarczy. Gdyby aplikacja zakończyła działanie przed drugim zapytaniem, pieniądze zniknęłyby z jednego konta, ale nie pojawiły się na drugim. Transakcja zabezpiecza oba działania przed takim częściowym wykonaniem.

Typowy przebieg wygląda następująco:

  1. Rozpoczęcie transakcji.
  2. Odczyt danych i sprawdzenie warunków.
  3. Wykonanie jednego lub wielu poleceń SQL.
  4. Zatwierdzenie przez COMMIT albo wycofanie przez ROLLBACK.

W wielu systemach domyślnie działa autocommit. Oznacza to, że pojedyncze polecenie jest automatycznie zatwierdzane po wykonaniu. To wygodne przy prostych odczytach i niezależnych zmianach, ale nie wystarcza wtedy, gdy kilka operacji musi zakończyć się razem.

Model ACID decyduje o bezpieczeństwie danych

Skrót ACID opisuje cztery właściwości, których oczekujemy od solidnego mechanizmu transakcyjnego. Nie jest to tylko teoria z podręcznika. Każda z tych cech odpowiada za inny rodzaj ryzyka, z którym aplikacja może spotkać się podczas awarii albo dużego obciążenia.

Właściwość Znaczenie Przykład
Atomicity, atomowość Operacje wykonują się w całości albo żadna z nich nie zostaje zachowana. Przelew zmniejsza i zwiększa saldo albo nie zmienia żadnego z nich.
Consistency, spójność Po zakończeniu transakcji dane nadal spełniają reguły i ograniczenia. Saldo nie może spaść poniżej zera, jeżeli wymusza to ograniczenie.
Isolation, izolacja Równoległe transakcje nie powinny wzajemnie ujawniać niezatwierdzonych zmian. Drugi użytkownik nie widzi przejściowego salda podczas przelewu.
Durability, trwałość Po zatwierdzeniu zmiany nie powinny zniknąć po awarii procesu lub serwera. Potwierdzone zamówienie pozostaje zapisane po restarcie bazy.

Trzeba jednak rozumieć granice ACID. Baza może zagwarantować spójność własnych tabel, ale nie cofnie wysłanego e-maila, żądania do zewnętrznego API ani płatności wykonanej poza nią. Dlatego w systemach rozproszonych stosuje się dodatkowe mechanizmy, na przykład idempotencję, kolejki i wzorzec outbox.

Moja praktyczna zasada jest prosta. Do transakcji wkładam tylko operacje, które rzeczywiście muszą być atomowe, a komunikację z zewnętrznymi usługami obsługuję osobno. Dzięki temu połączenie z bazą nie pozostaje zablokowane przez wolny serwis płatniczy.

COMMIT, ROLLBACK i SAVEPOINT w SQL

Podstawowe polecenia transakcyjne są podobne w większości relacyjnych systemów baz danych, choć szczegóły składni mogą się różnić. Najczęściej spotkasz BEGIN lub START TRANSACTION, COMMIT oraz ROLLBACK.

BEGIN;

UPDATE konta
SET saldo = saldo - 100
WHERE id = 1 AND saldo >= 100;

UPDATE konta
SET saldo = saldo + 100
WHERE id = 2;

COMMIT;

Ten przykład ma ważne ograniczenie. Sam kod nie sprawdza, czy pierwsze UPDATE rzeczywiście zmieniło rekord. Jeżeli saldo wynosiło mniej niż 100 zł, druga operacja nadal może zostać wykonana. W praktycznej aplikacji trzeba sprawdzić liczbę zmodyfikowanych wierszy i w razie niepowodzenia wywołać rollback całej transakcji.

BEGIN;

UPDATE konta
SET saldo = saldo - 100
WHERE id = 1 AND saldo >= 100;

-- aplikacja sprawdza, czy zmieniono dokładnie 1 rekord

UPDATE konta
SET saldo = saldo + 100
WHERE id = 2;

COMMIT;

ROLLBACK przywraca stan z początku bieżącej transakcji. Jest szczególnie istotny przy wyjątkach, błędach walidacji i konfliktach blokad. W kodzie aplikacji powinien znajdować się w obsłudze błędu, a połączenie z bazą nie powinno być ponownie używane, dopóki transakcja nie zostanie zakończona.

częściowe wycofanie przez SAVEPOINT

SAVEPOINT tworzy punkt kontrolny wewnątrz transakcji. Pozwala wycofać tylko późniejsze operacje, zachowując wcześniejsze zmiany. Przydaje się na przykład podczas importu wielu rekordów, gdy pojedynczy wadliwy element nie powinien anulować całego procesu.

BEGIN;

INSERT INTO zamowienia (id, klient_id)
VALUES (101, 7);

SAVEPOINT przed_pozycjami;

INSERT INTO pozycje_zamowien (zamowienie_id, produkt_id, liczba)
VALUES (101, 55, 2);

ROLLBACK TO SAVEPOINT przed_pozycjami;

COMMIT;

Po poleceniu ROLLBACK TO SAVEPOINT zamówienie pozostaje w transakcji, ale operacje wykonane po punkcie kontrolnym zostają cofnięte. Nie należy jednak traktować savepointów jako zamiennika rozsądnego podziału procesu. Bardzo długa transakcja nadal zużywa zasoby i może utrudniać pracę innym użytkownikom.

Poziomy izolacji a równoległe operacje

Izolacja określa, jak jedna transakcja widzi działania innych transakcji wykonywanych w tym samym czasie. Im silniejsza izolacja, tym mniejsze ryzyko niespójnych odczytów, ale zwykle rośnie koszt blokad, oczekiwania i ponawiania operacji.

Poziom Co dopuszcza Kiedy rozważyć
READ UNCOMMITTED Może pozwolić na odczyt niezatwierdzonych zmian, czyli tak zwany dirty read. Rzadkie raporty, gdy dokładność chwilowego odczytu ma małe znaczenie.
READ COMMITTED Odczyt widzi tylko zatwierdzone dane, ale dwa zapytania w jednej transakcji mogą zobaczyć różne wyniki. Typowe operacje biznesowe i większość aplikacji webowych.
REPEATABLE READ Powtórny odczyt w ramach transakcji zachowuje stabilniejszy obraz danych. Procesy wymagające spójnego widoku kilku odczytów.
SERIALIZABLE Najściślej kontroluje współbieżność, jak gdyby transakcje wykonywały się jedna po drugiej. Krytyczne operacje, gdy ważniejsza jest poprawność niż przepustowość.

Nie istnieje jeden najlepszy poziom dla całej aplikacji. PostgreSQL domyślnie korzysta z READ COMMITTED, natomiast InnoDB w MySQL domyślnie używa REPEATABLE READ. To jedna z przyczyn, dla których identyczny kod może zachowywać się inaczej po przeniesieniu między silnikami.

Warto znać także typowe anomalie. Dirty read oznacza odczyt zmiany, która może zostać wycofana. Non-repeatable read występuje wtedy, gdy ten sam rekord zwraca w jednej transakcji różne wartości. Phantom read pojawia się, gdy ponowne wykonanie warunku zwraca dodatkowe albo znikające wiersze.

Poziom SERIALIZABLE nie jest magicznym wyłącznikiem problemów. System może odrzucić transakcję z powodu konfliktu i oczekiwać, że aplikacja ponowi cały proces od początku. Retry powinien mieć limit, opóźnienie i ochronę przed wielokrotnym wykonaniem skutków ubocznych.

Wizualizacja ACID: Atomicity, Consistency, Isolation, Durability. Kluczowe cechy transakcji baz danych zapewniające ich niezawodność.

Jak projektować transakcje, żeby nie szkodziły wydajności

Dobra transakcja jest możliwie krótka, ale obejmuje wszystkie operacje, które muszą być atomowe. Nie warto otwierać jej przed wykonaniem kosztownego obliczenia, pobraniem pliku ani wywołaniem zewnętrznego serwisu. Każda dodatkowa sekunda może oznaczać dłużej utrzymywane blokady i większe kolejki.

Najczęstsze błędy

  • Brak rollbacku po wyjątku. Po błędzie połączenie może pozostawać w niedokończonej transakcji.
  • Założenie, że każde zapytanie jest częścią transakcji. Przy autocommicie każde polecenie może zostać zatwierdzone osobno.
  • Brak kontroli liczby zmienionych rekordów. Zapytanie może wykonać się poprawnie technicznie, ale nie zmienić żadnego wiersza.
  • Trzymanie transakcji podczas żądania HTTP. Wolny klient lub usługa zewnętrzna może blokować zasoby bazy.
  • Niewłaściwy silnik tabeli. W MySQL mieszanie tabel transakcyjnych i nietransakcyjnych może uniemożliwić pełne cofnięcie zmian.
  • Brak obsługi zakleszczeń. Dwie transakcje mogą czekać na zasoby zablokowane przez siebie nawzajem.

Zakleszczenie nie zawsze oznacza błąd projektu. Przy dużej współbieżności może wystąpić nawet w dobrze napisanym systemie. Aplikacja powinna przechwycić odpowiedni błąd, odczekać krótko i ponowić bezpieczną transakcję, najlepiej z losowym opóźnieniem.

Pomaga także stała kolejność modyfikowania rekordów. Jeżeli każda transakcja najpierw blokuje konto o niższym identyfikatorze, a dopiero później konto o wyższym, ryzyko wzajemnego oczekiwania wyraźnie spada.

Przeczytaj również: Normalizacja danych w SQL - Jak uporządkować bazę?

Transakcja w Pythonie

W kodzie Pythona trzeba sprawdzić, jak konkretna biblioteka obsługuje zatwierdzanie. W poniższym przykładzie używam wbudowanego modułu sqlite3. Blok with zatwierdzi zmiany po pomyślnym zakończeniu, a wyjątek spowoduje ich wycofanie.

import sqlite3

with sqlite3.connect("sklep.db") as conn:
    conn.execute(
        "UPDATE konta SET saldo = saldo - ? "
        "WHERE id = ? AND saldo >= ?",
        (100, 1, 100)
    )

    if conn.total_changes != 1:
        raise ValueError("Brak wystarczających środków")

    conn.execute(
        "UPDATE konta SET saldo = saldo + ? WHERE id = ?",
        (100, 2)
    )

W większych aplikacjach podobny schemat realizuje się przez menedżer kontekstu sesji lub połączenia. Najważniejsze jest, aby commit następował dopiero po przejściu wszystkich walidacji, a wyjątek nie był cicho ignorowany.

Transakcje w realnych scenariuszach

W systemie sklepowym jedna transakcja może utworzyć zamówienie, odjąć stan magazynowy i zapisać pozycje zamówienia. To dobry kandydat do atomowego wykonania, ponieważ brak któregokolwiek elementu utrudnia późniejsze rozliczenie.

BEGIN;

INSERT INTO zamowienia (klient_id, status)
VALUES (7, 'nowe');

UPDATE produkty
SET stan = stan - 1
WHERE id = 55 AND stan > 0;

-- aplikacja sprawdza, czy stan został zmniejszony

INSERT INTO pozycje_zamowien (zamowienie_id, produkt_id, liczba)
VALUES (101, 55, 1);

COMMIT;

Nie umieszczałbym jednak w tej samej transakcji wysyłki e-maila ani oczekiwania na operatora płatności. Lepszy układ to zapisanie zdarzenia do tabeli outbox, zatwierdzenie zmian, a potem wysłanie komunikatu przez osobny proces. Awaria wysyłki nie niszczy wtedy poprawnie zapisanego zamówienia.

Inaczej wygląda masowy import. Transakcja obejmująca kilka milionów rekordów może długo zajmować blokady, rozrastać log transakcyjny i utrudniać odzyskiwanie po awarii. W takim przypadku często lepiej użyć partii, na przykład po 1000 lub 10 000 rekordów, po wcześniejszym przetestowaniu wpływu na konkretny system.

W aplikacjach finansowych, magazynowych i rezerwacyjnych szczególne znaczenie ma warunek sprawdzany razem z modyfikacją. Zamiast najpierw odczytywać saldo, a później wykonywać niezależny zapis, można zastosować UPDATE ... WHERE saldo >= kwota. To ogranicza okno, w którym dwie równoległe operacje mogłyby wykorzystać ten sam stan danych.

Jak sprawdzić, czy mechanizm działa poprawnie

Test transakcji nie powinien kończyć się na sprawdzeniu, czy zapytanie zwróciło wynik. Trzeba zasymulować również błąd po pierwszej modyfikacji, zerwanie połączenia, dwa równoległe żądania i próbę zapisania danych łamiących ograniczenie.

  • Sprawdź, czy błąd w połowie procesu wycofuje wszystkie wcześniejsze zmiany.
  • Uruchom równolegle dwie operacje dotyczące tego samego rekordu.
  • Przetestuj zachowanie po przekroczeniu limitu lub braku produktu.
  • Zmierz czas trwania transakcji i liczbę oczekujących blokad.
  • Zweryfikuj, czy retry nie tworzy podwójnego zamówienia albo podwójnej płatności.

Loguję przede wszystkim identyfikator transakcji, czas rozpoczęcia, czas zakończenia i przyczynę wycofania. Nie zapisuję bez potrzeby pełnych danych użytkownika ani wrażliwych parametrów. Takie metryki szybko pokazują, czy problemem jest baza, aplikacja czy zewnętrzna usługa.

Jeżeli transakcje często kończą się błędem, samo zwiększenie limitu czasu oczekiwania zwykle nie rozwiązuje problemu. Najpierw sprawdzam kolejność blokowania, indeksy, długość operacji i poziom izolacji. Wydajność transakcji poprawia się przez skracanie pracy wewnątrz niej, a nie przez bezrefleksyjne wyłączanie zabezpieczeń.

Najprostszy model decyzyjny przed wdrożeniem

Przed połączeniem kilku zapytań w jedną transakcję zadaję sobie trzy pytania. Czy wszystkie operacje muszą udać się razem? Czy dane są modyfikowane równolegle przez wielu użytkowników? Czy aplikacja potrafi bezpiecznie powtórzyć proces po konflikcie?

Jeżeli odpowiedź na pierwsze pytanie brzmi tak, potrzebujesz jawnej transakcji i kontroli błędów. Gdy ważna jest współbieżność, dobierz poziom izolacji do konkretnego przypadku, zamiast automatycznie wybierać najbardziej restrykcyjny wariant. Jeżeli operacja może zostać ponowiona, zadbaj o idempotentny identyfikator, który uniemożliwi podwójny zapis.

Najważniejsza lekcja jest praktyczna. Transakcja nie służy do opakowania całej funkcji aplikacji, tylko do ochrony konkretnej zmiany, która musi przejść z jednego poprawnego stanu do drugiego. Tak zaprojektowane mechanizmy są łatwiejsze do testowania, szybsze i znacznie mniej podatne na trudne do odtworzenia błędy.

FAQ - Najczęstsze pytania

COMMIT zatwierdza wszystkie poprawnie wykonane zmiany na stałe. ROLLBACK wycofuje całą bieżącą transakcję, dlatego należy użyć go po wyjątku, błędzie walidacji lub konflikcie blokad. Przed zatwierdzeniem warto sprawdzić między innymi liczbę zmodyfikowanych rekordów.

READ COMMITTED pokazuje tylko zatwierdzone dane, ale kolejne odczyty mogą zwrócić różne wyniki. REPEATABLE READ zapewnia stabilniejszy obraz danych w jednej transakcji, natomiast SERIALIZABLE najściślej kontroluje współbieżność kosztem wydajności i możliwych konfliktów wymagających ponowienia całej transakcji.

Użyj SAVEPOINT, aby utworzyć punkt kontrolny wewnątrz transakcji, a następnie ROLLBACK TO SAVEPOINT, by wycofać tylko operacje wykonane później. Pozwala to na przykład zachować utworzone zamówienie mimo błędu przy dodawaniu jego pozycji. Bardzo długa transakcja nadal zużywa zasoby i może blokować innych użytkowników.

Baza danych nie cofnie płatności, e-maila ani żądania do zewnętrznego API po wykonaniu ROLLBACK. Lepiej zatwierdzić zmianę w bazie i zapisać zdarzenie w tabeli outbox, a wysyłkę obsłużyć osobnym procesem. Ogranicza to czas blokowania połączenia i ułatwia ponawianie operacji.

Aplikacja powinna przechwycić błąd, odczekać krótko z losowym opóźnieniem i ponowić całą transakcję. Pomaga także stała kolejność blokowania rekordów, na przykład modyfikowanie kont według rosnącego identyfikatora. Retry powinien mieć limit i ochronę przed wielokrotnym wykonaniem skutków ubocznych.

Oceń artykuł

Ocena: 0.00 Liczba głosów: 0

Tagi

transakcje
izolacja
acid
zakleszczenia
autocommit
Autor Tymoteusz Kowalski
Tymoteusz Kowalski
Nazywam się Tymoteusz Kowalski i od 7 lat zajmuję się programowaniem, ze szczególnym uwzględnieniem Pythona oraz nowoczesnych technologii. Moja przygoda z programowaniem zaczęła się od fascynacji możliwościami, jakie daje kod, a z czasem przerodziła się w pasję do dzielenia się wiedzą i pomagania innym w zrozumieniu złożonych zagadnień. Interesuje mnie, jak można uprościć trudne tematy, aby były bardziej przystępne dla każdego, niezależnie od poziomu zaawansowania. W moich tekstach staram się dostarczać rzetelne, aktualne i zrozumiałe informacje, a także porównywać różne źródła, aby zapewnić czytelnikom szeroki kontekst. Piszę o praktycznych zastosowaniach Pythona, nowinkach technologicznych oraz najlepszych praktykach programistycznych. Moim celem jest nie tylko przedstawienie teorii, ale także pokazanie, jak można ją zastosować w praktyce, co mam nadzieję, uczyni moją twórczość użyteczną dla każdego, kto pragnie rozwijać swoje umiejętności w programowaniu.

Udostępnij artykuł

Napisz komentarz