Développer un service de mise en cache générique pour améliorer les performances

Supposons qu'un collègue vous demande une liste de tous les pays du monde. Parce que vous n'êtes pas un expert en géographie, vous surfez sur le site Web des Nations Unies, téléchargez la liste et l'imprimez pour elle. Cependant, elle souhaite seulement examiner la liste; elle ne l'emporte pas avec elle. Parce que la dernière chose dont vous avez besoin est un autre morceau de papier sur votre bureau, vous transmettez la liste à la déchiqueteuse.

Un jour plus tard, un autre collègue demande la même chose: une liste de tous les pays du monde. Vous maudissant de ne pas avoir gardé la liste, vous surfez à nouveau sur le site Web des Nations Unies. Lors de cette visite sur le site Web, vous constatez que l'ONU met à jour sa liste de pays tous les six mois. Vous téléchargez et imprimez la liste pour votre collègue. Il la regarde, merci, et encore une fois, vous laisse la liste. Cette fois, vous classez la liste avec un message sur une note Post-it jointe qui vous rappelle de la supprimer après six mois.

Effectivement, au cours des prochaines semaines, vos collègues continueront à demander la liste encore et encore. Vous vous félicitez d'avoir déposé le document car vous pouvez extraire le document du classeur plus rapidement que vous ne pouvez l'extraire du site Web. Votre concept de classeur prend de l'ampleur; bientôt tout le monde commence à mettre des objets dans votre armoire. Pour éviter que l'armoire ne se désorganise, vous définissez des règles d'utilisation. En votre qualité officielle de gestionnaire du classeur, vous demandez à vos collègues de placer des étiquettes et des post-it sur tous les documents, qui identifient les documents et leur date de mise au rebut / d'expiration. Les étiquettes aident vos collègues à localiser le document qu'ils recherchent et les notes Post-it permettent de déterminer si les informations sont à jour.

Le classeur devient si populaire que bientôt vous ne pourrez plus y déposer de nouveaux documents. Vous devez décider quoi jeter et quoi garder. Bien que vous jetiez tous les documents périmés, l'armoire déborde toujours de papier. Comment décidez-vous des documents non expirés à supprimer? Supprimez-vous le document le plus ancien? Vous pouvez jeter le moins fréquemment utilisé ou le moins récemment utilisé; dans les deux cas, vous auriez besoin d'un journal indiquant le moment où chaque document a été consulté. Ou peut-être pourriez-vous décider des documents à éliminer en vous basant sur un autre déterminant; la décision est purement personnelle.

Pour relier l'analogie du monde réel ci-dessus au monde informatique, le classeur fonctionne comme un cache: une mémoire à grande vitesse qui nécessite parfois une maintenance. Les documents dans le cache sont des objets mis en cache, qui sont tous conformes aux normes définies par vous, le gestionnaire de cache. Le processus de nettoyage du cache est appelé purge. Étant donné que les éléments mis en cache sont purgés après un certain temps, le cache est appelé cache minuté.

Dans cet article, vous apprendrez à créer un cache Java pur à 100% qui utilise un thread d'arrière-plan anonyme pour purger les éléments expirés. Vous verrez comment concevoir un tel cache tout en comprenant les compromis impliqués dans différentes conceptions.

Construisez le cache

Assez d'analogies de classeurs: passons aux sites Web. Les serveurs de sites Web doivent également gérer la mise en cache. Les serveurs reçoivent à plusieurs reprises des demandes d'informations, identiques aux autres demandes. Pour votre prochaine tâche, vous devez créer une application Internet pour l'une des plus grandes entreprises du monde. Après quatre mois de développement, dont de nombreuses nuits blanches et beaucoup trop de colas Jolt, l'application passe en test de développement avec 1000 utilisateurs. Un test de certification de 5 000 utilisateurs et un déploiement de production ultérieur de 20 000 utilisateurs suivent les tests de développement. Toutefois, après avoir reçu des erreurs de mémoire insuffisante alors que seuls 200 utilisateurs testent l'application, les tests de développement s'arrêtent.

Pour discerner la source de la dégradation des performances, vous utilisez un produit de profilage et découvrez que le serveur charge plusieurs copies de bases de données ResultSet, chacune contenant plusieurs milliers d'enregistrements. Les enregistrements constituent une liste de produits. De plus, la liste des produits est identique pour chaque utilisateur. La liste ne dépend pas de l'utilisateur, comme cela aurait pu être le cas si la liste de produits était issue d'une requête paramétrée. Vous décidez rapidement qu'une copie de la liste peut servir tous les utilisateurs simultanés, vous la mettez donc en cache.

Cependant, un certain nombre de questions se posent, qui comprennent des complexités telles que:

  • Et si la liste de produits change? Comment le cache peut-il expirer les listes? Comment savoir combien de temps la liste de produits doit rester dans le cache avant son expiration?
  • Que faire si deux listes de produits distinctes existent et que les deux listes changent à des intervalles différents? Puis-je expirer chaque liste individuellement ou doivent-elles toutes avoir la même durée de conservation?
  • Que faire si le cache est vide et que deux demandeurs essaient le cache exactement en même temps? Lorsqu'ils le trouveront tous les deux vide, créeront-ils leurs propres listes, puis essaieront-ils tous les deux de mettre leurs copies dans le cache?
  • Que faire si les éléments restent dans le cache pendant des mois sans y avoir accès? Ne mangeront-ils pas de mémoire?

Pour relever ces défis, vous devez créer un service de mise en cache logicielle.

Dans l'analogie avec le classeur, les gens vérifient toujours le classeur en premier lorsqu'ils recherchent des documents. Votre logiciel doit implémenter la même procédure: une requête doit vérifier le service de mise en cache avant de charger une nouvelle liste depuis la base de données. En tant que développeur de logiciels, votre responsabilité est d'accéder au cache avant d'accéder à la base de données. Si la liste de produits est déjà chargée dans le cache, vous utilisez la liste mise en cache, à condition qu'elle ne soit pas expirée. Si la liste de produits n'est pas dans le cache, vous la chargez à partir de la base de données et la mettez en cache immédiatement.

Remarque: avant de passer aux exigences et au code du service de mise en cache, vous voudrez peut-être consulter la barre latérale ci-dessous, «Caching versus pooling». Il explique la mise en commun, un concept connexe.

Exigences

Conformément aux bons principes de conception, j'ai défini une liste d'exigences pour le service de mise en cache que nous développerons dans cet article:

  1. Toute application Java peut accéder au service de mise en cache.
  2. Les objets peuvent être placés dans le cache.
  3. Les objets peuvent être extraits du cache.
  4. Les objets mis en cache peuvent déterminer eux-mêmes leur date d'expiration, permettant ainsi une flexibilité maximale. Les services de mise en cache qui expirent tous les objets en utilisant la même formule d'expiration ne parviennent pas à fournir une utilisation optimale des objets mis en cache. Cette approche est inadéquate dans les systèmes à grande échelle car, par exemple, une liste de produits peut changer quotidiennement, alors qu'une liste d'emplacements de magasin ne peut changer qu'une fois par mois.
  5. Un thread d'arrière-plan qui s'exécute avec une priorité faible supprime les objets mis en cache expirés.
  6. Le service de mise en cache peut être amélioré ultérieurement grâce à l'utilisation d'un mécanisme de purge le moins récemment utilisé (LRU) ou le moins fréquemment utilisé (LFU).

la mise en oeuvre

Pour satisfaire l'exigence 1, nous adoptons un environnement Java 100% pur. En fournissant du public getet des setméthodes dans le service de mise en cache, nous remplissons également les exigences 2 et 3.

Avant de procéder à une discussion sur la condition 4, je mentionnerai brièvement que nous allons satisfaire la condition 5 en créant un thread anonyme dans le gestionnaire de cache; ce thread démarre dans le bloc statique. De plus, nous répondons à l'exigence 6 en identifiant les points où le code serait ajouté ultérieurement pour implémenter les algorithmes LRU et LFU. J'entrerai plus en détail sur ces exigences plus loin dans l'article.

Maintenant, revenons à l'exigence 4, où les choses deviennent intéressantes. Si chaque objet mis en cache doit déterminer par lui-même s'il a expiré, vous devez avoir un moyen de demander à l'objet s'il a expiré. Cela signifie que les objets du cache doivent tous se conformer à certaines règles; vous accomplissez cela en Java en implémentant une interface.

Commençons par les règles qui régissent les objets placés dans le cache.

  1. Tous les objets doivent avoir une méthode publique appelée isExpired(), qui renvoie une valeur booléenne.
  2. Tous les objets doivent avoir une méthode publique appelée getIdentifier(), qui retourne un objet qui distingue l'objet de tous les autres dans le cache.

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.

Maintenant, la isExpired()méthode doit simplement déterminer si le dateofExpirationest avant ou après la date et l'heure actuelles. Si la date est antérieure à l'heure actuelle et que l'objet mis en cache est considéré comme expiré, la isExpired()méthode renvoie true; si la date est postérieure à l'heure actuelle, l'objet mis en cache n'est pas expiré et isExpired()renvoie false. Bien sûr, si dateofExpirationest nul, ce qui serait le cas si minutesToLiveétait nul, alors la isExpired()méthode retourne toujours false, indiquant que l'objet mis en cache vit pour toujours.