Opracuj ogólną usługę buforowania, aby poprawić wydajność

Załóżmy, że współpracownik prosi Cię o listę wszystkich krajów na świecie. Ponieważ nie jesteś ekspertem w dziedzinie geografii, przeglądasz stronę internetową ONZ, pobierasz listę i drukujesz ją dla niej. Jednak chce tylko przejrzeć listę; ona właściwie nie bierze tego ze sobą. Ponieważ ostatnią rzeczą, której potrzebujesz, jest kolejna kartka papieru na biurku, listę podajesz do niszczarki.

Dzień później inny współpracownik prosi o to samo: listę wszystkich krajów na świecie. Przeklinając siebie za nieprzechowywanie listy, ponownie wracasz do witryny ONZ. Podczas tej wizyty w Serwisie zauważasz, że ONZ aktualizuje swoją listę krajów co sześć miesięcy. Pobierasz i drukujesz listę dla swojego współpracownika. Patrzy na to, dziękuje i jeszcze raz zostawia ci listę. Tym razem odkładasz listę wraz z wiadomością na załączonej karteczce Post-it, która przypomina o odrzuceniu jej po sześciu miesiącach.

I rzeczywiście, przez kilka następnych tygodni Twoi współpracownicy będą wciąż prosić o listę. Gratulujesz sobie złożenia dokumentu, ponieważ możesz go wyodrębnić z szafki na dokumenty szybciej niż z witryny internetowej. Twoja koncepcja szafek na akta przyjmuje się; wkrótce wszyscy zaczną wkładać przedmioty do Twojej szafki. Aby zapobiec dezorganizacji szafki, ustalasz wytyczne dotyczące jej używania. Pełniąc oficjalne obowiązki kierownika szafy na dokumenty, polecasz swoim współpracownikom umieszczanie etykiet i karteczek samoprzylepnych na wszystkich dokumentach, które identyfikują dokumenty i ich datę wyrzucenia / wygaśnięcia. Etykiety pomagają współpracownikom zlokalizować dokument, którego szukają, a notatki Post-it pozwalają sprawdzić, czy informacje są aktualne.

Szafka na akta staje się tak popularna, że ​​wkrótce nie będzie można w niej składować żadnych nowych dokumentów. Musisz zdecydować, co wyrzucić, a co zachować. Chociaż wyrzucasz wszystkie przeterminowane dokumenty, szafka nadal jest przepełniona papierem. Jak decydujesz, które niewygasłe dokumenty odrzucić? Czy odrzucasz najstarszy dokument? Możesz odrzucić najmniej często używane lub najmniej ostatnio używane; w obu przypadkach potrzebny byłby dziennik zawierający informacje o dostępie do każdego dokumentu. A może mógłbyś zdecydować, które dokumenty odrzucić na podstawie innych wyznaczników; decyzja jest czysto osobista.

Aby odnieść powyższą analogię do świata rzeczywistego ze światem komputerowym, szafka na akta działa jak pamięć podręczna: pamięć o dużej szybkości, która czasami wymaga konserwacji. Dokumenty w pamięci podręcznej są obiektami w pamięci podręcznej, z których wszystkie są zgodne ze standardami określonymi przez Ciebie, menedżera pamięci podręcznej. Proces czyszczenia pamięci podręcznej jest nazywany czyszczeniem. Ponieważ elementy w pamięci podręcznej są czyszczone po upływie określonego czasu, pamięć podręczna nazywana jest pamięcią podręczną czasową.

W tym artykule dowiesz się, jak utworzyć w 100% czystą pamięć podręczną Java, która używa anonimowego wątku w tle do usuwania wygasłych elementów. Zobaczysz, jak zaprojektować taką pamięć podręczną, jednocześnie rozumiejąc kompromisy związane z różnymi projektami.

Zbuduj pamięć podręczną

Dość analogii do kartotek: przejdźmy do stron internetowych. Serwery witryn internetowych również muszą zajmować się buforowaniem. Serwery wielokrotnie otrzymują żądania informacji, które są identyczne jak inne żądania. Do następnego zadania musisz zbudować aplikację internetową dla jednej z największych firm na świecie. Po czterech miesiącach tworzenia aplikacji, w tym wielu nieprzespanych nocy i zbyt wielu Jolt coli, aplikacja przechodzi testy rozwojowe na 1000 użytkowników. Po testach rozwojowych następuje test certyfikacyjny dla 5000 użytkowników, a następnie wdrożenie produkcyjne dla 20 000 użytkowników. Jednak po pojawieniu się błędów braku pamięci, gdy tylko 200 użytkowników testuje aplikację, testowanie programistyczne zostaje wstrzymane.

Aby rozpoznać źródło spadku wydajności, należy użyć produktu do profilowania i odkryć, że serwer ładuje wiele kopii baz danych ResultSet, z których każda ma kilka tysięcy rekordów. Zapisy tworzą listę produktów. Ponadto lista produktów jest identyczna dla każdego użytkownika. Lista nie zależy od użytkownika, co mogłoby mieć miejsce, gdyby lista produktów była wynikiem zapytania sparametryzowanego. Szybko decydujesz, że jedna kopia listy może obsługiwać wszystkich jednocześnie pracujących użytkowników, więc zapisujesz ją w pamięci podręcznej.

Pojawia się jednak szereg pytań, które obejmują takie zawiłości, jak:

  • Co się stanie, jeśli lista produktów ulegnie zmianie? W jaki sposób pamięć podręczna może wygasać listy? Skąd będę wiedzieć, jak długo lista produktów powinna pozostać w pamięci podręcznej, zanim wygaśnie?
  • Co się stanie, jeśli istnieją dwie odrębne listy produktów, które zmieniają się w różnych odstępach czasu? Czy mogę wygasać każdą listę indywidualnie, czy też wszystkie muszą mieć ten sam okres trwałości?
  • Co się stanie, jeśli pamięć podręczna jest pusta i dwóch żądających spróbuje jej użyć dokładnie w tym samym czasie? Kiedy oboje uznają to za puste, czy utworzą własne listy, a następnie oboje spróbują umieścić swoje kopie w pamięci podręcznej?
  • Co się stanie, jeśli przedmioty będą pozostawać w pamięci podręcznej przez wiele miesięcy bez dostępu? Czy nie pożrą pamięci?

Aby sprostać tym wyzwaniom, musisz stworzyć usługę buforowania oprogramowania.

W analogii do szafek na dokumenty ludzie zawsze najpierw sprawdzali szafę, szukając dokumentów. Twoje oprogramowanie musi implementować tę samą procedurę: żądanie musi sprawdzić usługę buforowania przed załadowaniem nowej listy z bazy danych. Jako programista odpowiadasz za dostęp do pamięci podręcznej przed uzyskaniem dostępu do bazy danych. Jeśli lista produktów została już załadowana do pamięci podręcznej, użyj listy przechowywanej w pamięci podręcznej, pod warunkiem, że nie utraciła ważności. Jeśli lista produktów nie znajduje się w pamięci podręcznej, ładujesz ją z bazy danych i natychmiast buforujesz.

Uwaga: przed przejściem do wymagań i kodu usługi buforowania warto zapoznać się z poniższym paskiem bocznym „Buforowanie a pule”. Wyjaśnia łączenie, pokrewną koncepcję.

Wymagania

Zgodnie z zasadami dobrego projektowania zdefiniowałem listę wymagań dla usługi buforowania, którą opracujemy w tym artykule:

  1. Dostęp do usługi buforowania może uzyskać każda aplikacja Java.
  2. Obiekty można umieszczać w buforze.
  3. Obiekty można wyodrębnić z pamięci podręcznej.
  4. Obiekty w pamięci podręcznej mogą samodzielnie określić, kiedy wygasną, zapewniając w ten sposób maksymalną elastyczność. Usługi buforowania, które wygasają wszystkie obiekty przy użyciu tej samej formuły wygaśnięcia, nie zapewniają optymalnego wykorzystania buforowanych obiektów. Takie podejście jest niewystarczające w systemach o dużej skali, ponieważ na przykład lista produktów może zmieniać się codziennie, podczas gdy lista lokalizacji sklepów może się zmieniać tylko raz w miesiącu.
  5. Wątek działający w tle, który działa z niskim priorytetem, usuwa wygasłe obiekty z pamięci podręcznej.
  6. Usługę pamięci podręcznej można później ulepszyć za pomocą mechanizmu czyszczenia najmniej niedawno używanego (LRU) lub najmniej często używanego (LFU).

Realizacja

Aby spełnić wymaganie 1, stosujemy w 100% czyste środowisko Java. Udostępniając publiczność geti setmetody w usłudze buforowania, spełniamy również wymagania 2 i 3.

Zanim przejdę do omówienia Wymagania 4, wspomnę pokrótce, że spełnimy Wymaganie 5, tworząc anonimowy wątek w menedżerze pamięci podręcznej; ten wątek zaczyna się w bloku statycznym. Ponadto spełniamy wymaganie 6, identyfikując punkty, w których później zostanie dodany kod w celu zaimplementowania algorytmów LRU i LFU. Bardziej szczegółowo omówię te wymagania w dalszej części artykułu.

Wróćmy teraz do wymagania 4, gdzie sprawy stają się interesujące. Jeśli każdy obiekt w pamięci podręcznej musi sam określić, czy wygasł, to musisz mieć możliwość zapytania obiektu, czy wygasł. Oznacza to, że wszystkie obiekty w pamięci podręcznej muszą być zgodne z określonymi regułami; osiągasz to w Javie, implementując interfejs.

Zacznijmy od reguł rządzących obiektami umieszczonymi w pamięci podręcznej.

  1. Wszystkie obiekty muszą mieć wywołaną metodę publiczną isExpired(), która zwraca wartość logiczną.
  2. Wszystkie obiekty muszą mieć wywołaną metodę publiczną getIdentifier(), która zwraca obiekt, który odróżnia obiekt od wszystkich innych w pamięci podręcznej.

Note: Before jumping straight into the code, you must understand that you can implement a cache in many ways. I have found more than a dozen different implementations. Enhydra and Caucho provide excellent resources that contain several cache implementations.

You'll find the interface code for this article's caching service in Listing 1.

Listing 1. Cacheable.java

/** * Title: Caching Description: This interface defines the methods, which must be implemented by all objects wishing to be placed in the cache. * * Copyright: Copyright (c) 2001 * Company: JavaWorld * FileName: Cacheable.java @author Jonathan Lurie @version 1.0 */ public interface Cacheable { /* By requiring all objects to determine their own expirations, the algorithm is abstracted from the caching service, thereby providing maximum flexibility since each object can adopt a different expiration strategy. */ public boolean isExpired(); /* This method will ensure that the caching service is not responsible for uniquely identifying objects placed in the cache. */ public Object getIdentifier(); } 

Any object placed in the cache -- a String, for example -- must be wrapped inside an object that implements the Cacheable interface. Listing 2 is an example of a generic wrapper class called CachedObject; it can contain any object needed to be placed in the caching service. Note that this wrapper class implements the Cacheable interface defined in Listing 1.

Listing 2. CachedManagerTestProgram.java

/** * Title: Caching * Description: A Generic Cache Object wrapper. Implements the Cacheable interface * uses a TimeToLive stategy for CacheObject expiration. * Copyright: Copyright (c) 2001 * Company: JavaWorld * Filename: CacheManagerTestProgram.java * @author Jonathan Lurie * @version 1.0 */ public class CachedObject implements Cacheable { // +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ /* This variable will be used to determine if the object is expired. */ private java.util.Date dateofExpiration = null; private Object identifier = null; /* This contains the real "value". This is the object which needs to be shared. */ public Object object = null; // +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ public CachedObject(Object obj, Object id, int minutesToLive) { this.object = obj; this.identifier = id; // minutesToLive of 0 means it lives on indefinitely. if (minutesToLive != 0) { dateofExpiration = new java.util.Date(); java.util.Calendar cal = java.util.Calendar.getInstance(); cal.setTime(dateofExpiration); cal.add(cal.MINUTE, minutesToLive); dateofExpiration = cal.getTime(); } } // +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ public boolean isExpired() { // Remember if the minutes to live is zero then it lives forever! if (dateofExpiration != null) { // date of expiration is compared. if (dateofExpiration.before(new java.util.Date())) { System.out.println("CachedResultSet.isExpired: Expired from Cache! EXPIRE TIME: " + dateofExpiration.toString() + " CURRENT TIME: " + (new java.util.Date()).toString()); return true; } else { System.out.println("CachedResultSet.isExpired: Expired not from Cache!"); return false; } } else // This means it lives forever! return false; } // +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ public Object getIdentifier() { return identifier; } // +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ } 

The CachedObject class exposes a constructor method that takes three parameters:

public CachedObject(Object obj, Object id, int minutesToLive) 

The table below describes those parameters.

Parameter descriptions of the CachedObject constructor
Name Type Description
Obj Object The object that is shared. It is defined as an object to allow maximum flexibility.
Id Object Id contains a unique identifier that distinguishes the obj parameter from all other objects residing in the cache. The caching service is not responsible for ensuring the uniqueness of the objects in the cache.
minutesToLive Int The number of minutes that the obj parameter is valid in the cache. In this implementation, the caching service interprets a value of zero to mean that the object never expires. You might want to change this parameter in the event that you need to expire objects in less than one minute.

The constructor method determines the expiration date of the object in the cache using a time-to-live strategy. As its name implies, time-to-live means that a certain object has a fixed time at the conclusion of which it is considered dead. By adding minutesToLive, the constructor's int parameter, to the current time, an expiration date is calculated. This expiration is assigned to the class variable dateofExpiration.

Teraz isExpired()metoda musi po prostu określić, czy dateofExpirationjest przed, czy po bieżącej dacie i godzinie. Jeśli data jest wcześniejsza niż aktualna godzina, a buforowany obiekt zostanie uznany za wygasły, isExpired()metoda zwraca wartość true; jeśli data jest późniejsza niż bieżąca godzina, buforowany obiekt nie wygasł i isExpired()zwraca wartość false. Oczywiście, jeśli dateofExpirationma wartość null, co miałoby miejsce, gdyby minutesToLivebyło równe zero, wówczas isExpired()metoda zawsze zwraca wartość false, co oznacza, że ​​buforowany obiekt żyje wiecznie.