7 min

Transakcje Postgres dla wieloetapowych przepływów: praktyczne wzorce

Naucz się używać transakcji Postgres dla wieloetapowych przepływów: jak grupować aktualizacje, zapobiegać częściowym zapisom, obsługiwać retry i zachować spójność danych.

Transakcje Postgres dla wieloetapowych przepływów: praktyczne wzorce

Dlaczego wieloetapowe aktualizacje często kończą się niespójnością

Większość realnych funkcji to nie pojedyncza zmiana w bazie. To krótki łańcuch: wstaw row, zaktualizuj saldo, oznacz status, zapisz wpis audytowy, może dodaj zadanie do kolejki. Częściowy zapis zdarza się, gdy tylko niektóre z tych kroków trafią do bazy.

Objawia się to, gdy coś przerwie łańcuch: błąd serwera, timeout między aplikacją a Postgresem, awaria po kroku 2 lub retry, który ponownie wykona krok 1. Każde polecenie samo w sobie jest poprawne. Przepływ psuje się, gdy zatrzyma się w połowie.

Zwykle łatwo to zauważyć:

  • Istnieje wiersz, ale brak powiązanego wiersza (zamówienie utworzone, brak pozycji)
  • Pieniądze przeszły, ale status się nie zmienił (zapłacono, nadal oznaczone jako nieopłacone)
  • Dwa rekordy, które powinny być jednym (zduplikowane subskrypcje po retry)
  • Flagi się nie zgadzają (użytkownik jest „aktywny”, ale nie ma planu)
  • Stany pojawiają się tylko przy obciążeniu lub awariach

Konkretny przykład: aktualizacja planu zmienia plan klienta, dodaje rekord płatności i zwiększa dostępne kredyty. Jeśli aplikacja padnie po zapisaniu płatności, ale przed dodaniem kredytów, dział wsparcia widzi w jednej tabeli „zapłacono”, a w innej „brak kredytów”. Jeśli klient ponowi żądanie, możesz nawet zapisać płatność dwa razy.

Cel jest prosty: traktuj przepływ jak pojedynczy przełącznik. Albo wszystkie kroki się powiodą, albo żaden, żeby nigdy nie przechowywać pracy w połowie zrobionej.

Transakcje, prostymi słowami

Transakcja to sposób bazy danych, by powiedzieć: traktuj te kroki jako jedną jednostkę pracy. Albo wszystkie zmiany zajdą, albo żadna. To ma znaczenie zawsze, gdy Twój przepływ wymaga więcej niż jednej aktualizacji, np. stworzenie wiersza, zmiana salda i zapis audytu.

Pomyśl o przenoszeniu pieniędzy między dwoma kontami. Musisz odpowiednio odjąć z konta A i dodać do konta B. Jeśli aplikacja padnie po pierwszym kroku, nie chcesz, żeby system „zapamiętał” tylko odjęcie.

Commit vs rollback

Kiedy commitujesz, mówisz Postgresowi: zachowaj wszystko, co zrobiłem w tej transakcji. Wszystkie zmiany stają się trwałe i widoczne dla innych sesji.

Kiedy rollbackujesz, mówisz Postgresowi: zapomnij wszystko, co zrobiłem w tej transakcji. Postgres cofa zmiany tak, jakby transakcja nigdy nie miała miejsca.

Co Postgres gwarantuje (a czego nie)

W ramach transakcji Postgres gwarantuje, że nie ujawnisz innym sesjom półgotowych wyników przed commitem. Jeśli coś się nie powiedzie i wycofasz transakcję, baza posprząta zapisy z tej transakcji.

Transakcja nie naprawi złego projektu przepływu. Jeśli odjąłeś złą kwotę, użyłeś złego identyfikatora użytkownika lub pominąłeś potrzebną kontrolę, Postgres wiernie zacommituje błędny wynik. Transakcje też nie zapobiegają automatycznie wszystkim konfliktom biznesowym (jak overselling), chyba że połączysz je z odpowiednimi ograniczeniami, blokadami lub poziomem izolacji.

Przepływy, które warto grupować

Za każdym razem, gdy aktualizujesz więcej niż jedną tabelę (lub więcej niż jeden wiersz) by zakończyć jedną realną akcję, masz kandydata do transakcji. Zasada jest ta sama: albo wszystko jest zrobione, albo nic.

Klasyczny przypadek to przepływ zamówienia. Tworzysz wiersz zamówienia, rezerwujesz zapas, pobierasz płatność, potem oznaczasz zamówienie jako opłacone. Jeśli płatność się powiedzie, ale aktualizacja statusu nie, masz pieniądze pobrane, a zamówienie wygląda na nieopłacone. Jeśli wiersz zamówienia jest utworzony, ale zapas nie został zarezerwowany, możesz sprzedać produkt, którego realnie nie masz.

Podobnie psuje się onboarding użytkownika. Tworzenie użytkownika, wstawienie profilu, przypisanie ról i zapis, że trzeba wysłać e‑mail powitalny to jedna logiczna akcja. Bez grupowania możesz mieć użytkownika, który się może zalogować, ale nie ma uprawnień, albo profil bez powiązanego użytkownika.

Działania back-office często potrzebują ścisłego zachowania „papierowy ślad + zmiana stanu”. Zatwierdzenie wniosku, zapisanie wpisu audytowego i aktualizacja salda powinny się udać razem. Jeśli saldo się zmieniło, a log audytu zaginął, tracisz dowód, kto i dlaczego wprowadził zmianę.

Prace w tle też na tym zyskują, zwłaszcza gdy przetwarzasz element roboczy wieloetapowo: zarezerwuj element, żeby dwóch workerów go nie przetworzyło, zastosuj zmianę biznesową, zapisz wynik do raportowania i retry, a potem oznacz element jako wykonany (lub niepowodzenie z powodem). Jeśli te kroki się rozjadą, retry i współbieżność zrobią bałagan.

Zaprojektuj przepływ zanim napiszesz SQL

Wieloetapowe funkcje psują się, gdy traktujesz je jak stertę niezależnych aktualizacji. Zanim otworzysz klienta bazy, napisz przepływ jako krótką historię z jednym wyraźnym punktem końcowym: co dokładnie liczy się jako „zrobione” dla użytkownika?

Zacznij od wypisania kroków prostym językiem, potem zdefiniuj jedną warunkującą się definicję sukcesu. Na przykład: „Zamówienie jest utworzone, zapas zarezerwowany, a użytkownik widzi numer potwierdzenia zamówienia.” Wszystko krótsze niż to to nie jest sukces, nawet jeśli niektóre tabele zostały zaktualizowane.

Następnie oddziel twardą linią pracę bazodanową od pracy zewnętrznej. Kroki bazodanowe to te, które możesz chronić transakcjami. Wywołania zewnętrzne jak płatności kartą, wysyłka maili czy API stron trzecich zawodzą wolno i nieprzewidywalnie i zwykle nie możesz ich cofnąć.

Proste podejście planistyczne: podziel kroki na (1) muszą być wszystko-albo-nic, (2) mogą się zdarzyć po commicie.

Zdecyduj, co należy do transakcji

W transakcji trzymaj tylko kroki, które muszą być razem spójne:

  • Tworzenie lub aktualizacja kluczowych wierszy (zamówienie, faktura, saldo konta)
  • Rezerwowanie współdzielonych zasobów (zapas, miejsca, quota)
  • Zapis trwałego zdarzenia „co zrobić dalej” (tabela outbox)
  • Wymuszanie reguł ograniczeniami (klucze unikatowe, klucze obce)

Przenieś skutki uboczne na zewnątrz. Na przykład: commituj zamówienie, a potem wyślij maila potwierdzającego na podstawie rekordu outbox.

Napisz oczekiwania rollbacku dla każdego kroku

Dla każdego kroku ustal, co powinno się stać, gdy następny krok zawiedzie. „Rollback” może znaczyć wycofanie transakcji lub wykonanie działania kompensującego.

Przykład: jeśli płatność powiedzie się, ale rezerwacja zasobów nie, ustal z góry, czy od razu zwracasz środki, czy oznaczasz zamówienie jako „płatność pobrana, oczekuje na zapas” i obsługujesz to asynchronicznie.

Krok po kroku: opakowanie przepływu w transakcję

Transakcja mówi Postgresowi: traktuj te kroki jako jedną jednostkę. Albo wszystkie zajdą, albo żaden. To najprostszy sposób, by zapobiec częściowym zapisom.

Podstawowy przebieg

Użyj jednego połączenia z bazą (jednej sesji) od początku do końca. Jeśli rozdzielisz kroki na różne połączenia, Postgres nie będzie w stanie zagwarantować wyniku wszystko‑albo‑nic.

Sekwencja jest prosta: BEGIN, wykonaj potrzebne odczyty i zapisy, COMMIT jeśli wszystko się uda, w przeciwnym razie ROLLBACK i zwróć czytelny błąd.

Oto minimalny przykład w SQL:

BEGIN;

-- reads that inform your decision
SELECT balance FROM accounts WHERE id = 42 FOR UPDATE;

-- writes that must stay together
UPDATE accounts SET balance = balance - 50 WHERE id = 42;
INSERT INTO ledger(account_id, amount, note) VALUES (42, -50, 'Purchase');

COMMIT;

-- on error (in code), run:
-- ROLLBACK;

Trzymaj transakcje krótkie (i łatwe do debugowania)

Transakcje utrzymują blokady podczas działania. Im dłużej są otwarte, tym bardziej blokujesz inne prace i rośnie prawdopodobieństwo timeoutów lub deadlocków. Wykonuj w transakcji to, co niezbędne, a przenoś wolne zadania (wysyłka maili, wywołania providerów płatności, generowanie PDF) poza nią.

Gdy coś zawiedzie, loguj wystarczający kontekst, by odtworzyć problem bez wycieku danych wrażliwych: nazwa przepływu, order_id lub user_id, kluczowe parametry (kwota, waluta) oraz kod błędu Postgresa. Unikaj logowania pełnych payloadów, danych kart czy szczegółów osobowych.

Podstawy współbieżności: blokady i izolacja bez żargonu

Zamień kroki w plan
Użyj Planning Mode do odwzorowania kroków, invariants i ścieżek błędów przed napisaniem SQL.

Współbieżność to po prostu dwa zdarzenia zachodzące jednocześnie. Wyobraź sobie dwóch klientów próbujących kupić ostatni bilet. Oba ekrany pokazują „1 pozostało”, obie osoby klikają Zapłać i twoja aplikacja musi zdecydować, kto dostanie bilet.

Bez ochrony oba żądania mogą odczytać tę samą starą wartość i obie zapisać aktualizację. Tak powstaje ujemny zapas, zduplikowane rezerwacje czy płatność bez zamówienia.

Blokady wierszy to najprostsza ochrona. Zablokujesz konkretny wiersz, który chcesz zmienić, wykonasz swoje sprawdzenia, a potem go zaktualizujesz. Inne transakcje dotykające tego samego wiersza muszą poczekać aż ty commitujesz lub rollbackujesz, co zapobiega podwójnym aktualizacjom.

Typowy wzorzec: rozpocznij transakcję, wybierz wiersz inventory z FOR UPDATE, sprawdź, czy jest zapas, zmniejsz go, a potem wstaw zamówienie. To „trzyma drzwi” podczas krytycznych kroków.

Poziomy izolacji kontrolują, ile nieoczekiwanych nakładań ze współbieżnych transakcji tolerujesz. Zwykle to kompromis między bezpieczeństwem a wydajnością:

  • Read Committed (domyślny): szybki, ale widzisz zmiany commitowane przez innych między zapytaniami.
  • Repeatable Read: twoja transakcja widzi stabilny snapshot, dobry do spójnych odczytów, może powodować więcej retry.
  • Serializable: najsilniejsze bezpieczeństwo, Postgres może przerwać jedną transakcję, by zachować efekt, jakby wykonywały się sekwencyjnie.

Trzymaj blokady krótkie. Jeśli transakcja siedzi otwarta podczas wywołania zewnętrznego API lub czekania na akcję użytkownika, stworzysz długie oczekiwania i time‑outy. Wolisz jasną ścieżkę awaryjną: ustaw timeout blokady, obsłuż błąd i zwróć „proszę spróbować ponownie”, zamiast pozwalać, by żądania wisiały.

Jeśli musisz wykonać pracę poza bazą (np. obciążyć kartę), rozdziel przepływ: zarezerwuj szybko, commituj, potem zrób wolny krok i zakończ krótką transakcją.

Retry, które nie tworzą duplikatów

Retry to normalna rzecz w aplikacjach opartych o Postgresa. Żądanie może się nie powieść nawet, gdy kod jest poprawny: deadlocki, time‑outy zapytań, krótkie przerwy sieciowe lub błąd serializacji przy wyższych poziomach izolacji. Jeśli po prostu uruchomisz handler ponownie, ryzykujesz stworzenie drugiego zamówienia, podwójne obciążenie karty lub wstawienie zduplikowanych wierszy "zdarzenie".

Rozwiązaniem jest idempotencja: operacja powinna być bezpieczna do uruchomienia dwa razy z tymi samymi danymi. Baza powinna być w stanie rozpoznać „to to samo żądanie” i zareagować spójnie.

Praktyczny wzorzec: dołącz klucz idempotencyjny (często generowany po stronie klienta request_id) do każdego wieloetapowego przepływu i zapisz go na głównym rekordzie, potem dodaj na niego ograniczenie unikatowości.

Na przykład: w checkout wygeneruj request_id, gdy użytkownik kliknie Zapłać, potem wstaw zamówienie z tym request_id. Jeśli nastąpi retry, drugie żądanie trafi w ograniczenie unikatowości i zwrócisz istniejące zamówienie zamiast tworzyć nowe.

Co zwykle się liczy:

  • Użyj ograniczenia unikatowego na (request_id) lub (user_id, request_id), by zablokować duplikaty.
  • Gdy ograniczenie zostanie naruszone, pobierz istniejący wiersz i zwróć ten sam wynik.
  • Efekty uboczne też muszą respektować tę regułę: tylko jeden intent płatniczy na zamówienie, tylko jedno zdarzenie "order_confirmed" na zamówienie.
  • Loguj request_id, by wsparcie mogło śledzić, co się stało.

Trzymaj pętlę retry poza transakcją. Każda próba powinna zaczynać świeżą transakcję i odtwarzać jednostkę pracy od początku. Retry wewnątrz nieudanej transakcji nie pomaga, bo Postgres oznacza ją jako aborted.

Mały przykład: aplikacja próbuje utworzyć zamówienie i zarezerwować zapas, ale wystąpił timeout tuż po COMMIT. Klient ponawia żądanie. Z kluczem idempotencyjnym drugie żądanie zwróci już utworzone zamówienie i nie zdubluje rezerwacji.

Wykorzystaj bazę do wymuszania reguł, nie tylko kod

Zaprojektuj bezpieczny przepływ
Najpierw zaplanuj granice transakcji, potem wygeneruj kod React i Go z chatu.

Transakcje trzymają wieloetapowy przepływ razem, ale nie robią danych poprawnymi za Ciebie. Silny sposób, by uniknąć skutków częściowych zapisów, to sprawić, że „błędne” stany będą trudne lub niemożliwe do uzyskania w bazie, nawet jeśli błąd w aplikacji się zdarzy.

Zacznij od podstawowych zabezpieczeń. Klucze obce upewniają, że referencje są prawdziwe (pozycja zamówienia nie może wskazywać na brakujące zamówienie). NOT NULL zatrzymuje półwypełnione wiersze. CHECK constraints łapią wartości, które nie mają sensu (np. quantity > 0, total_cents >= 0). Te reguły działają przy każdym zapisie, bez względu na to, która usługa lub skrypt modyfikuje bazę.

Dla dłuższych przepływów modeluj zmiany stanu wprost. Zamiast wielu booleani użyj jednego pola status (pending, paid, shipped, canceled) i pozwól tylko na legalne przejścia. Możesz to wymusić ograniczeniami lub triggerami, żeby baza odrzucała nielegalne przeskoki, np. shipped -> pending.

Unikalność to kolejna forma poprawności. Dodaj ograniczenia unikatowości tam, gdzie duplikaty złamią przepływ: order_number, invoice_number czy idempotency_key użyty dla retry. Wtedy, jeśli aplikacja spróbuje ponownie to samo żądanie, Postgres zablokuje drugi insert i możesz bezpiecznie zwrócić "już przetworzone" zamiast tworzyć drugie zamówienie.

Gdy potrzebujesz śledzenia, zapisuj je jawnie. Tabela audytu (lub historia) z informacją kto co zmienił i kiedy zmienia „tajemnicze aktualizacje” w fakty, które możesz zapytać podczas incydentów.

Częste błędy prowadzące do częściowych zapisów

Większość częściowych zapisów nie bierze się z „złego SQL”. Wynika z decyzji w projekcie przepływu, które pozwalają zapisać tylko połowę historii.

Pułapki spotykane w realnych aplikacjach

  • Robienie wolnej pracy zewnętrznej podczas otwartej transakcji. Wywołanie providera płatności, wysyłka maila czy wysyłka pliku wewnątrz transakcji trzyma blokady dłużej niż trzeba. Jeśli API jest wolne lub timeouotuje, inni użytkownicy stoją w kolejce za twoją otwartą transakcją.
  • Odczyty poza transakcją, a potem zapisy później. Przykład: pobierasz balance użytkownika, pokazujesz go na ekranie, a potem odliczasz bazując na tej starej wartości. Inna sesja mogła zmienić saldo w międzyczasie.
  • Łapanie błędu, ale mimo to commitowanie czegoś. Częsty wzorzec: "spróbuj krok 1, spróbuj krok 2, zaloguj błąd, zwróć sukces mimo wszystko." Jeśli kod osiągnie COMMIT po błędzie, świadomie robisz bazę niespójną.
  • Aktualizowanie tabel w różnych porządkach w różnych ścieżkach kodu. Jeśli jedno żądanie aktualizuje accounts potem orders, a inne najpierw orders potem accounts, rośnie ryzyko deadlocków przy obciążeniu.
  • Trzymanie transakcji otwartych zbyt długo. Długie transakcje mogą blokować zapisy, opóźniać sprzątanie vacuum i powodować mylące timeouty.

Konkretny przykład: w checkout rezerwujesz zapas, tworzysz zamówienie, a potem obciążasz kartę. Jeśli obciążysz kartę wewnątrz tej samej transakcji, możesz trzymać blokadę inventory czekając na sieć. Jeśli obciążenie się powiedzie, a później transakcja wycofa się, obciążyłeś klienta bez zamówienia.

Bezpieczniejszy wzorzec: trzymaj transakcję skupioną na stanie bazy (rezerwuj zapas, twórz zamówienie, zapisz próbę płatności jako pending), commituj, potem wykonaj zewnętrzne API, a wynik zapisz w nowej krótkiej transakcji. Wiele zespołów realizuje to przez status pending i zadanie w tle.

Krótka lista kontrolna dla operacji wszystko‑albo‑nic

Gdy przepływ ma wiele kroków (insert, update, charge, send), cel jest prosty: albo wszystko jest zapisane, albo nic.

Granice transakcji

Trzymaj wszystkie wymagane zapisy bazodanowe w jednej transakcji. Jeśli któryś krok zawiedzie, rollback i przywróć dane do stanu sprzed.

Uczyń warunek sukcesu explicite. Na przykład: „Zamówienie jest utworzone, zapas zarezerwowany, a status płatności zapisany.” Wszystko inne to ścieżka błędu, która powinna przerwać transakcję.

  • Wszystkie wymagane zapisy wykonuje się w jednym bloku BEGIN ... COMMIT.
  • W bazie jest jedno jasne „zrobione” (nie tylko w pamięci aplikacji).
  • Każdy błąd prowadzi do ROLLBACK, a wywołujący dostaje czytelny komunikat o niepowodzeniu.

Szyny bezpieczeństwa (by retry nie szkodziły)

Zakładaj, że to samo żądanie może być powtórzone. Baza powinna pomóc wymusić reguły wykonania tylko raz.

  • Wspieraj działania „tylko raz” ograniczeniami unikatowości (jedna płatność na zamówienie, jedna rezerwacja na przedmiot i zamówienie).
  • Spraw, by powtórzenia były bezpieczne i powtarzalne (te same dane dają ten sam stan końcowy, a nie duplikaty).

Trzymaj transakcje krótkie

Wykonuj minimalną pracę w transakcji i unikaj czekania na wywołania sieciowe podczas trzymania blokad.

  • Trzymaj transakcje krótkie i ustaw timeout, żeby nie wisiały.
  • Wykonywanie wolnej pracy (np. kontakt z providerem płatności) rób poza transakcją, potem zapisz wynik w nowej krótkiej transakcji.

Obserwuj błędy

Jeśli nie widzisz, gdzie się psuje, będziesz zgadywać.

  • Loguj krok przepływu i request id przy każdym błędzie.
  • Monitoruj wskaźniki rollbacków i timeoutów blokad, żeby wcześnie łapać ryzyko częściowych zapisów.

Przykład: checkout, który pozostaje spójny przy awariach

Prototypuj funkcje wieloetapowe
Zbuduj przepływ checkout lub onboarding z jasnym zachowaniem commit i rollback.

Checkout ma kilka kroków, które powinny przesuwać się razem: utworzyć zamówienie, zarezerwować zapas, zapisać próbę płatności, a potem oznaczyć status zamówienia.

Wyobraź sobie, że użytkownik klika Kup dla 1 przedmiotu.

Bezpieczny przebieg (praca w DB jako jedna jednostka)

W jednej transakcji wykonaj tylko zmiany w bazie:

  • Wstaw wiersz orders ze statusem pending_payment.
  • Zarezerwuj zapas (np. zmniejsz inventory.available lub utwórz wiersz reservations).
  • Wstaw wiersz payment_intents z kluczem idempotencyjnym dostarczonym przez klienta (unikalnym).
  • Wstaw wiersz do outbox typu "order_created".

Jeśli któreś polecenie zawiedzie (brak zapasu, błąd ograniczenia, awaria), Postgres wycofa całą transakcję. Nie skończysz z zamówieniem bez rezerwacji ani z rezerwacją bez zamówienia.

Co jeśli płatność zawiedzie w połowie?

Dostawca płatności jest poza twoją bazą, więc traktuj go jako krok oddzielny.

Jeśli wywołanie providera nie powiedzie się przed commitem, abortuj transakcję i nic nie jest zapisane. Jeśli wywołanie zawiedzie po commicie, uruchom nową transakcję, która oznaczy próbę płatności jako nieudaną, zwolni rezerwację i ustawi status zamówienia na canceled.

Retry bez tworzenia drugiego zamówienia

Niech klient wysyła idempotency_key dla próby checkoutu. Wymuś to unikalnym indeksem na payment_intents(idempotency_key) (albo na orders, jeśli wolisz). Przy retry kod pobiera istniejące wiersze i kontynuuje zamiast wstawiać nowe zamówienie.

Emaile i powiadomienia

Nie wysyłaj maili wewnątrz transakcji. Zapisz rekord outbox w tej samej transakcji, a potem worker w tle wyśle maila po commicie. Dzięki temu nigdy nie wyślesz maila dla zamówienia, które zostało wycofane.

Następne kroki: zastosuj to w jednym przepływie w tym tygodniu

Wybierz jeden przepływ, który dotyka więcej niż jednej tabeli: signup + kolejka maila powitalnego, checkout + zapas, faktura + wpis w ledgerze czy utworzenie projektu + domyślne ustawienia.

Najpierw napisz kroki, potem reguły, które zawsze muszą być prawdziwe (twoje invariants). Przykład: „Zamówienie jest albo w pełni opłacone i zarezerwowane, albo nieopłacone i niezarezerwowane. Nigdy połowicznie.” Zamień te reguły w jednostkę wszystko‑albo‑nic.

Prosty plan:

  • Wypisz dokładne operacje SQL w kolejności (odczyty, inserty, update, delete).
  • Dodaj brakujące ograniczenia w bazie (unikatowe klucze, klucze obce, CHECK).
  • Dodaj klucz idempotencyjny dla requestu, by retry nie tworzyły duplikatów.
  • Owiń kroki w jedną transakcję i upewnij się, że punkt sukcesu jest jawny (commit tylko, gdy wszystkie sprawdzenia przejdą).
  • Zdecyduj, jak wygląda bezpieczny retry (ten sam idempotency key, ten sam rezultat).

Potem testuj celowo złe przypadki. Symuluj awarię po kroku 2, timeout tuż przed commitem i podwójne wysłanie z UI. Celem są nudne wyniki: brak osieroconych wierszy, brak podwójnych obciążeń, nic nie wiszące w stanie pending na zawsze.

Jeśli prototypujesz szybko, pomaga szkicować przepływ w narzędziu planistycznym zanim wygenerujesz handlery i schemat. Na przykład, Koder.ai (koder.ai) ma Planning Mode i wspiera snapshoty oraz rollback, co bywa przydatne podczas iteracji nad granicami transakcji i ograniczeniami.

Zrób to dla jednego przepływu w tym tygodniu. Drugi pójdzie znacznie szybciej.

Często zadawane pytania

Kiedy należy użyć transakcji w Postgresie?

Użyj transakcji, gdy jedno działanie użytkownika zmienia kilka wierszy lub tabel, które muszą zachować spójność. Typowe przykłady to finalizacja zakupu, przelewy między kontami, rejestracja z przypisaniem roli oraz rezerwacje zapasów.

Co robi COMMIT w Postgresie?

Zatwierdzenie sprawia, że wszystkie zmiany w transakcji stają się trwałe i widoczne dla innych sesji bazy danych. Zatwierdzaj transakcję dopiero po pomyślnym wykonaniu wszystkich wymaganych kontroli i zapisów.

Co się dzieje po wykonaniu ROLLBACK?

Wycofanie anuluje transakcję i usuwa jej niezatwierdzone zmiany. Kod powinien wycofać transakcję po niepowodzeniu dowolnego wymaganego kroku, a następnie zwrócić błąd, który wywołujący może obsłużyć.

Czy wszystkie zapytania w transakcji muszą używać tego samego połączenia?

Odczyty i zapisy bazy danych w tym przepływie powinny korzystać z tego samego połączenia z bazą, od BEGIN do COMMIT lub ROLLBACK. Osobne połączenia tworzą osobne transakcje.

Czy należy wywoływać API płatności lub e-maili wewnątrz transakcji?

Nie. Transakcje utrzymują zmiany w bazie danych razem, lecz nie cofają obciążenia karty, wysłanego e-maila ani wywołania zewnętrznego API. Najpierw zatwierdź stan bazy danych, zapisz zdarzenie outboxa lub status oczekujący, a potem wykonaj pracę zewnętrzną osobno.

Jak zapobiec kupieniu ostatniej sztuki przez dwóch użytkowników?

Zablokuj wiersz przed sprawdzeniem i aktualizacją współdzielonej wartości, takiej jak dostępny stan magazynowy. Na przykład wybierz wiersz zapasów za pomocą FOR UPDATE, sprawdź stan, zmniejsz go i utwórz zamówienie przed zatwierdzeniem transakcji.

Jak ponowne próby mogą zapobiegać duplikowaniu zamówień?

Nadaj każdemu żądaniu stały klucz idempotencji i wymuś jego unikalność za pomocą ograniczenia unikalności. Gdy ponowna próba naruszy to ograniczenie, wczytaj i zwróć istniejący wynik zamiast dodawać kolejne zamówienie lub rekord płatności.

Dlaczego ponowne próby muszą rozpoczynać nową transakcję?

Ponów próbę od początku w nowej transakcji. Po błędzie instrukcji Postgres oznacza bieżącą transakcję jako przerwaną, więc nie można jej bezpiecznie kontynuować, dopóki jej nie wycofasz.

Które ograniczenia bazy danych pomagają zapobiegać niespójnym danym?

Używaj kluczy obcych dla prawidłowych odwołań, NOT NULL dla wymaganych pól, ograniczeń CHECK dla prawidłowych wartości oraz ograniczeń unikalności dla rekordów, które mogą istnieć tylko raz. Te reguły chronią dane, nawet jeśli kod aplikacji zawiera błąd.

Dlaczego transakcje powinny być krótkie?

Transakcje powinny obejmować tylko szybkie operacje na bazie danych. Długie transakcje utrzymują blokady, wydłużają czas oczekiwania i mogą powodować zakleszczenia lub przekroczenia limitu czasu, zwłaszcza gdy czekają na wywołanie sieciowe.

Related posts