Jest to właściwość izolacji transakcji. Dużo na ten temat napisano i gorąco polecam przegląd w Projektowanie z intensywnym przetwarzaniem danych Aplikacje . Uważam, że jest to najbardziej pomocny opis w polepszeniu mojego osobistego zrozumienia.
Domyślny poziom postgres to CZYTAJ ZATWIERDZONE co pozwala każdej z tych jednoczesnych transakcji zobaczyć podobny (stan dostępnych środków), mimo że powinny być zależne.
Jednym ze sposobów rozwiązania tego problemu byłoby oznaczenie każdej z tych transakcji jako spójności "SERIALIZABLE".
Powinno to wymusić poprawność Twojej aplikacji kosztem dostępności, tzn. w tym przypadku druga transakcja nie będzie mogła modyfikować rekordów i zostanie odrzucona, co będzie wymagało ponownej próby. W przypadku aplikacji POC lub aplikacji o małym natężeniu ruchu jest to zwykle całkowicie akceptowalny pierwszy krok, ponieważ na razie możesz zapewnić poprawność.
Również w książce, o której mowa powyżej, myślę, że był przykład dostępności uchwytu bankomatu. Pozwalają na ten stan wyścigu, a użytkownik może przekroczyć limit, jeśli nie jest w stanie połączyć się ze scentralizowanym bankiem, ale ogranicza maksymalną wypłatę, aby zminimalizować promień wybuchu!
Innym architektonicznym sposobem rozwiązania tego problemu jest przełączenie transakcji w tryb offline i uczynienie ich asynchronicznymi, tak aby każda transakcja wywołana przez użytkownika była publikowana w kolejce, a dzięki posiadaniu jednego konsumenta kolejki w naturalny sposób można uniknąć wyścigu. Kompromis tutaj jest podobny, istnieje stała przepustowość dostępna od jednego pracownika, ale pomaga to rozwiązać problem poprawności na razie :P
Blokowanie na różnych maszynach (takie jak używanie redis na postgres/grpc) nazywa się blokowaniem rozproszonym i jest o nim dużo napisane https://martin.kleppmann.com/2016/02/08/jak-zrobic-blokada-rozpowszechniona.html