Blokady i cache

W procesie przygotowania do certyfikacji przyszedł czas na trochę bardziej skomplikowane zagadnienia. W dzisiejszym wpisie skupię się na dwóch z nich – blokowaniu oraz cachowaniu. Do roboty.

Podstawy

Blokowanie (ang. locking) jest procesem, który polega na zapewnieniu spójności zmian w bazie danych (w dużym uproszczeniu). Wyróżniamy dwa typy blokowania – optymistyczne oraz pesymistyczne. Ze względu na wydajność bazy danych najczęściej stosuje się blokowanie optymistyczne i od niego też zaczniemy. Podejście optymistyczne charakteryzuje się założeniem, iż obecnie wykonywana transakcja jest jedyną, która dokonuje zmian na naszej encji(ach). Pobieramy encję z bazy danych, wrzucamy do kontekstu, dokonujemy jakiś zmian i finalizujemy transakcję. W 99% takie podejście jest słuszne – przeprowadzone zmiany zostaną poprawnie odwzorowane w bazie danych. Co jednakże w przypadku, kiedy chwilkę przed commitem naszej transakcji, ktoś inny dokonał zmian w bazie? Ano wtedy przy próbie zapisu dostaniemy wyjątek OptimisticLockException, zaś transakcja zostanie cofnięta.

W jaki jednakże sposób nasz dostawca JPA wie, że stan w bazie danych się zmienił? Ano by to osiągnąć encja musi posiadać zadeklarowane pole/własność, w którym będzie trzymana informacja o wersji encji. Domyślnie w czystym JPA nie trzeba zadeklarować tego typu własność w encji – sam dostawca JPA dostarcza mechanizmy, które sprawdzają wersję encji, istnienie takiego pola nie jest wymagane. Jednakże jeśli z jakiegoś powodu chcielibyśmy mieć dostęp do wersji naszej encji możemy to zrobić dość prosto:

@Version private int version;  // oczywiscie nazwa moze byc dowolna

Do deklaracji takiego pola możemy użyć jednego z typów: int,short oraz long, ich wrapperów oraz java.sql.Timestamp. Wygląda to pięknie, jednakże jak zawsze jest łyżka dziegciu – nie zawsze można być pewnym, że własność ta została zaktualizowana w bazie danych w przypadku zmian dokonywanych poza JPA. Co więcej – istotnym jest też to, iż pole to niekoniecznie jest zmieniane w przypadku relacji, których encja nie jest właścicielem (stroną posiadającą). Jednakże da się to obejść – stosując jeden z typów blokowania optymistycznego.

Typy blokowania optymistycznego

Domyślnie JPA w przypadku używania pola wersjonującego używa dla transakcji izolacji Read Commited. Użycie tego typu oznacza to, iż dopóki transakcja nie zostanie zacommitowana, zmiany w niej wprowadzone nie będą widoczne dla kogokolwiek innego. Jednakże jeśli potrzebujemy bardziej zaawansowanych blokad, możemy wykorzystać dodatkowe możliwości JPA przy użyciu jednej z poniższych metod – trzech z metod interfejsu EntityManager oraz jednej interfejsu Query:

  • lock() – jednoznaczne zablokowanie wszystkich obiektów znajdujących się w danym persistence context
  • refresh() – jedna z sygnatur tej metody umożliwia przekazanie typu blokady do wskazywanej encji
  • find() – jedna z sygnatur tej metody umożliwia przekazanie typu blokady do pobieranej encji. Zwrócony wynik ma założoną blokadę.
  • Query.setLockMode() – pozwala na wskazanie typu blokady, która zostanie użyta podczas egzekucji zapytania.

Uzbrojeni w tę wiedzę możemy pokazać typy optymistycznego blokowania, które można użyć (wartości te są dostępne w typie wyliczeniowym LockModeType):

  • OPTIMISTIC – zachowanie, które najczęściej powoduje, że w momencie commitu transakcji baza danych wykonuje uaktualnienie encji za pomocą instrukcji SELECT ….. FOR UPDATE. Dzięki temu ewentualne zmiany dokonywane przez naszą transakcję spowoduję, że gdyby w danym momencie dane próbowała zaktualizować konkurencyjna transakcja – ta druga rzuci wyjątkiem. Jest to zachowanie bardzo bliskie domyślnemu zachowaniu JPA i dość często niczym się od niego nie różni.
  • OPTIMISTIC_FORCE_INCREMENT – robi dokładnie to samo co powyższa, z tym tylko, iż przy okazji nawet jeśli encja nie uległa zmianie (sama z siebie, ale np. zmieniliśmy jej relacje), numer wersji w bazie dla tej encji zostanie zmieniony.

Blokowanie pesymistyczne

Od JPA w wersji 2.0 mamy też możliwość stosowania blokowania pesymistycznego. W skrócie – przy pobieraniu encji z bazy danych następuje zablokowanie obiektu na poziomie bazy (blokada najczęściej dotyczy 1 wiersza), dzięki czemu inna transakcja nie może dokonywać jakichkolwiek zmian. Ma to jednakże dość spory narzut na wydajność oraz grozi potencjalnymi deadlockami.

Dobrze jest w tym miejscu również wspomnieć o tym, iż blokowanie optymistyczne ma miejsce zawsze w przypadku zadeklarowania pola @Version! Zatem nawet jeśli zablokujemy obiekt pesymistycznie, ale dopiero po jakimś czasie od pobrania encji z bazy, istnieje możliwość, że przy próbie zapisu dostaniemy OptimisticLockException. Dobrze jest to mieć na uwadze.

Jednakże czasem użycie tego typu blokowania jest konieczne – wówczas JPA dostarcza nam trzy typy blokowania.

  • PESSIMISTIC_WRITE – wspominałem, że w przypadku blokowania optymistycznego dostawca JPA może użyć ‘pod spodem’ instrukcji SELECT … FOR UPDATE. W przypadku tego typu na 99,9% tak właśnie zrobi.
  • PESSIMISTIC_READ – akurat ten typ niekoniecznie musi u konkretnego dostawcy bazy danych różnić w implementacji (chodzi o wywoływanie pod spodem SELECT … FOR UPDATE). Teoretycznie przy tym typie blokady encja jest blokowana zaraz po jej odczytaniu. Generalnie polecam lekturę wątku na Stackoverflow by zrozumieć różnice między tymi dwoma.
  • PESSIMISTIC_FORCE_INCREMENT – podobnie jak w przypadku wersji optymistycznej nawet jeśli encja nie zostanie zmieniona, wymuszamy zmianę jej wersji.

Na sam koniec warto wspomnieć o możliwych timeoutach dla oczekiwania na blokadę pesymistyczną. Większość dostawców JPA umożliwia wskazanie okresu czasu, po którym zostanie wyrzucony LockTimeoutException – w użyciu oczywiście jest znów mechanizm sugestii (ang. hints), gdzie nazwa sugestii to javax.persistence.lock.timeout (podawany w milisekundach). Generalnie wymieniony powyżej wyjątek otrzymamy nie tylko przy timeoucie, ale również przy innych błędach, które baza danych uzna za niekrytyczne. Jednakże w przypadku poważniejszych błędów (czyli takich, które mają docelowo spowodować rollback transakcji) otrzymamy PessimisticLockException

Cachowanie

Zasadniczo cachowanie jest mechanizmem dość dobrze znanym programistom. Jednakże w przypadku JPA mechanizmy te nie do końca są tym, do czego jesteśmy przyzwyczajeni. Dla przykładu – w rozwiązaniach komercyjnych (Memcached, EhCache, Oracle Coherence) to programista wyraźnie ‘wkłada’ dane do cache używając określonego klucza. Pobiera te dane również posiłkując się owym kluczem. W przypadku JPA jest trochę inaczej – programista bezpośrednio nie użytkuje cache. Ba! Specyfikacja JPA nie wymusza na dostawcy dostarczeniu w ogóle implementacji mechanizmu cache!

Co zatem w ogóle mamy na myśli pisząc – cache – po stronie JPA? Ano w tym kontekście za cache uznajemy współdzielony cache, z którego korzysta instancja EntityManager, a tym samym – persistence context. Dla programisty odbywa się to zupełnie transparentnie, zaś samo operowanie na cache jest realizowane przez dość ubogi interfejs o nazwie Cache. Odpowiednią implementację otrzymujemy poprzez wywołanie metody EntityManagerFactory.getCache(). Interfejs udostępnia kilka prostych metod – głównie chodzi o evict (umożliwia usunięcie konkretnej encji z cache) oraz contains (nazwa dość obrazowa). To wszystko. Samo pobieranie encji do cache i zarządzanie wersjami jest robotą dla dostawcy JPA i programista nie za wiele może tutaj zdziałać.

Co zatem możemy zrobić jeśli chodzi o cachowanie w JPA? Ano tak naprawdę to skupić się na dwóch rzeczach – co chcemy cachować, a także w jaki sposób to robić.
Do konfiguracji cachowania możemy użyć zarówno konfiguracji opartej o XML (persistence.xml), albo też adnotacji używanej w klasach encyjnych. Zapis w pliku konfiguracyjnym (przy pomocy elementu shared-cache-mode) docelowo ma zastosowanie w całej jednostce persystencji. Zanim przejdziemy do adnotacji wypada wylistować dostępne wartości:

  • NOT_SPECIFIED – wartość domyślna, która pozostawia dostawcy JPA decyzję czy cache jest w ogóle używany.
  • ALL – wszystkie encje są cachowane
  • NONE – odwrotnie do powyższego – nic nie jest cachowane
  • DISABLE_SELECTIVE – domyślnie cachowaniu podlegają wszystkie encje, z wyjątkiem tych, które oznacyzmy adnotacją @Cacheable(false)
  • ENABLE_SELECTIVE – przeciwieństwo poprzedniego – nie cachujemy niczego z wyjątkiem encji oznaczonych adnotacją @Cacheable(true)

To podstawowa konfiguracja zapisana w jednostce persystencji. Jednakże istnieje możliwość dynamicznego (w trakcie wykonywania programu) dostosowania konfiguracji. Istnieją dwie własności, które możemy ustawić – CacheRetrieveMode oraz CacheStoreMode. Wartości tych typów wyliczeniowych możemy przekazać do metody find() EntityManagera, albo też w formie podpowiedzi do instancji Query. Wypadałoby wytłumaczyć dlaczego mamy dwie oddzielne własności. Otóż jeżeli poruszamy się zawsze w obrębie jednej maszyny wirtualnej i jednego EntityManagerFactory problem zasadniczo nie istnieje. Jednakże w momencie, gdy dane w bazie danych mogą być zmieniane poza naszą aplikacją, wówczas rozróżnienie zapisu i odczytywania z cache staje się całkiem zrozumiałe. Wartości wspomnianych własności to – BYPASS, USE oraz w przypadku zapisu – REFRESH. Po szczegóły jednakże odsyłam do dokumentacji.

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s