Разработете обща кешираща услуга, за да подобрите производителността

Да предположим, че колега ви иска списък с всички държави в света. Тъй като не сте експерт по география, сърфирате до уебсайта на ООН, изтегляте списъка и го разпечатвате за нея. Тя обаче иска само да разгледа списъка; тя всъщност не го взема със себе си. Тъй като последното нещо, от което се нуждаете, е още едно парче хартия на бюрото ви, подавате списъка на шредера.

Ден по-късно друг колега иска същото: списък на всяка държава в света. Псувайки себе си, че не поддържате списъка, вие отново сърфирате обратно към уебсайта на ООН. При това посещение на уебсайта отбелязвате, че ООН актуализира списъка си с държави на всеки шест месеца. Изтегляте и отпечатвате списъка за вашия колега. Той го поглежда, благодаря ви и отново оставя списъка с вас. Този път изпращате списъка със съобщение в приложена бележка Post-it, което ви напомня да го изхвърлите след шест месеца.

Разбира се, през следващите няколко седмици вашите колеги продължават да изискват списъка отново и отново. Поздравявате се за подаването на документа, тъй като можете да извлечете документа от картотеката по-бързо, отколкото да го извлечете от уебсайта. Вашата концепция на картотеката се задържа; скоро всички започват да поставят предмети във вашия шкаф. За да предотвратите дезорганизирането на шкафа, вие задавате насоки за използването му. В качеството си на служител на кабинета, вие инструктирате колегите си да поставят етикети и бележки след него върху всички документи, които идентифицират документите и тяхната дата на изхвърляне / изтичане. Етикетите помагат на вашите колеги да намерят документа, който търсят, а бележките Post-it определят дали информацията е актуална.

Шкафът за картотеки става толкова популярен, че скоро не можете да подавате нови документи в него. Трябва да решите какво да изхвърлите и какво да запазите. Въпреки че изхвърляте всички изтекли документи, шкафът все още прелива от хартия. Как решавате кои документи с неизтекъл срок на годност да бъдат изхвърлени? Изхвърляте ли най-стария документ? Можете да изхвърлите най-рядко използваните или най-малко използваните наскоро; и в двата случая ще ви е необходим дневник, който да съдържа списък при достъп до всеки документ. Или може би бихте могли да решите кои документи да се изхвърлят въз основа на някакъв друг определящ фактор; решението е чисто лично.

За да свърже горната аналогия в реалния свят с компютърния свят, картотеката работи като кеш: високоскоростна памет, която от време на време се нуждае от поддръжка. Документите в кеша са кеширани обекти, всички от които отговарят на стандартите, зададени от вас, кеш мениджъра. Процесът на почистване на кеша се нарича продухване. Тъй като кешираните елементи се изчистват след изтичането на определен период от време, кешът се нарича кеширан с време.

В тази статия ще научите как да създадете 100 процента чист Java кеш, който използва анонимен фонов поток за прочистване на изтекли елементи. Ще видите как да създадете такъв кеш, докато разбирате компромисите, свързани с различни дизайни.

Изградете кеша

Достатъчно много аналогии с картотеката: нека преминем към уебсайтове. Сървърите на уебсайтове също трябва да се справят с кеширането. Сървърите многократно получават заявки за информация, които са идентични с други заявки. За следващата си задача трябва да създадете интернет приложение за една от най-големите компании в света. След четири месеца разработка, включително много безсънни нощи и твърде много Jolt colas, приложението влиза в тестове за разработка с 1000 потребители. Тест за сертифициране от 5000 потребители и последващо разпространение на производство от 20 000 потребители следват теста за разработка. След като получите грешки при липса на памет, докато само 200 потребители тестват приложението, тестването за разработка спира.

За да разпознаете източника на влошаване на производителността, използвате профилиращ продукт и откривате, че сървърът зарежда множество копия на бази данни ResultSet, всяка от които има няколко хиляди записа. Записите съставят списък с продукти. Освен това списъкът с продукти е идентичен за всеки потребител. Списъкът не зависи от потребителя, какъвто може да е случаят, ако продуктовият списък е резултат от параметризирана заявка. Бързо решавате, че едно копие от списъка може да обслужва всички едновременни потребители, така че го кеширайте.

Възникват обаче редица въпроси, които включват такива сложности като:

  • Какво ще стане, ако списъкът с продукти се промени? Как кешът може да изтече списъците? Как да разбера колко дълго продуктовият списък трябва да остане в кеша, преди да изтече?
  • Ами ако съществуват два различни продуктови списъка и двата списъка се променят на различни интервали? Мога ли да изтича всеки списък поотделно или всички те трябва да имат еднакъв срок на годност?
  • Какво ще стане, ако кеш паметта е празна и двама заявители пробват кеша по едно и също време? Когато и двамата го намерят празен, ще създадат ли собствени списъци и след това и двамата ще се опитат да поставят своите копия в кеша?
  • Какво ще стане, ако елементите стоят в кеша в продължение на месеци, без да бъдат достъпни? Няма ли да изядат паметта?

За да се справите с тези предизвикателства, трябва да създадете услуга за кеширане на софтуер.

В аналогията на картотеката хората винаги проверявали първо шкафа, когато търсели документи. Вашият софтуер трябва да приложи същата процедура: заявката трябва да провери услугата за кеширане, преди да зареди нов списък от базата данни. Като разработчик на софтуер, вашата отговорност е да осъществите достъп до кеша преди достъп до базата данни. Ако списъкът с продукти вече е зареден в кеша, тогава използвате кеширания списък, при условие че той не е изтекъл. Ако списъкът с продукти не е в кеша, тогава го зареждате от базата данни и го кеширате незабавно.

Забележка: Преди да пристъпите към изискванията и кода на услугата за кеширане, може да искате да проверите страничната лента по-долу „Кеширане срещу пулиране“. Обяснява обединяването, свързана концепция.

Изисквания

В съответствие с добрите принципи на проектиране, дефинирах списък с изисквания за кеширащата услуга, който ще разработим в тази статия:

  1. Всяко приложение на Java може да има достъп до услугата за кеширане.
  2. Обектите могат да се поставят в кеша.
  3. Обектите могат да бъдат извлечени от кеша.
  4. Кешираните обекти могат сами да определят кога изтичат, като по този начин позволяват максимална гъвкавост. Услугите за кеширане, които изтичат всички обекти, използващи една и съща формула за изтичане, не осигуряват оптимално използване на кеширани обекти. Този подход е неадекватен в широкомащабните системи, тъй като например списък с продукти може да се променя ежедневно, докато списъкът с местата за магазини може да се променя само веднъж месечно.
  5. Фонова нишка, която работи под нисък приоритет, премахва кешираните обекти с изтекъл срок на годност.
  6. Услугата за кеширане може да бъде подобрена по-късно чрез използването на механизъм за прочистване на най-малко наскоро (LRU) или най-рядко използван (LFU).

Изпълнение

За да удовлетворим изискване 1, ние приемаме 100% чиста Java среда. Предоставяйки public getи setметоди в услугата за кеширане, ние също изпълняваме изисквания 2 и 3.

Преди да продължа с обсъждането на изискване 4, накратко ще спомена, че ще удовлетворим изискване 5, като създадем анонимна нишка в кеш мениджъра; тази нишка започва в статичния блок. Също така, ние удовлетворяваме изискване 6, като идентифицираме точките, където по-късно ще бъде добавен код за реализиране на алгоритмите LRU и LFU. Ще разгледам по-подробно тези изисквания по-нататък в статията.

Сега, обратно към изискване 4, където нещата стават интересни. Ако всеки кеширан обект трябва сам да определи дали е с изтекъл срок, тогава трябва да имате начин да попитате обекта дали е изтекъл. Това означава, че всички обекти в кеша трябва да отговарят на определени правила; постигате това в Java чрез внедряване на интерфейс.

Нека започнем с правилата, които управляват обектите, поставени в кеша.

  1. Всички обекти трябва да имат извикан публичен метод isExpired(), който връща булева стойност.
  2. Всички обекти трябва да имат извикан публичен метод getIdentifier(), който връща обект, който отличава обекта от всички останали в кеша.

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.

Сега isExpired()методът трябва просто да определи дали dateofExpirationе преди или след текущата дата и час. Ако датата е преди текущото време и кешираният обект се счита за изтекъл, isExpired()методът връща true; ако датата е след текущото време, кешираният обект не е изтекъл и isExpired()връща false. Разбира се, ако dateofExpirationе null, което би било така, ако minutesToLiveбеше нула, тогава isExpired()методът винаги връща false, указвайки, че кешираният обект живее вечно.