Dwie transakcje - saveAndFlush, a długość requestu.

0

Cześć, mam taką zagwozdkę w temacie relacyjnych baz i transakcji.

Mam transakcję T1, która pobiera z repo counter (10), mnoży go x2 (20), zapisuje, ale metodą saveAndFlush, aby update poszedł od razu, po czym czeka załóżmy 5s (bo np jest jeszcze jakaś długa operacja biznesowa) i dopiero się commituje.

W międzyczasie (ale po tym jak fizycznie poszedł update countera (20) w T1) transakcja T2 pobiera counter (jest 10, bo izolacja READ_COMMITED), mnoży go x3 (30) i zapisuje do DB - i to jest zanim T1 się zakomituje, bo T2 nie ma żadnych długich operacji.

No i w wyniku mamy counter = 30. No i tu jest ok, ale zastanawiam się dlaczego request http, który zrobił T2 kręci się aż do czasu kiedy skończy się T1? W logach widzę, że T2 robi sql z updatem countera na 30 dużo wcześniej. Czy ta T2 musi z jakiegoś powodu czekać na commit T1? Czemu tak jest?

I dodatkowo jeśli mam w T1 saveAndFlush, a na jej końcu rzucę jakiś RuntimeException to rollback tej transakcji coś zrobi w ogóle? Odkręci ten update jak rozumiem?

Jeśli T1 nie ma flusha tylko normalny save, to request z T2 trwa ułamek sekundy, ale counter na końcu jest 20, bo T1 puszcza update do bazy na samym końcu transakcji i to jest dla mnie zrozumiałe.

1

Bawiąc się JPA poczytaj o adnotacji @Version, optimistic locking, pessimistic locking, no i jak to od strony bazy danych wygląda, czyli taki np. select for update.

6

Trzeba by zobaczyć od strony bazy danych jak wyglądają locki, natomiast saveAndFlush nie commituje transakcji, a jedynie odpala zapytania (zrzuca zmiany z cache JPA).

To tak jakbyś w konsoli bazy napisał „begin transaction”, zrobił jakiś update, ale nie zrobił „commit”. W konsekwencji na wierszach (a nawet na całej tabelce, jeśli jest TABLOCK) założony jest lock i pozostałe transakcje (w zależności od stopnia izolacji) będą lub nie czekać na zakończenie pierwszej.

5

Zasadniczo to co opisujesz to ani JPA, ani SQL. To kwestia konkretnego silnika bazy danych. Warto poćwiczyć na gołej konsoli SQL (najlepiej natywnej dla db).
Jak już potwierdzisz to szukasz pod hasłem concurrency database XXX (mvcc itp), locking i zwykle takie scenariusze są opisane. Jak spędzisz kilka dni z dokumentacją i testując kolejne przypadki to pewnie nawet poczujesz, że ogarniasz czemu tak się dzieje. A kilka tygodni później i tak zapomnisz :-) (swego czasu trochę spędziłem analizując różne podobne przypadki Postgresa (są dobrze opisane w necie) - ale to było dawno i nieprawda).

0

@Charles_Ray: nie wiem w którym miejscu zostałem źle zrozumiany, ale ja nigdzie nie napisałem, że transakcja mi się commituje przy saveAndFlush a ponad to przy tym jak leci sql do bazy danych ;p

Może, przedstawie to na jakimś diagramie:

screenshot-20210818213207.png

I teraz .. w którym miejscu popełniam błąd myślowy:

Dla T1:
findById = select po obiekt
saveAndFlush = update counter
I koniec T1 to jest commit

Dla T2:
findById = select, ale dostajemy 10 bo READ_COMMITED
save = update ALE DO CACHE JPA
Koniec T2 to już zrzucenie update do bazy

Moje pytanie ...
Czy i dlaczego jeśli tak jest T2 czeka na commit T1 aby, się skommitować?

W logach hibernatowych update do bazy w T2 idzie dużo wcześniej przed końcem T1, a mimo wszystko request T2 kręci się aż do końca requestu T1.

Na koniec counter jest 30.

Baza to h2, bo robię takie ćwiczenia po prostu

@jarekr000000
Dzięki, w wolnej chwili poćwiczę na konsoli sobie jakieś psqlowej i przypomnę księge do sql.

3

Wydaje mi się że masz locka na counterze który jest zwolniony dopiero po koncu transakcji.

0

@ProgScibi:

Nigdzie nie idzie mi select for update, nigdzie nie użyłem adnotacji @lock. Ponad to T2 może mi bez problemu odczytać counter i go zmienić, więc gdzie tu lock?
Nie mówię, że go nie ma, bo może tak jest, ale nie rozumiem skąd on tam i jak on w takim razie działa skoro pozwala bawić się counterem bez problemu. Są takie locki, które pozwalają na wszystko?

Ja znam optimistic locking związany z @Version oraz pesisimistic locking związany właśnie z w/w adnotacją @lock robiący select for update w zapytaniu. Chyba, że coś grubo pomieszałem.

3

@Bambo:

Nigdzie nie idzie mi select for update, nigdzie nie użyłem adnotacji @lock.

Ponad to T2 może mi bez problemu odczytać counter i go zmienić, więc gdzie tu lock?

zachęcam do przeczytania o poziomach izolacji. W read committed jedna transakcja nadpisuje wiersz dopero jak inna się skończy. Read committed jest od tego żebyś ne czytał zmian które jeszcze nie zostały zacommitowane. No i są różne rodzaje locków

Zacytuję z książki:

Transactions running at the read committed isolation level must prevent drity writes, usually by delaying the second write until the first write's transaction is commited or aborted

4

@Bambo Musisz zrozumieć, że silnik bazy danych zakłada locki na wierszach czy tego chcesz czy nie, to jest element zapewniania współbieżności i kontroli spójności danych. Pierwsze z brzegu: https://retool.com/blog/isolation-levels-and-locking-in-relational-databases/

4

Dokumentacja Postgresa:

Read Committed is the default isolation level in PostgreSQL. When a transaction uses this isolation level, a SELECT query (without a FOR UPDATE/SHARE clause) sees only data committed before the query began; it never sees either uncommitted data or changes committed during query execution by concurrent transactions. In effect, a SELECT query sees a snapshot of the database as of the instant the query begins to run. However, SELECT does see the effects of previous updates executed within its own transaction, even though they are not yet committed. Also note that two successive SELECT commands can see different data, even though they are within a single transaction, if other transactions commit changes after the first SELECT starts and before the second SELECT starts.

UPDATE, DELETE, SELECT FOR UPDATE, and SELECT FOR SHARE commands behave the same as SELECT in terms of searching for target rows: they will only find target rows that were committed as of the command start time.** However, such a target row might have already been updated (or deleted or locked) by another concurrent transaction by the time it is found. In this case, the would-be updater will wait for the first updating transaction to commit or roll back (if it is still in progress). **If the first updater rolls back, then its effects are negated and the second updater can proceed with updating the originally found row. If the first updater commits, the second updater will ignore the row if the first updater deleted it, otherwise it will attempt to apply its operation to the updated version of the row. The search condition of the command (the WHERE clause) is re-evaluated to see if the updated version of the row still matches the search condition. If so, the second updater proceeds with its operation using the updated version of the row. In the case of SELECT FOR UPDATE and SELECT FOR SHARE, this means it is the updated version of the row that is locked and returned to the client.

0

@ProgScibi:
Ok czyli wychodzi na to, że jak idzie fizycznie w DB update to nakładany jest lock i druga transakcja, żeby się zakommitować musi czekać, aż pierwsza się zakommituje albo zrollbackuje. Czaje, dzięki.
Wychodzi na to, że hibernate od razu zrobił sobie update do bazy, ale i tak czekał z zakkomitowaniem w T2 aż T1 się wykona. Gitara.

Zarejestruj się i dołącz do największej społeczności programistów w Polsce.

Otrzymaj wsparcie, dziel się wiedzą i rozwijaj swoje umiejętności z najlepszymi.