Transakcje baz danych - ACID, COMMIT, ROLLBACK i izolacja

Radosław Krajewski 5 września 2026
Warunki ACID dla transakcji baz danych: atomowość, spójność, izolacja, trwałość.

Spis treści

Co dzieje się z pieniędzmi, zamówieniem albo stanem magazynu, gdy druga operacja nie powiedzie się w połowie pracy? Transakcje bazy danych rozwiązują ten problem, łącząc kilka poleceń SQL w jedną logiczną całość. Pokażę, jak działają właściwości ACID, kiedy używać COMMIT i ROLLBACK, jak dobierać poziom izolacji oraz na co uważać w aplikacjach .NET i Azure.

Najważniejsze zasady bezpiecznej pracy z transakcjami

  • ACID chroni atomowość, spójność, izolację i trwałość zmian.
  • COMMIT zatwierdza całość, a ROLLBACK wycofuje niezapisane operacje.
  • Transakcja powinna obejmować tylko operacje, które muszą zakończyć się razem.
  • Poziom izolacji wpływa na blokady, widoczność danych i ryzyko odczytu nieaktualnych wartości.
  • W aplikacji trzeba obsłużyć błędy, ponowienia oraz sytuację, w której baza pozostaje w stanie niezatwierdzonej transakcji.

Po co właściwie używa się transakcji

Wyobraźmy sobie przelew między dwoma kontami. System powinien zmniejszyć saldo jednego konta i zwiększyć saldo drugiego. Jeśli pierwsze polecenie się powiedzie, a drugie zakończy błędem, baza pozostanie w stanie, którego nie da się zaakceptować. Transakcja sprawia, że oba zapisy zostaną wykonane razem albo oba zostaną anulowane.

To samo dotyczy złożenia zamówienia. Zapis nagłówka zamówienia, pozycji, płatności i zmniejszenie stanu magazynowego często tworzą jedną jednostkę biznesową. Nie każda operacja musi należeć do tej samej transakcji, ale jeśli częściowy wynik byłby błędem, powinny zostać objęte wspólnym zakresem.

ACID w praktyce

Właściwość Znaczenie Przykład
Atomicity Atomowość, czyli wszystko albo nic Nie można zapisać płatności bez powiązanego zamówienia.
Consistency Spójność danych po zakończeniu operacji Klucz obcy i ograniczenie unikalności nadal są respektowane.
Isolation Odseparowanie równoległych operacji Drugi proces nie powinien odczytać połowy przelewu.
Durability Trwałość zatwierdzonych zmian Po poprawnym zatwierdzeniu dane nie powinny zniknąć po restarcie serwera.

ACID nie oznacza, że aplikacja automatycznie rozwiąże każdy problem. Baza może zagwarantować spójność własnych danych, ale nie cofnie wysłanego e-maila, żądania do zewnętrznego API ani pliku zapisanego na dysku. To jedna z granic, o których początkujący często dowiadują się dopiero po wdrożeniu.

Jak wygląda cykl transakcji w SQL

Najprostszy schemat składa się z rozpoczęcia pracy, wykonania poleceń i zakończenia przez zatwierdzenie albo wycofanie. W SQL Server używa się zwykle poleceń BEGIN TRANSACTION, COMMIT TRANSACTION i ROLLBACK TRANSACTION.

BEGIN TRANSACTION;

UPDATE Konta
SET Saldo = Saldo - 100
WHERE Id = 1;

UPDATE Konta
SET Saldo = Saldo + 100
WHERE Id = 2;

COMMIT TRANSACTION;

Jeżeli dowolne polecenie zgłosi błąd, aplikacja powinna wykonać wycofanie. W przeciwnym razie połączenie może nadal utrzymywać blokady, a kolejne zapytania będą pracować w nieoczekiwanym stanie.

BEGIN TRY
    BEGIN TRANSACTION;

    UPDATE Konta
    SET Saldo = Saldo - 100
    WHERE Id = 1;

    UPDATE Konta
    SET Saldo = Saldo + 100
    WHERE Id = 2;

    COMMIT TRANSACTION;
END TRY
BEGIN CATCH
    IF XACT_STATE() <> 0
        ROLLBACK TRANSACTION;

    THROW;
END CATCH;

Włączenie SET XACT_ABORT ON jest w SQL Server często rozsądnym zabezpieczeniem, ponieważ część błędów automatycznie kończy wtedy transakcję. Nie zastępuje to jednak obsługi wyjątków. Najważniejsza zasada brzmi: każda ścieżka wykonania musi kończyć się świadomym COMMIT-em albo ROLLBACK-iem.

Autocommit i jawny zakres

W trybie autocommit pojedyncze polecenie jest traktowane jako osobna transakcja. To wygodne przy prostym SELECT albo pojedynczym UPDATE, ale nie wystarcza, gdy kilka instrukcji musi zachować wspólną spójność.

W aplikacjach .NET jawny zakres można otworzyć przez DbTransaction, SqlTransaction albo mechanizmy używane przez Entity Framework Core. Samo wywołanie kilku metod repozytorium nie tworzy jeszcze jednej transakcji. Granica musi być ustawiona na poziomie jednostki pracy, a nie przypadkowo wewnątrz pojedynczego zapytania.

Poziomy izolacji decydują o zachowaniu równoległych operacji

Gdy kilka żądań działa jednocześnie, pojawia się pytanie, co jedno z nich może zobaczyć z pracy drugiego. Poziom izolacji określa między innymi, czy odczyt może czekać na blokadę, zobaczyć niezatwierdzone dane albo ponownie odczytać ten sam rekord z inną wartością.

Poziom Co dopuszcza Kiedy uważać
READ UNCOMMITTED Możliwy odczyt zmian, których inna transakcja jeszcze nie zatwierdziła. Praktycznie nie nadaje się do danych finansowych ani biznesowych.
READ COMMITTED Nie pokazuje niezatwierdzonych zmian, ale kolejny odczyt może zwrócić inną wartość. To częsty poziom domyślny, lecz nie zawsze wystarcza dla złożonych procesów.
REPEATABLE READ Chroni wcześniej odczytane wiersze przed zmianą w czasie transakcji. Może zwiększyć liczbę blokad i nadal dopuszczać nowe wiersze spełniające warunek.
SERIALIZABLE Najsilniejsza izolacja, z zachowaniem efektu wykonywania operacji jedna po drugiej. Łatwo doprowadzić do długiego oczekiwania i zakleszczeń.

W SQL Server i PostgreSQL domyślnie często spotyka się READ COMMITTED, ale sposób jego realizacji może być różny. Niektóre konfiguracje korzystają z wersjonowania wierszy, inne w większym stopniu z blokad. Dlatego przed zmianą izolacji sprawdzam nie tylko dokumentację silnika, lecz także rzeczywistą konfigurację środowiska.

Wyższa izolacja nie jest automatycznie lepsza. Daje silniejsze gwarancje, ale może obniżyć współbieżność. Dla raportu, który nie wykonuje zapisów, czasem wystarczy słabsza ochrona. Dla rezerwacji ostatniego produktu trzeba już dokładnie zabezpieczyć odczyt i zapis, aby dwa żądania nie przyznały tego samego zasobu.

Wizualizacja właściwości ACID dla transakcji baz danych: Atomicity, Consistency, Isolation, Durability.

Blokady, zakleszczenia i anomalie danych

Transakcja chroni dane, ale sama również korzysta z zasobów. Przy zapisie baza może założyć blokadę wiersza, strony albo większego zakresu. Im dłużej trwa operacja, tym dłużej inne procesy mogą czekać, dlatego czas trwania transakcji ma bezpośredni wpływ na wydajność.

Najczęstsze anomalie

  • Brudny odczyt pojawia się wtedy, gdy proces widzi dane jeszcze niezatwierdzone przez inną transakcję.
  • Odczyt niepowtarzalny oznacza, że ten sam rekord zwraca różne wartości podczas jednej pracy.
  • Fantomy to nowe lub znikające wiersze pojawiające się przy ponownym wykonaniu tego samego warunku.
  • Zakleszczenie występuje, gdy dwie transakcje czekają na zasoby trzymane przez siebie nawzajem.

Zakleszczenie nie musi oznaczać błędu projektu. Przy dużym ruchu może pojawić się nawet w poprawnej aplikacji, dlatego warstwa aplikacyjna powinna rozpoznać odpowiedni wyjątek i ponowić całą transakcję. Ponowienie pojedynczego polecenia zwykle nie wystarcza, bo pozostałe operacje muszą zostać wykonane w nowym, spójnym kontekście.

Najlepiej ograniczyć ryzyko przez krótkie transakcje, stałą kolejność dostępu do tabel oraz dobre indeksy. Nie otwieram transakcji przed wywołaniem zewnętrznego API i nie czekam w niej na interakcję użytkownika. Takie decyzje potrafią zablokować rekordy na sekundy lub minuty, a przy większym ruchu szybko zamieniają się w lawinę oczekujących zapytań.

Savepointy i obsługa błędów w aplikacjach .NET

Czasem większa operacja składa się z kilku etapów, z których jeden można wycofać bez anulowania wcześniejszych zmian. Do tego służy SAVEPOINT. Po utworzeniu punktu można wykonać ROLLBACK TO i wrócić do wskazanego miejsca, zachowując wcześniejszą część pracy.

BEGIN TRANSACTION;

INSERT INTO Zamowienia (KlientId)
VALUES (10);

SAVE TRANSACTION Pozycje;

INSERT INTO PozycjeZamowien (ZamowienieId, ProduktId, Ilosc)
VALUES (100, 5, 2);

-- W razie błędu:
ROLLBACK TRANSACTION Pozycje;

COMMIT TRANSACTION;

Savepoint nie jest jednak zamiennikiem dobrego podziału procesu. Jeśli etap jest niezależny biznesowo, często lepiej zapisać go w osobnej transakcji. W Entity Framework Core trzeba też pamiętać, że kontekst śledzi zmiany w pamięci, a wycofanie transakcji w bazie nie zawsze oznacza wyczyszczenie stanu śledzenia encji.

Co powinien robić kod aplikacji

  • Otwierać transakcję możliwie blisko granicy jednostki biznesowej.
  • Wykonywać rollback w bloku obsługi błędów.
  • Logować identyfikator operacji, czas trwania i typ błędu.
  • Ponawiać tylko błędy przejściowe, takie jak chwilowa niedostępność lub wybrane zakleszczenia.
  • Zapewniać idempotencję, czyli możliwość bezpiecznego powtórzenia żądania.

Idempotencja jest szczególnie ważna przy płatnościach i kolejkach wiadomości. Jeśli klient nie wie, czy żądanie zakończyło się przed zerwaniem połączenia, może wysłać je ponownie. Unikalny identyfikator operacji pozwala rozpoznać duplikat i uniknąć podwójnego zapisu.

Najczęstsze błędy przy projektowaniu transakcji

Transakcja obejmuje zbyt dużo

Trzymanie otwartej transakcji podczas generowania raportu, wysyłania e-maila albo wywołania usługi płatniczej zwiększa liczbę blokad i ryzyko przekroczenia limitu czasu. Dane należy zapisać szybko, a komunikację z innymi systemami oprzeć na kolejce, zdarzeniu domenowym lub wzorcu outbox.

Rollback jest tylko teoretyczny

Częsty błąd polega na obsłużeniu wyjątku i zwróceniu komunikatu do klienta bez faktycznego wycofania zmian. W SQL Server trzeba też uważać na transakcję, która po błędzie jest już niezatwierdzalna. Sprawdzenie XACT_STATE() pomaga ustalić, czy można wykonać rollback.

Brak ograniczeń w samej bazie

Transakcja nie zastępuje kluczy obcych, indeksów unikalnych ani ograniczeń CHECK. Jeśli reguła ma chronić dane zawsze, powinna być zapisana również w schemacie bazy. Kod aplikacji może zawierać błąd, działać w kilku instancjach albo zostać ominięty przez skrypt administracyjny.

Przeczytaj również: Rekurencyjne zapytania SQL bez pułapek CTE i cykli

Mylenie transakcji z blokadą

Transakcja opisuje logiczną całość pracy, natomiast blokada jest jednym z mechanizmów ochrony współbieżnego dostępu. Można mieć transakcję, która prawie nie blokuje odczytów dzięki wersjonowaniu wierszy, albo krótkie zapytanie, które czeka na zasób zablokowany przez inną operację. To rozróżnienie bardzo ułatwia diagnozowanie problemów wydajnościowych.

Jak dobrać bezpieczny schemat dla konkretnej operacji

Zaczynam od pytania, co dokładnie musi być prawdziwe po zakończeniu procesu. Jeśli odpowiedź brzmi „albo wszystkie te zapisy, albo żaden”, tworzę jedną transakcję. Jeśli część procesu może zostać wykonana później, oddzielam ją i przekazuję dalej przez zdarzenie lub kolejkę.

Dla typowej operacji biznesowej stosuję prostą kolejność:

  1. Sprawdź dane wejściowe i uprawnienia przed otwarciem transakcji.
  2. Rozpocznij transakcję możliwie późno.
  3. Wykonaj wszystkie zależne od siebie odczyty i zapisy.
  4. Zatwierdź zmiany natychmiast po spełnieniu warunków.
  5. W przypadku błędu wycofaj całość i zwróć kontrolowany rezultat.
  6. Zmierz czas trwania oraz sprawdź, czy nie pojawiają się blokady.

Jeśli operacja dotyczy rezerwacji, dodaję kontrolę konkurencji, na przykład warunek w instrukcji UPDATE albo odpowiedni indeks i poziom izolacji. Sam wcześniejszy SELECT z informacją „produkt jest dostępny” nie gwarantuje jeszcze, że będzie dostępny w chwili zapisu.

Moim zdaniem najwięcej problemów bierze się nie z braku znajomości składni, lecz z niejasnej granicy procesu. Dobrze zaprojektowana transakcja jest krótka, przewidywalna, możliwa do ponowienia i obejmuje dokładnie te dane, które muszą zmienić się razem.

Spójność danych zaczyna się od dobrze wyznaczonej granicy

Transakcje są fundamentem bezpiecznych operacji w relacyjnych bazach danych, ale ich skuteczność zależy od decyzji projektowych. Trzeba dobrać właściwy zakres, poziom izolacji, obsługę błędów i strategię ponowień, a przy tym pamiętać, że system zewnętrzny nie zostanie automatycznie cofnięty razem z bazą.

Na początek wystarczy przeanalizować trzy rzeczy: które zapisy muszą być atomowe, jakie operacje mogą działać równolegle i co stanie się po zerwaniu połączenia. Taka krótka analiza zwykle daje większą poprawę jakości niż samo dodanie BEGIN TRANSACTION do istniejącego kodu.

FAQ - Najczęstsze pytania

Atomowość zapewnia wykonanie wszystkich operacji razem albo wycofanie całości. Spójność pilnuje reguł bazy, izolacja ogranicza wpływ równoległych operacji, a trwałość zachowuje zatwierdzone zmiany po restarcie serwera.

COMMIT stosuje się po poprawnym wykonaniu wszystkich zależnych poleceń. Jeśli wystąpi błąd, aplikacja powinna wykonać ROLLBACK, a w SQL Server warto sprawdzić XACT_STATE(), ponieważ nie każdą transakcję można już zatwierdzić.

READ COMMITTED nie pokazuje niezatwierdzonych danych, REPEATABLE READ chroni wcześniej odczytane wiersze, a SERIALIZABLE zapewnia najsilniejszą izolację kosztem większej liczby blokad i oczekiwania. READ UNCOMMITTED może ujawniać niezatwierdzone zmiany, dlatego nie nadaje się do danych finansowych ani biznesowych.

Transakcję należy wycofać, zalogować błąd i ponowić całą operację, a nie tylko pojedyncze polecenie. Pomagają krótkie zakresy transakcji, stała kolejność dostępu do tabel, dobre indeksy oraz idempotencja. Wywołań zewnętrznych API nie należy wykonywać wewnątrz transakcji; lepiej użyć kolejki, zdarzenia lub wzorca outbox.

Oceń artykuł

Ocena: 0.00 Liczba głosów: 0

Tagi

transakcje
acid
izolacja
zakleszczenia
idempotencja
Autor Radosław Krajewski
Radosław Krajewski
Nazywam się Radosław Krajewski i od 6 lat zgłębiam tajniki programowania .NET, chmury Azure oraz sztucznej inteligencji. Moja przygoda z tymi technologiami zaczęła się od fascynacji tym, jak złożone problemy można rozwiązywać za pomocą kodu i innowacyjnych narzędzi. Staram się przekazywać tę wiedzę w sposób zrozumiały, dzieląc się swoimi doświadczeniami i spostrzeżeniami na kursdotnet.pl. W moich artykułach skupiam się na praktycznych aspektach, porównuję różne rozwiązania i analizuję najnowsze trendy, aby dostarczyć Wam rzetelne i aktualne informacje, które pomogą Wam rozwijać się w tej dynamicznie zmieniającej się dziedzinie.

Udostępnij artykuł

Napisz komentarz