Cache de second niveau Hibernate

Hibernate cache de second niveau

1. Vue d'ensemble

L'un des avantages des couches d'abstraction de base de données telles que les frameworks ORM (object-relational mapping) est leurability to transparently cache data récupéré à partir du magasin sous-jacent. Cela permet d’éliminer les coûts d’accès à la base de données pour les données fréquemment consultées.

Les gains de performance peuvent être importants si les taux de lecture / écriture du contenu mis en cache sont élevés, en particulier pour les entités constituées de graphiques d'objets de grande taille.

Dans cet article, nous explorons le cache de deuxième niveau Hibernate.

Nous expliquons quelques concepts de base et, comme toujours, nous illustrons tout avec des exemples simples. Nous utilisons JPA et utilisons l'API native d'Hibernate uniquement pour les fonctionnalités non normalisées dans JPA.

2. Qu'est-ce qu'un cache de deuxième niveau?

Comme la plupart des autres infrastructures ORM entièrement équipées, Hibernate utilise le concept de cache de premier niveau. Il s'agit d'un cache de portée de session qui garantit que chaque instance d'entité n'est chargée qu'une seule fois dans le contexte persistant.

Une fois la session fermée, le cache de premier niveau est également terminé. Cela est en fait souhaitable, car cela permet aux sessions simultanées de travailler avec des instances d'entité indépendamment les unes des autres.

D'un autre côté, le cache de second niveau a une portéeSessionFactory, ce qui signifie qu'il est partagé par toutes les sessions créées avec la même fabrique de sessions. Lorsqu'une instance d'entité est recherchée par son identifiant (soit par la logique d'application, soit par Hibernate en interne,e.g. lorsqu'elle charge des associations vers cette entité à partir d'autres entités), et si la mise en cache de second niveau est activée pour cette entité, ce qui suit se produit:

  • Si une instance est déjà présente dans le cache de premier niveau, elle est renvoyée à partir de là.

  • Si une instance est introuvable dans le cache de premier niveau et que l'état de l'instance correspondante est mis en cache dans le cache de second niveau, les données sont extraites à partir de là et une instance est assemblée et renvoyée.

  • Sinon, les données nécessaires sont chargées à partir de la base de données et une instance est assemblée et renvoyée.

Une fois que l'instance est stockée dans le contexte de persistance (cache de premier niveau), elle est renvoyée depuis tous les appels ultérieurs de la même session jusqu'à sa fermeture ou jusqu'à ce que l'instance soit manuellement évacuée du contexte de persistance. En outre, l'état de l'instance chargée est stocké dans le cache L2 s'il n'y était pas déjà.

3. Usine de région

La mise en cache de second niveau d'Hibernate est conçue pour ignorer le fournisseur de cache utilisé. Hibernate n'a besoin que d'une implémentation de l'interfaceorg.hibernate.cache.spi.RegionFactory qui encapsule tous les détails spécifiques aux fournisseurs de cache réels. Fondamentalement, il agit comme un pont entre Hibernate et les fournisseurs de cache.

Dans cet article,we use Ehcache as a cache provider, qui est un cache mature et largement utilisé. Vous pouvez bien sûr choisir n'importe quel autre fournisseur, tant qu'il existe une implémentation d'unRegionFactory pour celui-ci.

Nous ajoutons l'implémentation de fabrique de région Ehcache au classpath avec la dépendance Maven suivante:


    org.hibernate
    hibernate-ehcache
    5.2.2.Final

Take a look here pour la dernière version dehibernate-ehcache. Cependant, assurez-vous que la version dehibernate-ehcache est égale à la version d'Hibernate que vous utilisez dans votre projet,e.g. si vous utilisezhibernate-ehcache 5.2.2.Final comme dans cet exemple, alors la version d'Hibernate devrait également être5.2.2.Final.

L'artefacthibernate-ehcache dépend de l'implémentation Ehcache elle-même, qui est donc également incluse de manière transitoire dans le chemin de classe.

4. Activation de la mise en cache de second niveau

Avec les deux propriétés suivantes, nous disons à Hibernate que la mise en cache L2 est activée et lui donnons le nom de la classe de fabrique de région:

hibernate.cache.use_second_level_cache=true
hibernate.cache.region.factory_class=org.hibernate.cache.ehcache.EhCacheRegionFactory

Par exemple, danspersistence.xml, cela ressemblerait à:


    ...
    
    
    ...

Pour désactiver la mise en cache de deuxième niveau (à des fins de débogage par exemple), définissez simplement la propriétéhibernate.cache.use_second_level_cache sur false.

5. Rendre une entité cachable

Pourmake an entity eligible for second-level caching, nous l'annotons avec l'annotation@org.hibernate.annotations.Cache spécifique à Hibernate et spécifions uncache concurrency strategy.

Certains développeurs considèrent que c'est une bonne convention d'ajouter également l'annotation standard@javax.persistence.Cacheable (bien que non requise par Hibernate), donc une implémentation de classe d'entité pourrait ressembler à ceci:

@Entity
@Cacheable
@org.hibernate.annotations.Cache(usage = CacheConcurrencyStrategy.READ_WRITE)
public class Foo {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    @Column(name = "ID")
    private long id;

    @Column(name = "NAME")
    private String name;

    // getters and setters
}

Hibernate utilisera une région de cache distincte pour stocker l'état des instances de cette classe pour chaque classe d'entité. Le nom de la région est le nom complet de la classe.

Par exemple, les instancesFoo sont stockées dans un cache nommécom.example.hibernate.cache.model.Foo dans Ehcache.

Pour vérifier que la mise en cache fonctionne, nous pouvons écrire un test rapide comme ceci:

Foo foo = new Foo();
fooService.create(foo);
fooService.findOne(foo.getId());
int size = CacheManager.ALL_CACHE_MANAGERS.get(0)
  .getCache("com.example.hibernate.cache.model.Foo").getSize();
assertThat(size, greaterThan(0));

Ici, nous utilisons l'API Ehcache directement pour vérifier que le cache decom.example.hibernate.cache.model.Foo n'est pas vide après le chargement d'une instance deFoo.

Vous pouvez également activer la journalisation du SQL généré par Hibernate et appelerfooService.findOne(foo.getId()) plusieurs fois dans le test pour vérifier que l'instructionselect pour le chargement deFoo est imprimée une seule fois (la première fois), ce qui signifie que lors des appels suivants, l'instance d'entité est extraite du cache.

6. Stratégie de concurrence du cache

Sur la base de cas d'utilisation, nous sommes libres de choisir l'une des stratégies de simultanéité de cache suivantes:

  • READ_ONLY: utilisé uniquement pour les entités qui ne changent jamais (une exception est levée si une tentative de mise à jour d'une telle entité est effectuée). C'est très simple et performant. Très approprié pour certaines données de référence statiques qui ne changent pas

  • NONSTRICT_READ_WRITE: le cache est mis à jour après qu'une transaction qui a modifié les données affectées a été validée. Par conséquent, une cohérence élevée n'est pas garantie et il existe une petite fenêtre temporelle dans laquelle des données obsolètes peuvent être obtenues à partir du cache. Ce type de stratégie convient aux cas d'utilisation pouvant tolérer une éventuelle cohérence.

  • READ_WRITE: Cette stratégie garantit une forte cohérence qu'elle atteint en utilisant des verrous `` logiciels '': lorsqu'une entité mise en cache est mise à jour, un verrou logiciel est également stocké dans le cache pour cette entité, qui est libéré une fois la transaction validée . Toutes les transactions simultanées qui accèdent à des entrées verrouillées par logiciel vont récupérer les données correspondantes directement à partir de la base de données

  • TRANSACTIONAL: les modifications du cache sont effectuées dans les transactions XA distribuées. Une modification dans une entité mise en cache est validée ou annulée à la fois dans la base de données et dans la mémoire cache dans la même transaction XA.

7. Gestion du cache

Si les stratégies d'expiration et d'expulsion ne sont pas définies, le cache pourrait s'agrandir indéfiniment et éventuellement utiliser toute la mémoire disponible. Dans la plupart des cas, Hibernate laisse les tâches de gestion du cache de ce type aux fournisseurs de cache, car ils sont en effet spécifiques à chaque implémentation de cache.

Par exemple, nous pourrions définir la configuration Ehcache suivante pour limiter le nombre maximum d'instancesFoo mises en cache à 1000:


    

8. Cache de collection

Les collections ne sont pas mises en cache par défaut et nous devons les marquer explicitement comme pouvant être mises en cache. Par exemple:

@Entity
@Cacheable
@org.hibernate.annotations.Cache(usage = CacheConcurrencyStrategy.READ_WRITE)
public class Foo {

    ...

    @Cacheable
    @org.hibernate.annotations.Cache(usage = CacheConcurrencyStrategy.READ_WRITE)
    @OneToMany
    private Collection bars;

    // getters and setters
}

9. Représentation interne de l'État mis en cache

Les entités ne sont pas stockées dans le cache de second niveau en tant qu'instances Java, mais dans leur état désassemblé (hydraté):

  • Id (clé primaire) n'est pas stocké (il est stocké dans la clé de cache)

  • Les propriétés transitoires ne sont pas stockées

  • Les collections ne sont pas stockées (voir ci-dessous pour plus de détails)

  • Les valeurs de propriété non-association sont stockées dans leur forme d'origine

  • Seul l'id (clé étrangère) est stocké pour les associationsToOne

Ceci décrit la conception de cache de second niveau d'Hibernate dans laquelle le modèle de cache reflète le modèle relationnel sous-jacent, qui optimise l'espace et facilite la synchronisation des deux.

9.1. Représentation interne des collections mises en cache

Nous avons déjà mentionné que nous devons indiquer explicitement qu'une collection (associationOneToMany ouManyToMany) peut être mise en cache, sinon elle n'est pas mise en cache.

En fait, Hibernate stocke les collections dans des régions de cache distinctes, une pour chaque collection. Le nom de la région est un nom de classe qualifié complet plus le nom de la propriété de collection, par exemple:com.example.hibernate.cache.model.Foo.bars. Cela nous donne la flexibilité de définir des paramètres de cache séparés pour les collections, la politique d'éviction / d'expiration dee.g..

De plus, il est important de mentionner que seuls les identifiants des entités contenues dans une collection sont mis en cache pour chaque entrée de la collection, ce qui signifie que dans la plupart des cas, il est également judicieux de rendre les entités contenues pouvant être mises en cache.

10. Invalidation du cache pour les requêtes de style HQL DML et les requêtes natives

En ce qui concerne les instructions HQL de style DML (instructions HQLinsert,update etdelete), Hibernate est capable de déterminer quelles entités sont affectées par de telles opérations:

entityManager.createQuery("update Foo set … where …").executeUpdate();

Dans ce cas, toutes les instances Foo sont expulsées du cache N2, tandis que les autres contenus mis en cache restent inchangés.

Cependant, lorsqu'il s'agit d'instructions DML SQL natives, Hibernate ne peut pas deviner ce qui est mis à jour, ce qui invalide tout le cache de second niveau:

session.createNativeQuery("update FOO set … where …").executeUpdate();

Ce n'est probablement pas ce que vous voulez! La solution consiste à indiquer à Hibernate quelles entités sont affectées par les instructions DML natives, afin qu'il ne puisse expulser que les entrées liées aux entitésFoo:

Query nativeQuery = entityManager.createNativeQuery("update FOO set ... where ...");
nativeQuery.unwrap(org.hibernate.SQLQuery.class).addSynchronizedEntityClass(Foo.class);
nativeQuery.executeUpdate();

Nous avons également recours à l'API native HibernateSQLQuery, car cette fonctionnalité n'est pas (encore) définie dans JPA.

Notez que ce qui précède s'applique uniquement aux instructions DML (insert,update,delete et appels de fonction / procédure native). Les requêtes natives deselectn'invalident pas le cache.

11. Cache de requête

Les résultats des requêtes HQL peuvent également être mis en cache. Ceci est utile si vous exécutez fréquemment une requête sur des entités qui changent rarement.

Pour activer le cache de requêtes, définissez la valeur de la propriétéhibernate.cache.use_query_cache surtrue:

hibernate.cache.use_query_cache=true

Ensuite, pour chaque requête, vous devez indiquer explicitement que la requête peut être mise en cache (via un indice de requêteorg.hibernate.cacheable):

entityManager.createQuery("select f from Foo f")
  .setHint("org.hibernate.cacheable", true)
  .getResultList();

11.1. Meilleures pratiques du cache de requêtes

Voici quelquesguidelines and best practices related to query caching:

  • Comme dans le cas des collections, seuls les identifiants des entités renvoyées à la suite d'une requête pouvant être mise en cache sont mis en cache. Il est donc vivement recommandé d'activer le cache de second niveau pour ces entités.

  • Il existe une entrée de cache pour chaque combinaison de valeurs de paramètre de requête (variables de liaison) pour chaque requête. Par conséquent, les requêtes pour lesquelles vous attendez de nombreuses combinaisons différentes de valeurs de paramètre ne sont pas de bons candidats pour la mise en cache.

  • Les requêtes impliquant des classes d'entités pour lesquelles il y a des modifications fréquentes dans la base de données ne constituent pas non plus un bon choix pour la mise en cache, car elles seront invalidées chaque fois qu'une modification est liée à l'une des entités classées participant à la requête, que les instances modifiées soient ou non mis en cache dans le résultat de la requête ou non.

  • Par défaut, tous les résultats du cache de requêtes sont stockés dans la régionorg.hibernate.cache.internal.StandardQueryCache. Comme pour la mise en cache d'entités / de collections, vous pouvez personnaliser les paramètres de cache pour cette région afin de définir des stratégies d'éviction et d'expiration en fonction de vos besoins. Pour chaque requête, vous pouvez également spécifier un nom de région personnalisé afin de fournir différents paramètres pour différentes requêtes.

  • Pour toutes les tables interrogées dans le cadre de requêtes pouvant être mises en cache, Hibernate conserve les horodatages de la dernière mise à jour dans une région distincte nomméeorg.hibernate.cache.spi.UpdateTimestampsCache. Être conscient de cette région est très important si vous utilisez la mise en cache des requêtes, car Hibernate l'utilise pour vérifier que les résultats des requêtes en cache ne sont pas périmés. Les entrées de ce cache ne doivent pas être expulsées / expirées tant qu'il y a des résultats de requête en cache pour les tables correspondantes dans les régions de résultats de requête. Il est préférable de désactiver l'expulsion et l'expiration automatiques pour cette région de cache, car elle ne consomme pas beaucoup de mémoire.

12. Conclusion

Dans cet article, nous avons étudié la procédure de configuration du cache de second niveau d'Hibernate. Nous avons constaté qu'il est assez facile à configurer et à utiliser, car Hibernate fait tout le travail lourd en coulisses pour rendre l'utilisation du cache de second niveau transparente à la logique métier de l'application.

L'implémentation de ce didacticiel Hibernate Cache de second niveau est disponibleon Github. Ceci est un projet basé sur Maven, il devrait donc être facile à importer et à exécuter tel quel.