Cache 2nd niveau Hibernate
Un article de ODcWiki.
| |
Ce document décrit la mise en place et la configuration d'un cache de second niveau dans une application utilisant Hibernate.
Un cache est une solution pour optimiser les accès aux données, mais ce n'est pas le remède miracle pour pallier à des problèmes de performances. La mise en place d'un tel cache doit être étudiée avec soin, car d'une part, employé à mauvais escient, cela peut dégrader les performances ou engendrer des problèmes d'accès concurrent. Et d'autre part, les problèmes peuvent parfois être résolus différemment, je pense en particulier aux problèmes de N+1.
Elle s'inscrit dans le cadre de l'optimisation des performances d'une application Java EE 5 qui s'exécute dans le serveur d'applications JBoss 4.2. Celui-ci utilise Hibernate 3.2 comme implémentation de JPA.
Il faut remarquer que le standard JPA/EJB 3 Entités ne spécifie rien concernant ce type de cache. Ce qui suit est donc spécifique à Hibernate et non portable vers d'autres implémentations (TopLink, Kodo...) bien que des mécanismes similaires existent souvent.
Sommaire |
Présentation
Dans une application JPA, 3 types d'actions peuvent déclencher une requête SQL de type Select :
- Une recherche d'entité par sa clé primaire (entityManager.find(...));
- Le parcourt d'une relation Lazy (chargement à la demande);
- L'exécution d'un requête EJBQL/HQL (entityManager.createQuery(...), entityManager.createNamedQuery(...)) ou, ce qui revient au même, l'utilisation des Criteria (spécifique Hibernate).
L'objectif du cache est d'éviter ces requêtes Select en conservant leur résultat en mémoire. Hibernate utilise pour cela deux niveaux de cache :
- Le cache de premier niveau est l'EntityManager (ou la Session). Sa durée de vie est courte, elle correspond à l'unité d'œuvre (premier appel de service sur un EJB Session, traitement d'un message pour EJB Message-driven...). La présence de ce cache est obligatoire (norme JPA) et son utilisation systématique afin de garantir l'intégrité de la transaction en cours.
- Le cache de second niveau est global à une application, il est commun à toutes les unités d'œuvre (EntityManager). Sa durée de vie est longue, mais la durée de vie des objets qu'il contient est paramétrable. La présence de ce cache est optionnelle (spécifique Hibernate) et son utilisation facultative.
Lorsque l'application a besoin d'une information, elle va rechercher successivement dans le cache de premier et second niveau avant d'exécuter, en dernier ressort, la requête SQL :
Les caches sont éventuellement enrichis et/ou mis à jour au passage de sorte que l'appel identique suivant profite du cache.
Hibernate permet de choisir entre plusieurs implémentations de cache de second niveau :
- Hashtable : implémentation simpliste à ne pas utiliser en production
- EHCache : cache complet utilisable aussi bien dans les environnements Java SE que Java EE.
- JBoss Tree Cache : cache complet qui fonctionne en cluster et nécessite un gestionnaire de transactions (environnement Java EE uniquement). Il peut aussi être utilisé pour le cache des sessions HTTP.
Configuration de l'application
Le cache de second niveau peut contenir 3 types d'éléments qui sont utiles dans des scénarios différents et se configurent différemment :
- Class Cache contient des entités seules;
- Collection Cache contient des collections d'entités;
- Query Cache contient des résultats de requêtes.
On peut configurer ce cache en ajoutant des annotations org.hibernate.annotations.Cache et org.hibernate.annotations.Query dans le code Java ou bien en ajoutant des propriétés Hibernate dans le fichier de configuration persistence.xml.
Class Cache
Ce cache est utilisé pour les recherches par clé primaire (entityManager.find(...)) et le parcourt de relation ???ToOne (OneToOne ou ManyToOne) Lazy. On spécifie quelle entité doit être mise dans le cache et quelle stratégie appliquer, par annotations :
import org.hibernate.annotations.Cache; import org.hibernate.annotations.CacheConcurrencyStrategy; @Entity @Cache(usage=CacheConcurrencyStrategy.TRANSACTIONAL) public class MonEntite { ... }
Ou bien par configuration dans le persistence.xml :
<property
name="hibernate.ejb.classcache.com.objetdirect.monappli.MonEntite"
value="transactional"/>
Avec JBoss Cache on dispose de 2 stratégies :
- read-only
- pour des entités en lecture seule;
- transactional
- pour des entités en lecture/écriture transactionnelle (tous les membres du cluster participent à la transaction).
D'autres implémentations du cache de second niveau (EHCache notamment) proposent d'autres stratégies (read-write, nonstrict-read-write), mais elles ne sont pas gérées par JBoss Tree Cache et ne s'appliquent pas.
Collection Cache
Ce cache est utilisé pour le parcourt des relations ???ToMany (OneToMany ou ManyToMany) Lazy On spécifie quelle relation ?-N doit être mise dans le cache et quelle stratégie appliquer, par annotations :
@Cache(usage=CacheConcurrencyStrategy.TRANSACTIONAL) @OneToMany(mappedBy="monEntite") private List<AutreEntite> autresEntites=new ArrayList<AutreEntite>();
Ou bien par configuration dans le persistence.xml :
<property
name="hibernate.ejb.collectioncache.com.objetdirect.monappli.MonEntite.autresEntites"
value="transactional"/>
Les stratégies read-only et transactional s'appliquent ici aussi de la même manière que précédemment.
Query Cache
Ce cache est utilisé pour l'exécution des requêtes EJBQL/HQL (entityManager.createQuery, entityManager.createNamedQuery...) et Criteria (spécifique Hibernate). Un tel cache n'est efficace que si les paramètres de requête sont les mêmes. Il s'active de manière globale pour toutes les requêtes en ajoutant la propriété dans le persistence.xml :
<property
name="hibernate.cache.use_query_cache"
value="true"/>
JBoss Tree Cache
JBoss Tree Cache est un service du serveur d'applications JBoss qui présente les caractéristiques suivantes :
- se présente sous la forme d'un MBean (Managed Bean JMX);
- s'enregistre dans l'annuaire JNDI;
- utilise le gestionnaire de transaction JTA (TransactionManager);
- s'appuie sur JGroups pour la réplication du cache sur plusieurs nœuds d'un cluster;
- peut-être commun à plusieurs applications déployées dans un même serveur;
- peut-être instancié plusieurs fois dans un même serveur.
Installation
On part d'une installation de JBoss 4.2 standard utilisant la configuration nommée default. Celle-ci n'inclut pas le cache de second niveau JBoss Tree Cache, on va prendre quelques éléments dans la configuration all :
- 2 librairies : jboss-cache et jgroups;
- 1 fichier de configuration :
Pour indiquer à Hibernate quelle implémentation du cache de second niveau utiliser, on ajoute dans le fichier de configuration persistence.xml :
<persistence ... version="1.0"> <persistence-unit ...> ... <properties> ... <property name="hibernate.cache.provider_class" value="org.jboss.ejb3.entity.TreeCacheProviderHook"/> <property name="hibernate.treecache.mbean.object_name" value="jboss.cache:service=EJB3EntityTreeCache"/> </properties> </persistence-unit> </persistence>
Configuration
La configuration du service se trouve pour l'instant dans le fichier ejb3-entity-cache-service.xml , il décrit un MBean de type org.jboss.cache.TreeCache.
Mode
L'attribut CacheMode du MBean décrit un mode de fonctionnement :
- LOCAL
- local, un seul serveur;
- REPL_SYNC, REPL_ASYNC
- en cluster, tous les nœuds possèdent une copie du cache avec les mêmes informations, ils doivent répliquer les changements.
- INVALIDATION_SYNC, INVALIDATION_ASYNC
- en cluster, à un instant donné, un nœud est propriétaire d'une information, pour le devenir il peut invalider un autre nœud.
Éviction des objets inutilisés
Le mécanisme de purge appelé éviction supprime les objets inutilisés du cache pour libérer de la mémoire. Plusieurs politiques de purge du cache sont utilisables :
- LRU (Least Recently Used)
- Les objets les moins utilisés et les plus anciens sont supprimés en priorité. C'est la politique utilisée par défaut.
- FIFO (First In, First Out)
- Le premier objet mis en cache est supprimé en priorité.
- MRU (Most Recently Used)
- Les objets les plus utilisés et les plus anciens sont supprimés en priorité.
- LFU (Least Frequently Used)
- Les objets les moins utilisés sont supprimés en priorité.
Chaque politique se configure différemment, et possède ses propres attributs :
| Attribut | Description | LRU | FIFO | MRU | LFU |
|---|---|---|---|---|---|
| maxNodes | Nombre maximum de nœuds | X | X | X | X |
| minNodes | Nombre minimum de nœuds | X | |||
| timeToLiveSeconds | Durée d'inactivité maximale (en secondes) | X | |||
| maxAgeSeconds | Durée de vie maximale (en secondes) | X |
L'attribut EvictionPolicyConfig du MBean contient une liste de régions. Chaque région correspond à une entité ou un groupe d'entité (les entités d'un même package par exemple) et peut avoir une politique différente et un paramétrage de cette politique différent :
<attribute name="EvictionPolicyConfig"> <config> <attribute name="wakeUpIntervalSeconds">5</attribute> <!-- Région et configuration par défaut --> <region name="/_default_"> <attribute name="maxNodes">5000</attribute> <attribute name="timeToLiveSeconds">1000</attribute> </region> <region name="/com/objetdirect/monappli/monpackage1" policyClass="org.jboss.cache.eviction.FIFOPolicy"> <attribute name="maxNodes">5000</attribute> </region> <region name="/com/objetdirect/monappli/monpackage2" policyClass="org.jboss.cache.eviction.MRUPolicy"> <attribute name="maxNodes">10000</attribute> </region> <region name="/com/objetdirect/monappli"> <attribute name="maxNodes">1000</attribute> <attribute name="timeToLiveSeconds">5</attribute> </region> </config> </attribute>
L'attribut wakeUpIntervalSeconds décrit la période à laquelle va se réveiller le thread de purge. L'ordre des régions est important, lorsque des régions sont imbriquées, il faut mettre les régions filles avant les régions mères (du plus fin au plus grossier).
On peut forcer une entité ou une collection à se placer dans une région du cache donnée, par annotation :
@Cache(usage=CacheConcurrencyStrategy.TRANSACTIONAL,region="MaRegion")
Ou bien par configuration :
<property name="hibernate.ejb.classcache.com.objetdirect.monappli.MonEntite" value="transactional,MaRegion"/> <property name="hibernate.ejb.classcache.com.objetdirect.monappli.MonEntite.autresEntites" value="transactional,MaRegion"/>
Le cache de requêtes utilise une région particulière nommée org.hibernate.cache.StandardQueryCache.
Supervision
On peut activer et surveiller les statistiques du cache JBoss en ouvrant une console d'administration (http://localhost:8080/web-console) et en se rendant sur la page du MBean jboss.cache:service=EJB3EntityTreeCache,treecache-interceptor=CacheMgmtInterceptor (dans l'arbre de navigation System, JMX MBeans, jboss.cache)
Si les statistiques du cache sont activées, les attributs du MBean donnent des statistiques :
| Attribut | Description |
|---|---|
| Hits | Nombre d'utilisations fructueuses |
| Misses | Nombre d'utilisations infructueuses |
| Evictions | Nombre d'évictions |
| NumberOfAttributes | Nombre d'attributs dans le cache |
| NumberOfNodes | Nombre de nœuds dans le cache |
| ElapsedTime | Temps (en secondes) écoulé depuis le démarrage du cache |
| TimeSinceReset | Temps (en secondes) écoulé depuis la dernière ré-initialisation des statistiques |
| AverageWriteTime | Temps (en millisecondes) moyen d'une écriture |
| HitMissRatio | Ratio Hit/Miss |
| ReadWriteRatio | Ration lecture/écriture |
Références
- Documentation de référence Hibernate 3.2, paragraphes 19.2 à 19.5
- Guide Utilisateur Hibernate Entity Manager 3.3, paragraphe 2.2.2
- Documentation Utilisateur JBoss Tree Cache 1.4.1, paragraphe 6



