7 min

Warunki wyścigu w aplikacjach CRUD: przyczyny i praktyczne rozwiązania

Warunki wyścigu w aplikacjach CRUD mogą powodować zdublowane zamówienia i błędne sumy. Poznaj typowe punkty kolizji i praktyczne poprawki: ograniczenia, blokady i zabezpieczenia w UI.

Warunki wyścigu w aplikacjach CRUD: przyczyny i praktyczne rozwiązania

Jak wygląda warunek wyścigu w aplikacji CRUD

Warunek wyścigu występuje, gdy dwa (lub więcej) żądań aktualizuje te same dane niemal w tym samym czasie, a ostateczny rezultat zależy od kolejności i timingu. Każde żądanie samo w sobie wygląda poprawnie. Razem dają niepoprawny wynik.

Prosty przykład: dwie osoby klikają Zapisz na tym samym rekordzie klienta w ciągu sekundy. Jedna zmienia e‑mail, druga numer telefonu. Jeśli oba żądania wysyłają cały rekord, drugi zapis może nadpisać pierwszy i jedna zmiana znika bez błędu.

Spotkasz to częściej w szybkich aplikacjach, bo użytkownicy wywołują więcej akcji na minutę. Szczyty występują też przy dużym natężeniu: wyprzedaże, koniec miesiąca, duże kampanie e‑mailowe lub każdy moment, gdy backlog żądań trafia w te same wiersze.

Użytkownicy rzadko zgłaszają „warunek wyścigu”. Zgłaszają objawy: zdublowane zamówienia lub komentarze, brakujące aktualizacje ("zapisane, ale wróciło do poprzedniego"), dziwne sumy (stan magazynu idzie poniżej zera, liczniki cofną się) lub statusy, które niespodziewanie się odwracają (zatwierdzone, potem znów oczekujące).

Powtórzenia pogarszają sprawę. Ludzie dwukrotnie klikają, odświeżają po wolnej odpowiedzi, wysyłają z dwóch kart albo mają niestabilne sieci, które ponawiają żądania. Jeśli serwer traktuje każde żądanie jak świeży zapis, możesz dostać dwa create, dwie płatności lub dwa przejścia statusu, które miały być wykonane raz.

Dlaczego warunki wyścigu zdarzają się częściej niż myślisz

Większość aplikacji CRUD wydaje się prosta: czytaj wiersz, zmień pole, zapisz. Problem w tym, że aplikacja nie kontroluje timingu. Baza danych, sieć, retry, praca w tle i zachowanie użytkownika nachodzą na siebie.

Częstym wyzwalaczem są dwie osoby edytujące ten sam rekord. Obie ładują te same „bieżące” wartości, obie dokonują poprawnych zmian i ostatni zapis cicho nadpisuje pierwszy. Nikt nie zrobił nic złego, ale jedna aktualizacja przepadła.

Dzieje się to też u jednej osoby. Podwójne kliknięcie Przycisnij Zapisz, szybkie przejście przód‑tył albo wolne połączenie, które powoduje ponowne naciśnięcie Wyślij, może wysłać ten sam zapis dwukrotnie. Jeśli endpoint nie jest idempotentny, dostaniesz duplikaty, podwójne obciążenia lub przeskok statusu o dwa kroki.

Nowoczesne użycie dodaje więcej nakładania się. Kilka kart lub urządzeń zalogowanych na to samo konto może generować sprzeczne aktualizacje. Zadania w tle (mailingi, billing, synchronizacja, sprzątanie) mogą dotykać tych samych wierszy co żądania webowe. Automatyczne retry po stronie klienta, load balancera czy runnera zadań mogą powtórzyć już zakończone żądanie.

Jeżeli szybko dostarczasz funkcje, ten sam rekord często jest aktualizowany z większej liczby miejsc niż ktokolwiek pamięta. Jeśli używasz narzędzia chatowego jak Koder.ai, aplikacja może rosnąć jeszcze szybciej — warto traktować współbieżność jako normalne zachowanie, a nie przypadek brzegowy.

Typowe punkty kolizji, na które warto uważać

Warunki wyścigu rzadko wychodzą w demo "stworzyć rekord". Pojawiają się tam, gdzie dwa żądania dotykają tej samej prawdy w niemal tym samym momencie. Znajomość punktów zapalnych pomaga projektować bezpieczne zapisy od pierwszego dnia.

Liczniki i pola "następny numer"

Wszystko, co wygląda jak "po prostu dodaj 1", może się zepsuć pod obciążeniem: polubienia, liczby wyświetleń, sumy, numery faktur, numery biletów. Ryzykowny wzorzec to odczytać wartość, dodać, a potem zapisać z powrotem. Dwa żądania mogą odczytać tę samą wartość początkową i nadpisać się nawzajem.

Przejścia statusów

Workflowy jak Draft -> Submitted -> Approved -> Paid wyglądają prostolinijnie, ale kolizje są powszechne. Problem zaczyna się, gdy dwie akcje są możliwe jednocześnie (zatwierdź i edytuj, anuluj i zapłać). Bez zabezpieczeń możesz dostać rekord, który pomija kroki, cofa się albo pokazuje różne stany w różnych tabelach.

Traktuj zmiany statusu jak kontrakt: pozwalaj tylko na następny poprawny krok i odrzucaj wszystko inne.

Zapasy, pojemność i sloty rezerwacji

Dostępne miejsca, stany magazynowe, terminy wizyt i pola "pojemność pozostała" tworzą klasyczny problem oversell. Dwóch kupujących zobaczy dostępność i obaj sfinalizują zakup. Jeśli baza danych nie jest ostatecznym sędzią, w końcu sprzedasz więcej niż masz.

Unikalność i reguły "tylko jedna aktywna rzecz"

Niektóre reguły są absolutne: jeden e‑mail na konto, jedna aktywna subskrypcja na użytkownika, jeden otwarty koszyk na użytkownika. Często zawodzą, gdy najpierw sprawdzasz ("czy istnieje?") a potem wstawiasz. Pod obciążeniem oba żądania mogą przejść sprawdzenie.

Jeśli szybko generujesz przepływy CRUD (na przykład poprzez chat z Koder.ai), zanotuj te punkty i zabezpiecz je ograniczeniami i bezpiecznymi zapisami, a nie tylko sprawdzeniami w UI.

Podwójne wysyłanie i powtarzane żądania z UI

Wiele warunków wyścigu zaczyna się od czegoś trywialnego: ta sama akcja jest wysyłana dwa razy. Użytkownicy dwukrotnie klikają. Sieć jest wolna, więc klikają ponownie. Telefon zarejestruje dwa tapnięcia. Czasem to nieintencjonalne: strona odświeża się po POST i przeglądarka proponuje ponowne wysłanie.

Gdy to następuje, backend może równolegle wykonać dwa create lub update. Jeśli oba się powiodą, masz duplikaty, błędne sumy lub zmianę statusu wykonaną dwukrotnie (np. approve i jeszcze jedno approve). Wygląda to losowo, bo zależy od timingu.

Najbezpieczniejsze podejście to obrona w głębi. Napraw UI, ale zakładaj, że UI zawiedzie.

Praktyczne zmiany, które możesz zastosować do większości ścieżek zapisu:

  • Wyłącz możliwość ponownego wysłania przy pierwszym kliknięciu i pokaż wyraźny stan "zapisywanie".
  • Przekieruj po udanym POST, aby odświeżenie nie wysyłało tego samego formularza ponownie.
  • Użyj klucza idempotency (idempotency key): unikalnego tokena dla akcji użytkownika, który serwer przechowuje i akceptuje tylko raz.
  • Wspieraj to regułą unikalności w bazie danych, żeby dwa identyczne inserty nie wygrały jednocześnie.

Przykład: użytkownik klika "Opłać fakturę" dwukrotnie na mobilu. UI powinno zablokować drugie tapnięcie. Serwer też powinien odrzucić drugie żądanie po wykryciu tego samego idempotency key i zwrócić oryginalny rezultat sukcesu zamiast obciążać dwukrotnie.

Przejścia statusów, które się rozjeżdżają

Pola statusu wydają się proste, dopóki dwie rzeczy nie próbują ich zmienić jednocześnie. Użytkownik klika Zatwierdź, podczas gdy zadanie automatyczne oznacza ten sam rekord jako Wygasłe, albo dwóch członków zespołu pracuje nad tym samym itemem w różnych kartach. Obie aktualizacje mogą się powieść, ale ostateczny status zależy od timingu, a nie twoich reguł.

Traktuj status jak małą maszynę stanów. Zachowaj krótką tabelę dozwolonych ruchów (np.: Draft -> Submitted -> Approved, oraz Submitted -> Rejected). Potem każdy zapis sprawdza: "Czy ten ruch jest dozwolony z bieżącego statusu?" Jeśli nie, odrzuć zamiast cicho nadpisać.

Optymistyczne blokowanie pomaga wykrywać przestarzałe aktualizacje bez blokowania innych użytkowników. Dodaj numer wersji (lub updated_at) i wymagaj, żeby pasował podczas zapisu. Jeśli ktoś inny zmienił wiersz po twoim odczycie, twój update dotknie 0 wierszy i możesz pokazać jasny komunikat typu: "Ten element się zmienił, odśwież i spróbuj ponownie."

Prosty wzorzec dla aktualizacji statusu:

  • Odczytaj bieżący status i wersję
  • Zwaliduj przejście
  • Zaktualizuj z zabezpieczeniem (WHERE status = ? AND version = ?)
  • Zwiększ numer wersji

Również trzymaj zmiany statusu w jednym miejscu. Jeśli aktualizacje są rozproszone po ekranach, zadaniach w tle i webhookach, przegapisz regułę. Umieść je za jedną funkcją lub endpointem, który za każdym razem egzekwuje te same sprawdzenia przejść.

Liczniki i sumy, które z czasem dryfują

Review and own the code
Wygeneruj aplikację, a następnie eksportuj kod źródłowy, by przejrzeć blokady i granice transakcji.

Najczęstszy błąd z licznikiem wygląda niegroźnie: aplikacja odczytuje wartość, dodaje 1, a potem zapisuje z powrotem. Pod obciążeniem dwa żądania mogą odczytać tę samą liczbę i oba zapisać tę samą nową liczbę, więc jedno inkrementowanie ginie. To łatwo przeoczyć, bo "zwykle działa" w testach.

Preferuj atomowe aktualizacje zamiast odczyt‑modyfikuj‑zapis

Jeśli wartość jest tylko inkrementowana lub dekrementowana, pozwól bazie danych zrobić to jednym zapytaniem. Wtedy baza bezpiecznie zastosuje zmiany nawet gdy wiele żądań uderza jednocześnie.

UPDATE posts
SET like_count = like_count + 1
WHERE id = $1;

Ta sama idea dotyczy zapasów, liczników wyświetleń, liczników retry i wszystkiego, co da się wyrazić jako "new = old + delta".

Sumy się rozjeżdżają, gdy przechowujesz to, co można obliczyć

Sumy często się psują, gdy przechowujesz liczbę pochodną (order_total, account_balance, project_hours) i aktualizujesz ją z wielu miejsc. Jeśli możesz obliczać sumę z wierszy źródłowych (pozycje zamówienia, wpisy księgowe), unikasz całej klasy błędów driftu.

Gdy musisz przechowywać sumę dla wydajności, traktuj to jak krytyczny zapis. Trzymaj aktualizacje wierszy źródłowych i przechowywanej sumy w tej samej transakcji. Zapewnij, że tylko jeden writer może aktualizować tę samą sumę w danym momencie (blokady, guarded updates lub pojedyncza ścieżka właściciela). Dodaj ograniczenia, które zapobiegają niemożliwym wartościom (np. stan magazynu >= 0). Potem okresowo przeprowadzaj rekonsyliację, która przelicza i flaguje niespójności.

Konkretne: dwaj użytkownicy dodają produkty do tego samego koszyka jednocześnie. Jeśli każde żądanie odczytuje cart_total, dodaje cenę i zapisuje z powrotem, jedna pozycja może zniknąć. Jeśli zaktualizujesz pozycje koszyka i total razem w jednej transakcji, total pozostanie poprawny nawet przy silnych, równoległych kliknięciach.

Zatrzymaj kolizje najpierw ograniczeniami bazy danych

Jeśli chcesz mniej warunków wyścigu, zacznij od bazy danych. Kod aplikacji może retry‑ować, timeoutować lub wykonać się dwa razy. Ograniczenie bazy to ostatnia brama, która pozostaje poprawna nawet gdy dwa żądania trafiają w tym samym czasie.

Unikalne ograniczenia zatrzymują duplikaty, które "nigdy nie powinny się zdarzyć" ale się zdarzają: adresy e‑mail, numery zamówień, ID faktur lub reguła "jedna aktywna subskrypcja na użytkownika". Gdy dwa zapisy lądują razem, baza zaakceptuje jeden wiersz i odrzuci drugi.

Klucze obce zapobiegają zerwanym referencjom. Bez nich jedno żądanie może usunąć rekord‑rodzica, podczas gdy inne tworzy dziecko wskazujące na nic, pozostawiając sieroty trudne do sprzątnięcia później.

Check constraints utrzymują wartości w bezpiecznym zakresie i egzekwują proste reguły stanu. Na przykład quantity >= 0, rating między 1 a 5, albo status ograniczony do dozwolonego zbioru.

Traktuj błędy ograniczeń jako spodziewane wyniki, nie "błędy serwera". Łap naruszenia unique, foreign key i check, zwracaj czytelny komunikat typu "Ten e‑mail jest już używany" i loguj szczegóły do debugowania bez wycieku wewnętrznych danych.

Przykład: dwie osoby klikają "Utwórz zamówienie" podczas opóźnienia. Z unique constraint na (user_id, cart_id) nie dostaniesz dwóch zamówień. Dostaniesz jedno zamówienie i jedno czyste, wytłumaczalne odrzucenie.

Używaj transakcji i blokad, gdy jedna zmiana musi wygrać

Niektóre zapisy to nie pojedyncze polecenie. Odczytujesz wiersz, sprawdzasz regułę, aktualizujesz status i może dodajesz wpis audytu. Jeśli dwa żądania zrobią to równocześnie, oba mogą przejść sprawdzenie i oba zapisać. To klasyczny wzorzec awarii.

Opakuj wieloetapowy zapis w jednej transakcji, żeby wszystkie kroki powiodły się razem albo nie powiodły wcale. Co ważniejsze, transakcja daje miejsce do kontrolowania, kto może zmieniać te same dane w tym samym czasie.

Gdy tylko jeden aktor może edytować rekord w danym momencie, użyj blokady na poziomie wiersza. Na przykład: zablokuj wiersz zamówienia, potwierdź, że wciąż jest w stanie "pending", potem przestaw na "approved" i zapisz wpis audytu. Drugie żądanie poczeka, potem ponownie sprawdzi stan i zatrzyma się.

Optymistyczne vs pesymistyczne blokowanie

Wybierz w zależności od częstotliwości kolizji:

  • Optymistyczne blokowanie: dodaj numer wersji (lub updated_at) i aktualizuj tylko jeśli pasuje.
  • Pesymistyczne blokowanie: zablokuj wiersz najpierw, potem czytaj i aktualizuj wewnątrz tej samej transakcji.

Trzymaj czas trwania blokady krótko. Rób jak najmniej pracy podczas jej trzymania: żadnych zewnętrznych wywołań API, żadnych wolnych operacji plikowych, żadnych dużych pętli. Jeśli budujesz flowy w narzędziu takim jak Koder.ai, trzymaj transakcję tylko na kroki bazodanowe, a resztę rób po commit.

Krok po kroku: utwardź jedną ścieżkę zapisu end‑to‑end

Change safely with rollback
Wdróż nowe ograniczenia z pewnością, używając snapshotów i rollbacku, jeśli pojawi się nieoczekiwany przypadek brzegowy.

Wybierz jedną ścieżkę, która może kosztować pieniądze lub zaufanie przy kolizji. Częsty przykład: utwórz zamówienie, zarezerwuj towar, potem ustaw status zamówienia na potwierdzone.

Spisz dokładne kroki, które dziś wykonuje twój kod, w kolejności. Bądź konkretny co jest czytane, co zapisywane i co oznacza "sukces". Kolizje chowają się w przerwie między odczytem a późniejszym zapisem.

Ścieżka utwardzania, która działa w większości stacków:

  • Zdefiniuj złe wyniki, które musisz zablokować (np. stan magazynu poniżej 0 albo dwie rezerwacje na ten sam order line).
  • Dodaj regułę w bazie, która czyni taki wynik niemożliwym (np. CHECK constraint zabraniający stock < 0 i UNIQUE constraint na (order_id, sku) dla rezerwacji).
  • Opakuj cały flow w jedną transakcję i zablokuj wiersze, które decydują o wyniku (np. zablokuj wiersz inventory produktu przed odjęciem stanu).
  • Wykonaj aktualizację w jednym zapytaniu gdy to możliwe (np.: "update stock where stock >= qty" żeby się bezpiecznie nie powiodło).
  • Obsłuż konflikty czysto: retrynij kilka razy albo zwróć jasny komunikat "Brak w magazynie, odśwież proszę".

Dodaj jeden test, który potwierdzi poprawkę. Uruchom dwa równoległe żądania przeciwko temu samemu produktowi i ilości. Asserty, że dokładnie jedno zamówienie zostaje potwierdzone, a drugie niepowoduje się w kontrolowany sposób (brak ujemnego stanu, brak zduplikowanych wierszy rezerwacji).

Nawet jeśli generujesz aplikacje szybko (również z platform jak Koder.ai), ta checklista jest warta zastosowania na kilku ścieżkach zapisu, które mają największe znaczenie.

Typowe błędy, które utrzymują warunki wyścigu przy życiu

Jedną z największych przyczyn jest zaufanie do UI. Wyłączone przyciski i sprawdzenia po stronie klienta pomagają, ale użytkownicy mogą dwukrotnie kliknąć, odświeżyć, otworzyć dwie karty lub odtworzyć żądanie z niestabilnego połączenia. Jeśli serwer nie jest idempotentny, duplikaty przedostaną się.

Inny cichy błąd: łapiesz błąd bazy (np. naruszenie unique), ale kontynuujesz workflow mimo to. To często kończy się sytuacją "tworzenie nie powiodło się, ale i tak wysłaliśmy e‑mail" albo "płatność nie powiodła się, ale oznaczyliśmy zamówienie jako opłacone". Gdy wystąpią skutki uboczne, trudno je cofnąć.

Długie transakcje też są pułapką. Jeśli trzymasz transakcję otwartą podczas wysyłania maili, płatności czy wywołań zewnętrznych, trzymasz blokady dłużej niż trzeba. To zwiększa oczekiwanie, time‑outy i szansę, że żądania będą się blokować wzajemnie.

Mieszanie zadań w tle i akcji użytkownika bez jednej źródłowej prawdy tworzy stan split‑brain. Job robi retry i aktualizuje wiersz, podczas gdy użytkownik go edytuje i teraz obie strony myślą, że były ostatnim zapisem.

Kilka "poprawek", które w praktyce nie rozwiązują problemu:

  • Naprawy tylko w UI do zapobiegania podwójnemu wysłaniu
  • Tłumienie błędu i kontynuowanie jakby nic się nie stało
  • Wykonywanie wolnych wywołań sieciowych wewnątrz transakcji
  • Pozwalanie jobom i żądaniom użytkownika aktualizować ten sam stan na różne sposoby
  • Aktualizowanie liczników przez odczyt‑modyfikuj‑zapis zamiast atomowych aktualizacji

Jeśli budujesz z narzędziem chat‑to‑app jak Koder.ai, te same reguły obowiązują: proś o zabezpieczenia po stronie serwera i jasne granice transakcyjne, nie tylko ładniejsze zabezpieczenia w UI.

Szybkie kontrole przed wypuszczeniem

Plan concurrency upfront
Mapuj każdą zmianę statusu i idempotentny zapis krok po kroku zanim wygenerujesz kod.

Warunki wyścigu często wychodzą tylko przy prawdziwym ruchu. Przejście pre‑release może złapać najczęstsze punkty kolizji bez przepisywania wszystkiego.

Zacznij od bazy danych. Jeśli coś musi być unikalne (e‑maile, numery faktur, jedna aktywna subskrypcja na użytkownika), zrób z tego realny unique constraint, nie regułę sprawdzaną tylko przez aplikację. Potem upewnij się, że kod spodziewa się, iż constraint czasami zawiedzie i zwraca jasną, bezpieczną odpowiedź.

Następnie spójrz na stany. Każda zmiana statusu (Draft -> Submitted -> Approved) powinna być walidowana względem jawnego zestawu dozwolonych przejść. Jeśli dwa żądania próbują przesunąć ten sam rekord, drugie powinno zostać odrzucone albo stać się no‑op, a nie generować stanu pośredniego.

Praktyczna lista kontrolna przed wydaniem:

  • Potwierdź, że istnieją unique constraints dla każdej reguły "musi być unikalne", także kompozytowe.
  • Upewnij się, że każdy endpoint zapisu jest bezpieczny do ponowienia (klucze idempotency, request ID lub naturalna idempotencja).
  • Opakuj wieloetapowe zapisy w transakcję, zwłaszcza gdy czytasz, a potem zapisujesz w oparciu o odczyt.
  • Używaj blokad tylko tam, gdzie jedna zmiana musi wygrać (np. jednorazowe zatwierdzenie zamówienia).
  • Loguj sygnały konfliktów (duplicate key errors, serialization failures, lock timeouts) i alertuj przy ich wzrostach.

Jeśli budujesz flowy w Koder.ai, traktuj to jako kryteria akceptacji: wygenerowana aplikacja powinna bezpiecznie się nie powieść przy powtórkach i współbieżności, a nie tylko przechodzić scenariusz "happy path".

Scenariusz przykładowy: zapobieganie podwójnemu zatwierdzeniu

Dwóch pracowników otwiera ten sam wniosek zakupowy. Obaj klikają Zatwierdź w ciągu kilku sekund. Oba żądania trafiają na serwer.

Co może pójść nie tak jest nieporządne: wniosek zostaje "zatwierdzony" dwukrotnie, wychodzą podwójne powiadomienia, a wszelkie sumy powiązane z zatwierdzeniami (zużyty budżet, dzienna liczba zatwierdzeń) rosną o 2. Obie aktualizacje są osobno poprawne, ale kolidują.

Oto plan naprawczy, który dobrze działa z bazą PostgreSQL.

1) Uczyń podwójne zatwierdzenie niemożliwym w bazie

Dodaj regułę, która gwarantuje, że tylko jeden rekord zatwierdzenia może istnieć dla żądania. Na przykład przechowuj zatwierdzenia w osobnej tabeli i narzuć unique constraint na request_id. Teraz drugi insert zawiedzie nawet gdy kod aplikacji ma błąd.

2) Opakuj zmianę stanu w transakcję

Przy zatwierdzaniu wykonaj cały przejście w jednej transakcji:

  • Zablokuj wiersz request (lub zrób update z warunkiem typu WHERE status = 'pending').
  • Zweryfikuj, że jest nadal pending.
  • Zapisz rekord zatwierdzenia.
  • Zaktualizuj status request na approved.

Jeśli drugi pracownik przyjdzie później, zobaczy albo 0 zaktualizowanych wierszy, albo błąd unique constraint. W obu wypadkach wygra tylko jedna zmiana.

3) Daj jasny feedback w UI

Po naprawie pierwszy pracownik widzi Approved i normalne potwierdzenie. Drugi widzi przyjazny komunikat typu: "To żądanie zostało już zatwierdzone przez kogoś innego. Odśwież, aby zobaczyć najnowszy status." Koniec z powiadomieniami podwójnymi i cichymi błędami.

Jeśli generujesz CRUD w platformie takiej jak Koder.ai (Go backend z PostgreSQL), możesz zaimplementować te sprawdzenia w akcji approve raz i wielokrotnie je wykorzystywać dla innych operacji "tylko jeden zwycięzca".

Następne kroki: uczynienie poprawek względem wyścigów elementem rutyny budowy

Warunki wyścigu najłatwiej naprawić, gdy traktujesz je jako powtarzalną rutynę, a nie jednorazowe polowanie na błąd. Skoncentruj się na kilku ścieżkach zapisu, które mają największe znaczenie, i spraw, by były nudno poprawne, zanim dopracujesz resztę.

Zacznij od nazwania swoich głównych punktów kolizji. W wielu aplikacjach CRUD to często ta sama trójka: liczniki (polubienia, zapasy, salda), zmiany statusów (Draft -> Submitted -> Approved) i podwójne wysyłania (dwukliknięcia, retry, wolne sieci).

Rutyna, która się sprawdza:

  • Spisz jedną prawdziwą regułę dla każdego flow (np.: zamówienie można zatwierdzić tylko raz i tylko z pending).
  • Dodaj najpierw ograniczenia w bazie (unique keys, foreign keys, check constraints), aby baza odrzucała niemożliwe stany.
  • Dodaj blokady/transakcje tylko tam, gdzie jedna zmiana musi wygrać (np. zablokowanie jednego wiersza przy zatwierdzaniu).
  • Dodaj jeden mały test współbieżności na krytyczny workflow, ukierunkowany na obawiany błąd (dwa zatwierdzenia, dwa inkrementy, dwa wysłania formularza).
  • Przejrzyj obsługę błędów, aby UI pokazywało jasny komunikat, gdy baza odrzuca konfliktowy zapis.

Jeśli budujesz na Koder.ai, Planning Mode to praktyczne miejsce do zmapowania każdego flowu zapisu jako kroków i reguł zanim wygenerujesz zmiany w Go i PostgreSQL. Snapshots i rollback też się przydają, gdy wdrażasz nowe ograniczenia lub zachowanie blokujące i chcesz szybki powrót w razie nieprzewidzianego przypadku brzegowego.

Z czasem stanie się to nawykiem: każda nowa funkcja zapisu dostaje constraint, plan transakcyjny i test współbieżności. Wtedy warunki wyścigu w aplikacjach CRUD przestają być niespodzianką.

Często zadawane pytania

Czym jest wyścig w aplikacji CRUD?

Wyścig występuje, gdy dwa żądania zmieniają te same dane niemal w tym samym czasie, a o wyniku decyduje kolejność ich wykonania. Jeden prawidłowy zapis może nadpisać drugi, utworzyć duplikat lub dwukrotnie zmienić status.

Które funkcje CRUD są najbardziej narażone na wyścigi?

Zwróć uwagę na liczniki, stany magazynowe, terminy rezerwacji, pola statusu oraz rekordy z zasadami wymagającymi unikalności. W tych obszarach często stosuje się schemat odczytu, a potem zapisu, który zawodzi, gdy żądania się nakładają.

Jak powstrzymać użytkowników przed dwukrotnym wysłaniem formularza?

Wyłącz przycisk po pierwszym kliknięciu, ale zabezpiecz też serwer. Wysyłaj jeden klucz idempotencji na akcję i zapisuj go, aby powtórzone żądania zwracały pierwotny wynik zamiast tworzyć drugi rekord.

Jak bezpiecznie aktualizować liczniki?

Użyj aktualizacji bazy danych, która zmienia wartość bezpośrednio, na przykład dodając 1 w instrukcji SQL. Nie odczytuj licznika do aplikacji, nie zmieniaj go tam i nie zapisuj z powrotem.

Jak zapobiec konfliktom przy zmianach statusu?

Umieść zmianę statusu za jedną regułą po stronie serwera, która sprawdza bieżący status i zezwala tylko na prawidłowe kolejne kroki. Odrzucaj nieaktualne lub nieprawidłowe przejścia z jasnym komunikatem.

Czym jest blokowanie optymistyczne?

Blokowanie optymistyczne zapisuje wersję lub znacznik czasu wraz z rekordem. Aktualizacja powiedzie się tylko wtedy, gdy ta wartość nadal się zgadza, dzięki czemu aplikacja może poinformować użytkownika, że ktoś inny wcześniej zmienił rekord.

Kiedy używać blokady bazy danych?

Użyj transakcji z blokadą wiersza, gdy jedno żądanie musi podjąć decyzję przed zmianą kilku rekordów, na przykład zarezerwować zapas i potwierdzić zamówienie. Ogranicz czas blokady do minimum i unikaj wywołań zewnętrznego API wewnątrz transakcji.

Dlaczego ograniczenia bazy danych są ważne w przypadku wyścigów?

Dodaj ograniczenia unikalności dla zasad, takich jak jeden adres e-mail na konto lub jedna akceptacja na żądanie. Dodaj klucze obce i ograniczenia sprawdzające dla prawidłowych odwołań i wartości. Baza danych odrzuci wtedy niemożliwe stany, nawet jeśli dwa żądania dotrą jednocześnie.

Jak zapobiec ujemnemu stanowi magazynowemu?

Ustaw warunek aktualizacji stanu magazynowego w jednej transakcji, na przykład odejmuj zapas tylko wtedy, gdy pozostaje go wystarczająco dużo. Jeśli aktualizacja nie zmieni żadnego wiersza, poinformuj kupującego, że produkt jest niedostępny, zamiast potwierdzać zamówienie z nadmierną sprzedażą.

Jak przetestować przepływ zapisu pod kątem wyścigów?

Uruchom dwa żądania jednocześnie dla tego samego rekordu lub tej samej ilości, a potem sprawdź wynik. Przetestuj, czy w razie potrzeby jedno żądanie wygrywa, konflikty zwracają kontrolowaną odpowiedź, a baza danych nie zawiera duplikatów ani nieprawidłowych danych.

Related posts