EhCache i Hibernate

Planuję doprowadzić development Star Trek API do końca. Będę przez jakiś czas, być może całkiem długi czas, jedyną osobą, która będzie płaciła za utrzymanie serwera, na którym API będzie działało. Nie planuję inwestować w taki serwer zbyt wielu środków, dlatego w moim interesie jest, by zrealizować wydajne cache’owanie. Przyda mi się ono na pewno, biorąc pod uwagę fakt, że do tej pory do Pokéapi zostało wykonane 175 milionów zapytań.

Wymagania

Wymagania dotyczące cache’u w Star Trek API są relatywnie proste, w porównaniu do innych systemów. Dane, które trafią do bazy, czeka jeden z dwóch scenariuszy. W pierwszym scenariuszu dane nie będą się zmieniać, gdy już trafią do bazy, ponieważ zasilenie dokona się poza serwerem aplikacyjnym, i serwerowi aplikacyjnemu zostanie tylko wskazana napełniona baza. Drugi scenariusz jest taki, że dane zmienią się co prawda, ale będę mógł kontrolować moment, gdy się to stanie, i wówczas będzie można wyczyścić cache. Problem inwalidacji cache’y w zasadzie nie będzie istniał.

Instalacja i konfiguracja

A móc używać EhCache, po pierwsze trzeba dodać do projektu zależności. Ja dodałem dwie:

compile group: 'net.sf.ehcache', name: 'ehcache'
compile group: 'org.hibernate', name: 'hibernate-ehcache'

Po drugie, trzeba skonfigurować przynajmniej domyślny cache. EhCache pozwala na składowanie cache’owanych zasobów w wielu regionach, ale zdecydowałem na początek, że przynajmniej póki nie będę wiedział więcej o tym, jakie zasoby są najczęściej kwerendowane, nie chcę przekombinować z konfiguracją. Moja obecna konfiguracja wygląda więc tak, i jest najbardziej podstawową wersją, jaka może być.

Integracja z Hibernatem

EntityManagerFactory musi dostać kilka dodatkowy propertisów, widocznych tutaj. Trzeba więc między innymi włączyć cache drugiego poziomu, a także wskazać EhCache jako fabrykę regionów cache’u. Oznacza to klasę implementującą interfejs zdefiniowany przez Hibernate’a, który gwarantuje wytworzenie kilku kolejnych obiektów, odpowiedzialnych kolejno za cache’owanie między innymi kolekcji, wyników zapytań, i encji.

Strategia cache’owania

Wymyśliłem, że dodatkowo będę miał własną strategię cache’owania. Interfejs jest prosty i wygląda tak. Strategia dostanie QueryBuilder, czyli obiekt, w którym przed wykonaniem zapytania gromadzone są jego parametry (kryteria zawężające, sortowanie, relacje do pobrania). Strategię cache’owania można zdefiniować w propertisach aplikacji. Napisałem trzy strategie. Jedną, która zawsze zwraca prawdę, druga, która zawsze zwraca fałsz, i trzecią, najciekawszą, której chce domyślnie używać, i która zwraca prawdę warunkowo, gdy jedynym kryterium w zapytaniu jest UID, czyli unikalny identyfikator obiektu.

Cache’owanie encji

Gdy już mamy wszystko zintegrowane, trzeba wyznaczyć encje, które będą cache’owane. W moich przypadku są to wszystkie encje, które trafiają do REST-owego i SOAP-owego API. Do encji trzeba było dodać adnotację @Cache, tak jak zostało zrobione tutaj. Dodatkowo napisałem test, który z pomocą refleksji pilnuje, żeby wszystkie encje, poza jawnie określonymi, miały zawsze adnotację @Cache. Napisałem też drugi test, który to samo robi dla pól zawierających relacje jeden do wielu i wiele do wielu.

Testy

Pozostało mi przetestować ten setup. Testy manualne polegały na zapytaniu kilku endpointów i obserwowaniu, jakie zapytania odkładają się w logach za pierwszym i za kolejnymi razami. Za kolejnymi razami było tych zapytań zdecydowanie mniej, chociaż ciągle jeszcze jakieś się pojawiają i będę musiał wrócić do tematu, żeby maksymalnie wyżyłować EhCache. Prędkość odpowiedzi endpointów za kolejnymi zapytaniami znacząco wzrosła.

Podsumowanie

Przewiduję, ze klienty będą wykonywać relatywnie mało zapytań przeszukujących bazę, w których zestaw kryteriów zapytania jest słabo powtarzalny, a relatywnie dużo zapytań, które pytają o konkretny zasób za pośrednictwem UID. To założenie może z łatwością okazać się nieprawdziwe, a wtedy trzeba będzie przestrajać cache. Ale to może być temat na kolejny post, za kilka miesięcy.