Blog Zenika

#CodeTheWorld

Java

Spring Cache

Introduction

La mise en cache a toujours été un besoin important pour à la fois améliorer les performances d’une application et alléger sa charge de travail. De plus, son utilité est particulièrement évidente aujourd’hui avec les applications Web qui peuvent être amenées à gérer des milliers de visiteurs concurrents.D’un point de vue architectural, la gestion du cache est orthogonale à la logique métier de l’application et pour cette raison, elle devrait avoir un impact minimal sur le développement de l’application elle-même.

Depuis la version 3.1, Spring fournit une api pour la gestion de cache, semblable à la gestion déclarative des transactions. L’abstraction de la mise en cache permet une utilisation cohérente des différentes solutions de mise en cache avec un impact minimal sur le code.
Le cache Spring est appliqué à des méthodes Java. Au premier appel d’une méthode avec une combinaison de paramètres, Spring stocke sa valeur de retour dans le cache. Ainsi, l’appel suivant se verra directement servir la valeur venant du cache sans avoir besoin d’appeler le traitement derrière qui peut être couteux. Le tout est appliqué de façon transparente sans impacter l’appelant.
Dans cet article nous allons voir deux implémentations différentes du stockage de cache avec Spring.

  • Implémentation á base des ConcurrentHashMap de Java
  • Implémentation ehcache

Utilisation

La mise en cache d’une méthode avec Spring est une opération simple et transparente, on doit annoter notre méthode via l’annotation @Cacheable.

@Cacheable(value= "dataCache")
public Reponse getDatas(Long param1, String param2){ }

dataCache est le nom du gestionnaire de cache associé.
Au premier appel a la méthode, Spring cache la reponse, identifiée par une clé unique calculer sur la base de hashcode des paramètres d’appels < param1, param2>, et pour le énième appel avec les mêmes paramètres, Spring retourne la réponse déjà cacher sans avoir ré-exécuter la méthode.
Aussi, il est possible d’associé plus qu’un seul cache à notre méthode

@Cacheable({"dataCache",”default”})
public Reponse getDatas(Long param1, String param2){   }

Dans ce cas, chacun des caches sera vérifié avant d’exécuter la méthode, si au moins un cache est trouvé, alors la valeur associée sera retourné.

Génération des clés de cache

L’algorithme de base d’une gestion de cache est relativement trivial. Le cache est une zone mémoire dans laquelle on stocke des objets dont chacun est identifié par une clé unique calculée, on utilise en général les Maps pour stocker un cache. L’algorithme de récupération d’un objet est donc le suivant :

  • On calcule sa clé (généralement hashCode ou combinaison de hashCode)
  • On recherche l’objet dans le cache (recherche par clé)
  • Si l’objet est trouvé

on le renvoie

  • Sinon

on recalcule l’objet réel
on met l’objet dans le cache associé à sa clé
on renvoie l’objet
De même Spring utilise un KeyGenerator basé sur le hachage simple, qui calcule la clé sur la base des tables de hachage des objets passés en paramètre de la méthode.

Cache personnalisé

La clé de cache (key)

Il est tout à fait probable que les méthodes cibles ont différentes signatures qui ne peuvent pas être simplement mappées, cela tend à devenir évident lorsque la méthode cible a plusieurs arguments dont seulement certains sont adaptés pour la mise en cache (alors que le reste n’est utilisé que par la logique de la méthode)

@Cacheable(value= "dataCache")
public Reponse getDatas(Long param1, String param2, boolean param3){   }

Pour de tels cas, l’annotation @ Cacheable permet au développeur de spécifier la manière dont la clé de cache est générée. Le développeur peut utiliser SpEL pour choisir les arguments d’intérêt (ou leurs propriétés imbriquées).

@Cacheable(value= "dataCache", key="#param2")
public Reponse getDatas(Long param1, String param2, boolean param3){     }

Dans ce cas, la clé de cache sera calculée seulement avec le deuxième paramètre <param2>.
Spring permet aussi de spécifier des propriétés imbriquées :

@Cacheable(value="dataCache", key=#param2.name")
 public Reponse getDatas(Long param1, Data param2, boolean param3){}

Dans ce cas, la clé de cache sera calculée avec l’attribut name du paramètre <param2>
Les exemples ci-dessus montrent comment il est simple de sélectionner certain arguments ou une de ses propriétés.

Condition de la mise en cache

Parfois, une méthode pourrait ne pas être appropriée pour mettre en cache tout le temps mais sous certaines conditions. Les annotations de cache supportent cette fonctionnalité. Le paramètre condition prend une expression SpEL qui est évaluée à true ou false. Donc si la condition est vraie, la méthode sera mise en cache.

@Cacheable(value= "dataCache", key="#param2", condition="#param2.length<64")
public Reponse getDatas(Long param1, String param2, boolean param3){   }

Dans ce cas, la méthode est mise en cache si et seulement si la taille du deuxième paramètre est inferieure a 64.

L’annotation @CacheEvict

Le cache Spring permet non seulement la population d’un magasin de cache mais aussi son expulsion. Ce processus est utile pour supprimer les données obsolètes ou inutilisées de la mémoire cache. Opposée à @ Cacheable, l’annotation @ CacheEvict délimite des méthodes qui effectuent l’expulsion de cache, ce sont des méthodes qui agissent comme des déclencheurs de suppression des données à partir du cache. @ CacheEvict nécessite un (ou plusieurs) caches qui sont touchés par l’action.

@CacheEvict(value= "dataCache")
public void reloadData(){   }

Cette option est très pratique lorsqu’une région de cache entier doit être vidée. Il est important de noter que les méthodes void peuvent être utilisées avec @ CacheEvict, ces méthodes agissent comme des déclencheurs de suppression de cache, les valeurs de retour sont ignorées (comme elles n’interagissent pas avec le cache) .ce n’est pas le cas avec @ Cacheable qui ajoute / mettre à jour les données dans le cache et nécessite donc un retour.

L’annotation @CachePut

Forcer la mise à jour d’une entrée de cache. Par opposition à l’annotation @Cacheable, cette annotation provoque toujours l’exécution de la méthode et le stockage de son résultat dans la mémoire cache.

@CachePut(value= "dataCache", key="#param2.name")
public Reponse getDatas(Long param1, Data param2, boolean param3){   }

Donc le @CachePut, est nécessaire pour forcer la création ou la mise à jour d’une entrée dans le cache, sans attendre son expiration.
Le seul cas où la méthode n’est pas exécutée, c’est quand vous fournissez l’option condition de @CachePut, et votre condition est évaluée a false.

Activation de cache

Il est important de noter que les annotations de cache ne déclenchent automatiquement pas leurs exécutions.
Pour activer le support de cache dans un projet Spring, on commence par activer le traitement des annotations @Cacheable via la balise annotation-driven du namespace cache :

<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:cache="http://www.springframework.org/schema/cache"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
       http://www.springframework.org/schema/beans/spring-beans-3.2.xsd
       http://www.springframework.org/schema/cache
       http://www.springframework.org/schema/cache/spring-cache.xsd
       http://www.springframework.org/schema/context">
<cache:annotation-driven />

 
Notez qu’il suffit de supprimer cette balise pour désactiver le cache.
On peut aussi activer le support de cache par annotation en ajoutant @enableCaching dans l’un de nos classes de configuration (@Configuration classes) :

@Configuration
@EnableCaching
public class AppConfig {   }

Contraintes techniques

  • Les objets passés en paramètre de la méthode doivent avoir leurs propres comportements haschode() afin que Spring puisse calculer les clés.
  • Les objets passés en paramètre et l’objet de retour (ainsi leurs propriétés imbriquées) doivent être serialisable.

Choix de l’implémentation

Spring offre deux implémentations différentes du stockage de cache :

  • Implémentation á base des ConcurrentHashMap de Java
  • Implémentation ehcache

Pour les utiliser, il faut simplement déclarer un CacheManager approprié et une entité qui contrôle et gère les caches.

Implémentation cache de base ConcurrentHashMap de Java

Il faut déclarer les gestionnaires de caches SimpleCacheManager dans le contexte de l’application.

<bean id="cacheManager"
class="org.springframework.cache.support.SimpleCacheManager">
<property name="caches">
<set>
<bean class="org.springframework.cache.concurrent.ConcurrentMapCacheFactoryBean" name="default"/>
<bean class="org.springframework.cache.concurrent.ConcurrentMapCacheFactoryBean" name="dataCache"/>
</set>
</property>
</bean>

 
Chaque gestionnaire nécessite un nom (name) unique afin de l’identifier par annotation.
On peut déclarer plusieurs caches gérés par un seul manager SimpleCacheManager
Cette implémentation est basique, elle n’as pas besoin d’une bibliothèque supplémentaire, mais elle n’est pas trop prévue pour des grosses charges qui nécessitent de paramétrages supplémentaires.

Implémentation ehcache

Cette implémentation utilise ehcache, elle est beaucoup plus puissante et flexible.et elle permet un paramétrage avancé du cache de l’application.
L’implémentation ehcache est localisée sous le package org.springframework.cache.ehcache. Pour l’utiliser on doit déclarer le CacheManager approprié.

<bean id="cacheManager"
class="org.springframework.cache.ehcache.EhCacheCacheManager">
 <property name="cacheManager" ref="ehcache"/>
</bean>
<bean id="ehcache" class="org.springframework.cache.ehcache.EhCacheManagerFactoryBean">
 <property name="configLocation" value="classpath:ehcache.xml"/>
 <property name="shared" value="true"/>
</bean>

 
Le fichier ehcache.xml est le fichier de paramétrage des caches de l’application :

<ehcache xsi:noNamespaceSchemaLocation="ehcache.xsd"
   updateCheck="true"
   monitoring="autodetect"
   dynamicConfig="true"
   maxBytesLocalHeap="150M">
   <diskStore path="java.io.tmpdir"/>
   <defaultCache eternal="false"
     maxElementsInMemory="100"
     overflowToDisk="false"/>
   <cache name="dataCache"
     eternal="false"
     timeToIdleSeconds="300"
     maxBytesLocalHeap="30M"
     timeToLiveSeconds="300"
     overflowToDisk="true"
     diskPersistent="false"
     diskExpiryThreadIntervalSeconds="120"
     memoryStoreEvictionPolicy="LRU"/>
   </ehcache>

 
En utilisant l’ehcache, on peut définir plusieurs caches avec différents paramètres d’une manière très simple

  • name:un identifiant de la cache
  • maxBytesLocalHeap : définit le nombre d’octets que le cache peut utiliser de la VM. Si un CacheManager maxBytesLocalHeap a été défini, la taille déterminée de ce cache sera soustraite du CacheManager. D’autres caches partage le reste. Les valeurs de cet attribut sont données à titre <nombre> k | K | M | M | g | G pour kilo-octets (k | K), mégaoctets (m | M), ou gigaoctets (g | G).
  • eternal: définit si les éléments sont éternels. Si c’est le cas, le timeout sera ignoré et l’élément n’est jamais expiré.
  • timeToIdleSeconds: C’est le nombre de secondes que l’élément doit vivre depuis sa dernière utilisation.La valeur par défaut est 0, l’élément reste toujours en repos
  • timeToLiveSeconds: C’est le nombre de secondes que l’élément doit vivre depuis sa création en cache.La valeur par défaut est 0, l’élément vivra éternellement.
  • memoryStoreEvictionPolicy: Politique d’éviction :
    LRU – le moins récemment utilisé
    LFU – moins fréquemment utilisé
    FIFO – premier entré, premier sorti, l’élément le plus ancien par date de création
  • diskExpiryThreadIntervalSeconds: Nombre de secondes entre deux exécutions du processus de contrôle d’éviction.
  • diskPersistent: Permet la mémorisation des objets sur le disque pour une récupération des objets entre deux démarrages de la VM.
  • overflowToDisk: Détermine si les objets peuvent être stockés sur le disque en cas d’atteinte du maximum d’éléments en mémoire

Pour résumer avec une simple formule mathématique :

 expirationTime = Math.min((creationTime + timeToLive),(mostRecentTime + timeToIdle))

Conclusion

La gestion de cache est une problématique critique pour les applications Web, ainsi que la définition d’un système de cache applicatif est généralement considérée comme relativement complexe à mettre en œuvre par les développeurs, pour cela Spring a proposé un système générique de définition de cache complètement transparent, et simple.
Une démo est disponible sous : https://github.com/Zenika/Blogs/tree/master/20140527demoSpringCache

7 réflexions sur “Spring Cache

  • Laabidi Raissi

    Très bon article.
    Une seule remarque, que je pense indispensable à inclure dans chaque tutorial à propos du cache sous Spring.
    La génération des clés ne prend pas en compte les noms des méthodes ni ceux des classes
    (voir: https://jira.spring.io/browse/SPR-8…). Seulement les valeurs des paramètres sont pris en compte (DefaultKeyGenerator pour les versions antérieures à Spring 4, ou SimpleKeyGenerator à partir de Spring 4)
    Ceci étant, il faut absolument inclure le nom de méthode dans le clé si on veut éviter une usine de bugs. Ex:
    key= »{#root.methodName,#yourParam} ». Ou bien implémenter un KeyGenerator personnalisé et qui prendra en compte les valeurs de Object target, Method method dans la méthode ‘generate’

    Répondre
  • Anis Ben Hamda

    @Laabidi Raissi

    Bonne remarque,ceci peut être l’objet d’un deuxième article .

    Répondre
  • Faycel ABIDI

    Article intéressant.

    C’est vrai que l’idée de rajouter un cache est devenue récurrente pour optimiser les performances d’une application à forte sollicitation.
    Mais il faut bien étudier ou il est utile de mettre un cache et l’utiliser avec précaution surtout dans des environnements clusterisés .
    Ci-dessous quelques points à bien étudier avant de mettre en place un cache pour une haute disponibilité :
    – La stratégies d’éviction, expiration d’objets
    – Type de cache ( local, répliqué, distribué, invalidé, etc … ) ==> les performances dépendent de la solution choisie
    – Type de notification ( synchrone ou asynchrone ) pour la mise à jour des caches distants ( par copie du différentiel ou par invalidation )
    – Types de transactions (Pessimiste ou Optimiste), détection du deadlock,…
    – Commit à une phase ou à deux phases ( base de données XA ), récupération des transactions : attention à la cohérence de données
    – Pour le cache de second niveau ( type de cache : cache d’entity ou de requête : à utiliser avec précaution) : attention aux performances
    – Le type de diffusion (brodcast) (sur TCP ou sur UDP ) : dépend du nombre de noeud
    – etc …

    @Quelques solutions qui gèrent bien le cache distribué (Infinispan, memcached, terracotta, … )

    Conclusion : Un caching mal étudié et mal configuré peut être inefficace

    Répondre
  • Aouissaoui

    Merci pour l’article.
    C’est bien de penser aussi à la partage du cache entre plusieurs serveurs.
    Par exemple si vous avez une application déployée sur deux tomcats.
    Ça serait bien de partager un seul cache au lieu d’utiliser deux caches sur chaque serveur.

    Répondre
  • Un petit conseil pour la config Ehcache, changez la ligne

    xsi:noNamespaceSchemaLocation= »ehcache.xsd »

    en

    xsi:noNamespaceSchemaLocation= »http://ehcache.org/ehcache.xsd« 

    Ceci ne change pas le fonctionnement du cache mais ça vous permettra de valider votre configuration lors de l’édition et être sûr d’avoir une configuration correcte pour la version la plus récente d’Ehcache.

    Répondre
  • Bon article.

    J’ajoute, pour les caches sans support de stockage. Plutôt que de retirer les déclarations de cache, on peut brancher dans un cache simple qui oblige les méthodes mises en cache à s’exécuter à chaque fois:
    Utiliser la propriété « fallbackToNoOpCache » du bean « org.springframework.cache.support.CompositeCacheManager »

       <property name="fallbackToNoOpCache" value="true"/>
    Répondre

Répondre à AurelienAnnuler la réponse.

Ce site utilise Akismet pour réduire les indésirables. En savoir plus sur comment les données de vos commentaires sont utilisées.

En savoir plus sur Blog Zenika

Abonnez-vous pour poursuivre la lecture et avoir accès à l’ensemble des archives.

Continue reading