Na pewno spotkałeś się z pojęciem równoległej edycji, gdzie wielu użytkowników edytuje (lub próbuje edytować) te same w tym samym momencie.
W którą stronę pojedziesz?
Zauważ, iż użytkownicy modyfikują jedną encję w tym samym momencie, patrząc na ekran. Skutkiem może być napisanie danych edycji użytkownika przez ostatnio wykonane zapytanie UPDATE.
Jak można reagować? Czy można zabezpieczyć się przed tym? Zapraszam do artykułu, w którym dowiesz się o kilku metodach:
- Pessimistic locking
- Pessimistic read
- Pessimistic write
- Pessimistic force increment
- Optimistic locking
- Inne metody
Pessimistic locking
Pessimistic locking to mechanizm wykorzystujący blokowanie rekordu na poziomie bazy danych. Zablokowany i zmodyfikowany rekord (lub rekordy) nie może być modyfikowany/usuwany przez inną transakcję/sesję.
Jak można reagować w aplikacji/systemie:
- Czekać określony czas na możliwość dostępu do danych i kontynuować (byle by użytkownik nie zanudził się oczekiwaniem)
- Rzucić/przechwycić wyjątek i wyświetlić użytkownikowi informację, iż rekord/obiekt jest aktualnie edytowany przez innego użytkownika. Przykładowe zachowanie systemu w takim wypadku:
- Uniemożliwić użytkownikowi pracę z edytowanym rekordem (nie wiadomo co się stanie – np. rekord może zostać za chwilę usunięty).
- Pozwolić na podgląd aktualnych danych.
- Pozwolić na podgląd niezatwierdzonych jeszcze danych z bazy danych (dirty read) – nie jest zalecane
Zalety pesymistycznego blokowania
- Po odblokowaniu dane mogą być przetwarzane dalej
- Zapewnienie integralności danych przez zablokowanie równoległej edycji danych
- Możliwość poinformowania użytkownika o tym, iż dane są aktualnie edytowane
- Możliwość kontrolowanej blokady danych (np. wykorzystanie przy realizacji zadań, które mają być przetwarzane TYLKO na jednej z instancji serwisu bez używania bibliotek typu Quartz)
Wady pesymistycznego blokowania
- Dane blokowane są przez cały czas transakcji (jeśli jest długa to może wystąpić timeout dla oczekiwania na lock)
- W przypadku umożliwienia podglądu 'brudnych’ danych (zmodyfikowanych, ale nie zatwierdzonych) można wprowadzić użytkownika w błąd (przecież użytkownik może się wycofać z edycji; dla aktualizacji stanów magazynowych można dokonać rezerwacji w osobnej transakcji.
Poniżej przedstawię wybrane tryby pesymistycznego blokowania dla JPA z wykorzystaniem encji Card.
Zapytania SQL generowane dla poszczególnych trybów są zależne od motoru bazy danych/dialektu Hibernate. W przykładach poniżej wykorzystuję bazę Postgresql.
Sposób 1- Tryb Pessimistic Read w JPA
Tryb pessimistic read zabezpiecza przed dostępem do zmodyfikowanych, ale jeszcze niezatwierdzonych danych rekordów (commit) w innej transakcji poprzez wstrzymanie wykonania operacji na tym rekordzie przez bazę.
Poniżej przykład kodu wykorzystującego pessimistic read
Przy wyjściu z metody zostanie wywołany commit
Zostanie wygenerowane zapytanie typu SELECT zależne od silnika bazy danych. Poniżej przykład dla PostgreSQL z frazą FOR SHARE.
W przypadku modyfikacji danych encji zostanie także wywołane zapytanie typu UPDATE, które jest najważniejsze dla blokowania.
Żeby łatwiej zobrazować i złapać moment w czasie, możesz tymczasowo dodać
- flush (dla wymuszenia wysłania SQL z UPDATE),
- sleep (dla ułatwienia debugowania)
- log z danymi karty (żeby zobaczyć, jakie dane zostały odczytane – czy zmodyfikowane i niezatwierdzone, czy też ostatnio zatwierdzone)
Uruchamiając wiele instancji programu (w czasie krótszym niż sleep) możesz zaobserwować na bazie stan blokad. Poniżej przykładowe zapytanie dla Postgresql
Rekord został zmodyfikowany.
- Po odblokowaniu rekordu z zakończeniem pierwszej transakcji przez commit) w bieżącej transakcji kolejny SELECT ... FOR SHARE pobierze zaktualizowany wiersz, ale następujący po nim UPDATE rzuci wyjątek – org.postgresql.util.PSQLException: ERROR: deadlock detected, a Hibernate zmapuje go na org.hibernate.exception.LockAcquisitionException
- UWAGA Dla drugiej transakcji próbującej wykonać operacje na tym samym rekordzie (zmodyfikowany i zablokowany)
- Instrukcja SELECT bez frazy FOR SHARE zwróci wynik bez czekania
- Instrukcja SELECT .. FOR SHARE na zmodyfikowanym rekordzie zostanie wstrzymana (działa lock)
- Instrukcja UPDATE bez wcześniejszego locka po odblokowaniu nadpisze wiersz (czyli stać się może coś, czego nie oczekiwałeś).
Jeżeli nie będzie modyfikacji danych, to obydwa zapytania typu SELECT .. FOR SHARE wykonają się bez blokady/czekania – dla blokowania rekordu istotna jest modyfikacja danych.
W przypadku gdy pierwsza transakcja zakończy się jej wycofaniem (rollback), to po zdjęciu blokady SELECT zwróci 'stare dane’ i kolejny lock czy UPDATE się powiedzie.
Sposób 2 – Tryb Pessimistic Write w JPA
W trybie pessimistic write rekord jest blokowany na wyłączność już na etapie wykonania zapytania SELECT i inna transakcja nie może odczytać (z pewnym wyjątkiem, ale o tym dalej).
Jeżeli użyjesz trybu LockModeType.PESSIMISTIC_WRITE to może zostać wygenerowane inne zapytanie – przykład dla PostgreSQL (tym razem fraza SQL FOR UPDATE)
W tym przypadku drugie równoległe zapytanie (typu SELECT FOR UPDATE dla tego samego rekordu) będzie oczekiwało na zwolnienie blokady.
Jeżeli drugie zapytanie, będzie zapytaniem typu SELECT bez frazy FOR UPDATE, to zostanie zwrócony od razu (bez czekania) ostatnio zatwierdzony stan rekordu.
Sposób 3 – Tryb Pessimistic Force Increment w JPA
Tryb pessimistic force increment jest połączeniem pessimistic i optimistic locking, gdzie implementacja JPA w ramach wyszukiwania encji
- Blokuje encję w trybie wyłącznym.
- Edytuje encję poprzez inkrementację wersji rekordu.
Umożliwia to tryb LockModeType.PESSIMISTIC_FORCE_INCREMENT. Jest on powiązany z wersjonowaniem (dodatkowa kolumna z anotacją @Version – zobacz dalej w artykule w sekcji Optimistic Locking).
Już dla samego odczytu encji przez EntityManager.find Zostaną wygenerowane dwa zapytania dla wywołania metody find na EntityManager.
Pierwsze dla SELECT – przykład dla PostgreSQL (tym razem fraza SQL FOR UPDATE NOWAIT)
Drugie inkrementujące wersję rekordu
Jak wspomniałem wcześniej tryb ten jest bliższy mechanizmowi optimistic locking – gdyż również wykorzystuje wersjonowanie encji. Bez tego (bez atrybutu anotowanego @Version) zostanie rzucony wyjątek.
Jeżeli w tym samym czasie baza zostanie odpytana tym samym zapytaniem (SELECT FOR UPDATE NOWAIT) zostanie rzucony wyjątek bazodanowy
could not obtain lock on row in relation "cards"
zmapowany przez Hibernate na PessimisticLockException.
UWAGA. Istnieje również tryb LockModeType.OPTIMISTIC_FORCE_INCREMENT jednak działa on inaczej. Wygenerowane zapytanie SELECT nie zawiera frazy blokującej (FOR SHARE/UPDATE).
Sposób 4 – Wersjonowanie w pessimistic locking
Możesz również dla pessimistic lock skorzystać z anotacji @Version, co da dodatkowy poziom zabezpieczenia, gdyż oprócz mechanizmu bazodanowego (lock) będzie weryfikacja zgodności wersji. Chociaż czystszym rozwiązaniem wydaje się opisany wcześniej PESSIMISTIC_FORCE_INCREMENT.
Sposób 5 – Optimistic locking
Optymistyczne blokowanie to mechanizm, który wykorzystuje numer wersji rekordu. Każda modyfikacja wiąże się z podbiciem numer wersji o jeden i użyciem we frazie WHERE warunku na stary numer wersji.
Poniżej przedstawiam przykład z wykorzystaniem anotacji @javax.persistence.Version dla Spring lub Hibernate.
Wystarczy jedynie istnienie kolumny numerycznej w tabeli powiązanej z encją
oraz anotacja nad polem z wersją w encji.
Uwagi odnośnie użycia:
- Modyfikacja musi być wykonana w ramach transakcji (i oczywiście musi być zmiana danych)
- Anotacja @Version z pakietu org.springframework.data.annotation nie zadziała
- atrybut dla anotacji @Version musi być jednym z typów int, Integer, long, Long, short, Short
Przykład logu zapytania SQL dla modyfikowanych danych
W logu powyżej.
- We frazie WHERE pojawia się warunek zawierający numer wersji (stara) – wartość równa 4 – parametr 7
- We frazie SET pojawia się automatyczne inkrementacja numeru wersji (nowa wartość równa 5 – parametr 5
Jeżeli żaden wiersz w bazie danych nie zostanie zaktualizowany, to oznacza to, iż wiersz został usunięty lub zaktualizowany w innej sesji. W przedstawionym powyżej przykładzie zostanie rzucony wyjątek
Można również zastosować zabezpieczenie na poziomie bazy danych w postaci triggera (update, delete), który sprawdzi zgodność starych wartości rekordu w bazie z tym co jest aktualizowane. Trigger w przypadku nieprawidłowości powinien rzucić wyjątek.
Pozwoli to zabezpieczyć przed nieprawidłowymi modyfikacjami danych z zewnątrz. Przykładem jest instrukcja SQL (np. skrypt) uruchomiona manualnie albo inna aplikacja, która ma dostęp do tych danych. Dzięki temu nieprawidłowe aktualizacje zostaną odrzucone (będą musiały być dostosowane do mechanizmu wersjonowania aby się powiodły).
A o ile używasz w Hibernate/Spring grupowania modyfikacji danych (batch updates) poprzez sterowanie properties, jak np:
- hibernate.jdbc.batch_size
- hibernate.order_inserts
- hibernate.order_updates
i chciałbyś również, aby wersjonowane encje podlegały tym regułom, to ustaw dodatkowo property hibernate.jdbc.batch_versioned_data na true.
Wady optymistycznego blokowania
- O modyfikacji/usunięciu danych dowiadujesz się po fakcie (nie można wykryć locka rekordu na bazie)
Zalety
- Szybsze niż pessimistic locking
Wyjątki JPA związane z pessimistic i optimistic locking
JPA definiuje kilka specjalizowanych wyjątków związanych z pessimistic i optimistic locking
- PessimisticLockException – w przypadku niemożności wykonania blokady rekordu
- LockTimeoutException – w przypadku przekroczenia dopuszczalnego czasu oczekiwania na blokadę. Ustawia się to w konfiguracji JPA atrybutem javax.persistence.lock.timeout a jego wartość to liczba ms.
Powyższe wyjątki mogą posłużyć np. do poinformowania użytkownika, iż rekord jest zablokowany do edycji w innej sesji (przez innego użytkownika).
Istnieje także PersistenceException – ale to bazowy wyjątek w JPA. Powyższe wyjątki dziedziczą z niego.
Sposób 6 – Inne sposoby zarządzania równoległą edycją
Pewną wariacją jest użycie flagi blokady edycji jako osobnej kolumny (lub kolumn) czy też tabeli, gdzie 'rezerwuje się’ chęć edycji. Wówczas nie jest konieczne utrzymywanie długiej transakcji (lub locka), ale konieczne są inne mechanizmy (czyszczenie osieroconych rezerwacji, filtrowanie jeżeli to konieczne edytowanych wierszy).
Możesz się także spotkać z dodatkowym podejściem, gdzie system poza rezerwacją edycji umożliwia 'projektowanie’ i podgląd zmiany. Jest to osobny zbiór tabel (kopia), gdzie modelujesz zmianę, a na samym końcu zmiana może zostać wprowadzona (odpowiednie zmiany na tabelach w bazie) lub choćby zaplanowana do wprowadzenia.
Psst… Interesujący artykuł?
Jeżeli podoba Ci się ten artykuł i chcesz takich więcej – dołącz do newslettera. Nie ominą Cię materiały tego typu.
Dziękujemy!
Wysłaliśmy Ci mail powitalny, w którym znajdziesz link do aktywacji newslettera. Do usłyszenia!
Apache Kafka – wydajność vs. gwarancja dostarczenia wiadomości
Jak stworzyć piekielnie szybką albo maksymalnie bezpieczną wersję producenta oraz konsumenta.
Psst… Interesujący artykuł?
Jeżeli podoba Ci się ten artykuł i chcesz takich więcej – dołącz do newslettera. Nie ominą Cię materiały tego typu.
Dziękujemy!
Wysłaliśmy Ci mail powitalny, w którym znajdziesz link do aktywacji newslettera. Do usłyszenia!
Przydatne linki
- https://www.baeldung.com/jpa-optimistic-locking
- https://www.baeldung.com/jpa-pessimistic-locking
- https://www.baeldung.com/java-jpa-transaction-locks
- https://shootskill.com/database/select-for-update/
- https://www.baeldung.com/jpa-hibernate-batch-insert-update
- http://www.quartz-scheduler.org/
- https://vladmihalcea.com/hibernate-locking-patterns-how-do-pessimistic_read-and-pessimistic_write-work/
- Obraz autorstwa wirestock na Freepik
Podoba Ci się ten artykuł? Weź więcej.
Jeżeli uważasz ten materiał za wartościowy i chcesz więcej treści tego typu – nie przegap ich i otrzymuj je prosto na swoją skrzynkę. Nawiążmy kontakt.
Dziękujemy!
Wysłaliśmy Ci mail powitalny, w którym znajdziesz link do aktywacji newslettera. Do usłyszenia!
Gdybyś potrzebował jeszcze więcej:
Jesteś Java Developerem?
Przejdź na wyższy poziom wiedzy
„Droga do Seniora” 🔥💪
Jesteś Team Leaderem? Masz zespół?
Podnieś efektywność i wiedzę swojego zespołu 👌