Utveckla en generisk cachingtjänst för att förbättra prestanda

Anta att en kollega ber dig om en lista över alla länder i världen. Eftersom du inte är någon geografisk expert surfar du över till FN: s webbplats, laddar ner listan och skriver ut den för henne. Men hon vill bara undersöka listan; hon tar det faktiskt inte med sig. Eftersom det sista du behöver är ett annat papper på skrivbordet, matar du listan till dokumentförstöraren.

En dag senare begär en annan kollega samma sak: en lista över alla länder i världen. Förbannad dig själv för att inte hålla listan surfar du tillbaka till FN: s webbplats. Vid detta besök på webbplatsen noterar du att FN uppdaterar sin landslista var sjätte månad. Du laddar ner och skriver ut listan för din kollega. Han tittar på det, tack, och lämnar igen listan med dig. Den här gången arkiverar du listan med ett meddelande på en bifogad Post-it-anteckning som påminner dig om att kasta den efter sex månader.

Visst nog, under de närmaste veckorna fortsätter dina medarbetare att begära listan om och om igen. Du gratulerar dig själv för att du arkiverade dokumentet eftersom du kan extrahera dokumentet från arkivskåpet snabbare än du kan extrahera det från webbplatsen. Ditt arkivskåpskoncept slår fast snart börjar alla lägga saker i ditt skåp. För att förhindra att skåpet växer oorganiserat anger du riktlinjer för att använda det. Som officiell chef för arkivskåp instruerar du dina kollegor att placera etiketter och Post-it-anteckningar på alla dokument som identifierar dokumenten och deras kassering / utgångsdatum. Etiketterna hjälper dina kollegor att hitta det dokument de letar efter, och Post-it-anteckningarna kvalificerar om informationen är uppdaterad.

Arkivskåpet blir så populärt att du snart inte kan lägga in några nya dokument i det. Du måste bestämma vad du ska kasta ut och vad du ska behålla. Även om du slänger ut alla utgångna dokument, flödar skåpet fortfarande av papper. Hur bestämmer du vilka dokument som inte finns kvar? Kasserar du det äldsta dokumentet? Du kan kasta det minst använda eller det senast använda; i båda fallen behöver du en logg som listades när varje dokument öppnades. Eller kanske du kan bestämma vilka dokument som ska kasseras baserat på någon annan determinant; beslutet är rent personligt.

För att relatera ovanstående verkliga analogi till datorvärlden fungerar arkivskåpet som ett cache: ett höghastighetsminne som ibland behöver underhåll. Dokumenten i cacheminnet är cachade objekt, som alla överensstämmer med de standarder som du, cachechefen har ställt in. Processen för att rensa bort cache kallas rensning. Eftersom cachade objekt rensas efter att en viss tid har förflutit kallas cachen en tidsinställd cache.

I den här artikeln lär du dig hur du skapar en 100 procent ren Java-cache som använder en anonym bakgrundstråd för att rensa utgångna objekt. Du kommer att se hur man arkiverar en sådan cache samtidigt som man förstår avvägningarna som är involverade i olika mönster.

Bygg cachen

Tillräckliga arkivskåpanalogier: låt oss gå vidare till webbplatser. Webbplatsens servrar måste också hantera cachning. Servrar får upprepade gånger förfrågningar om information, som är identiska med andra förfrågningar. För din nästa uppgift måste du bygga en Internetapplikation för ett av världens största företag. Efter fyra månaders utveckling, inklusive många sömnlösa nätter och alldeles för många Jolt colas, går applikationen in i utvecklingstest med 1 000 användare. Ett certifieringstest på 5 000 användare och en efterföljande produktion på 20 000 användare följer utvecklingen. Men när du får fel i minnet medan endast 200 användare testar applikationen, stoppas utvecklingstestningen.

För att urskilja källan till prestandaförsämringen använder du en profileringsprodukt och upptäcker att servern laddar flera kopior av databas ResultSet, var och en har flera tusen poster. Posterna utgör en produktlista. Dessutom är produktlistan identisk för varje användare. Listan beror inte på användaren, vilket kan ha varit fallet om produktlistan hade resulterat från en parametrerad fråga. Du bestämmer snabbt att en kopia av listan kan tjäna alla samtidiga användare, så du cachar den.

Emellertid uppstår ett antal frågor som inkluderar sådana komplexiteter som:

  • Vad händer om produktlistan ändras? Hur kan cacheminnet upphöra att gälla i listorna? Hur vet jag hur länge produktlistan ska finnas kvar i cachen innan den går ut?
  • Vad händer om det finns två olika produktlistor och de två listorna ändras med olika intervall? Kan jag förfalla varje lista individuellt, eller måste de alla ha samma hållbarhet?
  • Vad händer om cacheminnet är tomt och två förfrågare försöker cacheminnet exakt samtidigt? När de båda finner det tomt, kommer de att skapa sina egna listor och försöker båda lägga sina kopior i cachen?
  • Vad händer om objekt sitter i cachen i flera månader utan åtkomst? Kommer de inte att äta upp minnet?

För att hantera dessa utmaningar måste du konstruera en tjänst för cachning av programvara.

I arkivskåpets analogi kontrollerade människor alltid skåpet först när de letade efter dokument. Din programvara måste implementera samma procedur: en begäran måste kontrollera cachetjänsten innan du läser in en ny lista från databasen. Som programutvecklare är ditt ansvar att komma åt cacheminnet innan du går in i databasen. Om produktlistan redan har laddats in i cachen, använder du den cachade listan, förutsatt att den inte har gått ut. Om produktlistan inte finns i cachen laddar du den från databasen och cachar den omedelbart.

Obs! Innan du fortsätter med cachingtjänstens krav och kod kanske du vill kolla in sidofältet nedan, "Caching Versus Pooling." Det förklarar pooling, ett relaterat koncept.

Krav

I enlighet med goda designprinciper definierade jag en kravlista för cachetjänsten som vi kommer att utveckla i den här artikeln:

  1. Alla Java-applikationer kan komma åt cachetjänsten.
  2. Objekt kan placeras i cachen.
  3. Objekt kan extraheras från cachen.
  4. Cachade objekt kan bestämma själva när de upphör, vilket möjliggör maximal flexibilitet. Cachningstjänster som upphör att gälla för alla objekt med samma utgångsformel ger inte optimal användning av cachade objekt. Detta tillvägagångssätt är otillräckligt i storskaliga system eftersom till exempel en produktlista kan ändras dagligen, medan en lista över butiksplatser kan ändras bara en gång i månaden.
  5. En bakgrundstråd som har låg prioritet tar bort utgått cachade objekt.
  6. Cachningstjänsten kan förbättras senare genom användning av en spolningsmekanism som används minst nyligen (LRU) eller minst använda (LFU).

Genomförande

För att uppfylla krav 1 antar vi en 100 procent ren Java-miljö. Genom att tillhandahålla allmänhet getoch setmetoder i cachetjänsten uppfyller vi också kraven 2 och 3.

Innan jag fortsätter med en diskussion om krav 4 kommer jag kort att nämna att vi kommer att uppfylla krav 5 genom att skapa en anonym tråd i cachehanteraren; den här tråden börjar i det statiska blocket. Vi uppfyller också krav 6 genom att identifiera de punkter där koden senare skulle läggas till för att implementera LRU- och LFU-algoritmer. Jag kommer att gå in mer detaljerat om dessa krav senare i artikeln.

Nu, tillbaka till krav 4, där saker blir intressanta. Om varje cachat objekt själv måste avgöra om det har gått ut måste du ha ett sätt att fråga objektet om det har upphört att gälla. Det betyder att objekt i cacheminnet alla måste uppfylla vissa regler; du åstadkommer det i Java genom att implementera ett gränssnitt.

Låt oss börja med reglerna som styr objekten i cachen.

  1. Alla objekt måste ha en offentlig metod som kallas isExpired(), vilket returnerar ett booleskt värde.
  2. Alla objekt måste ha en offentlig metod som kallas getIdentifier(), vilket returnerar ett objekt som skiljer objektet från alla andra i cachen.

Obs! Innan du hoppar rakt in i koden måste du förstå att du kan implementera en cache på många sätt. Jag har hittat mer än ett dussin olika implementeringar. Enhydra och Caucho ger utmärkta resurser som innehåller flera cacheimplementeringar.

Du hittar gränssnittskoden för den här artikelns cachetjänst i 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.

Nu isExpired()måste metoden helt enkelt avgöra om det dateofExpirationär före eller efter aktuellt datum och tid. Om datumet är före aktuell tid och det cachade objektet anses utgått, isExpired()returnerar metoden true; om datumet är efter den aktuella tiden har det cachade objektet inte löpt ut och isExpired()returnerar falskt. Naturligtvis, om dateofExpirationär null, vilket skulle vara fallet om det minutesToLivevar noll, isExpired()returnerar metoden alltid falsk, vilket indikerar att det cachade objektet lever för alltid.