Ruhezustand Cache der zweiten Ebene

1. Übersicht

Einer der Vorteile von Datenbankabstraktionsschichten wie ORM-Frameworks (Object Relational Mapping) ist ihre Fähigkeit, Daten, die aus dem zugrunde liegenden Speicher abgerufen werden , transparent zwischenzuspeichern . Dies hilft, Datenbankzugriffskosten für Daten, auf die häufig zugegriffen wird, zu eliminieren.

Leistungssteigerungen können erheblich sein, wenn die Lese- / Schreibverhältnisse von zwischengespeicherten Inhalten hoch sind, insbesondere für Entitäten, die aus großen Objektgraphen bestehen.

In diesem Artikel untersuchen wir den Cache der zweiten Ebene im Ruhezustand.

Wir erklären einige grundlegende Konzepte und veranschaulichen wie immer alles anhand einfacher Beispiele. Wir verwenden JPA und greifen nur für die Funktionen, die in JPA nicht standardisiert sind, auf die native Hibernate-API zurück.

2. Was ist ein Cache der zweiten Ebene?

Wie die meisten anderen voll ausgestatteten ORM-Frameworks verfügt Hibernate über das Konzept des Caches der ersten Ebene. Es handelt sich um einen Cache mit Sitzungsbereich, der sicherstellt, dass jede Entitätsinstanz im persistenten Kontext nur einmal geladen wird.

Sobald die Sitzung geschlossen ist, wird auch der Cache der ersten Ebene beendet. Dies ist tatsächlich wünschenswert, da gleichzeitige Sitzungen mit voneinander isolierten Entitätsinstanzen arbeiten können.

Auf der anderen Seite hat der Cache der zweiten Ebene einen Sitzungsbereich, dh er wird von allen Sitzungen gemeinsam genutzt, die mit derselben Sitzungsfactory erstellt wurden. Wenn eine Entitätsinstanz anhand ihrer ID nachgeschlagen wird (entweder anhand der Anwendungslogik oder intern im Ruhezustand, z. B. wenn sie Zuordnungen von anderen Entitäten zu dieser Entität lädt) und das Caching der zweiten Ebene für diese Entität aktiviert ist, geschieht Folgendes:

  • Wenn eine Instanz bereits im Cache der ersten Ebene vorhanden ist, wird sie von dort zurückgegeben
  • Wenn eine Instanz nicht im Cache der ersten Ebene gefunden wird und der entsprechende Instanzstatus im Cache der zweiten Ebene zwischengespeichert wird, werden die Daten von dort abgerufen und eine Instanz wird zusammengestellt und zurückgegeben
  • Andernfalls werden die erforderlichen Daten aus der Datenbank geladen und eine Instanz zusammengestellt und zurückgegeben

Sobald die Instanz im Persistenzkontext (Cache der ersten Ebene) gespeichert ist, wird sie bei allen nachfolgenden Aufrufen innerhalb derselben Sitzung von dort zurückgegeben, bis die Sitzung geschlossen oder die Instanz manuell aus dem Persistenzkontext entfernt wird. Außerdem wird der Status der geladenen Instanz im L2-Cache gespeichert, sofern er noch nicht vorhanden war.

3. Region Factory

Das Caching der zweiten Ebene im Ruhezustand ist so konzipiert, dass der tatsächlich verwendete Cache-Anbieter nicht bekannt ist. Hibernate muss nur mit einer Implementierung der Schnittstelle org.hibernate.cache.spi.RegionFactory bereitgestellt werden, die alle Details enthält, die für die tatsächlichen Cache-Anbieter spezifisch sind. Grundsätzlich fungiert es als Brücke zwischen dem Ruhezustand und den Cache-Anbietern.

In diesem Artikel verwenden wir Ehcache als Cache-Anbieter , einen ausgereiften und weit verbreiteten Cache. Sie können natürlich jeden anderen Anbieter auswählen, sofern eine RegionFactory dafür implementiert ist.

Wir fügen dem Klassenpfad die Implementierung der Ehcache-Region-Factory mit der folgenden Maven-Abhängigkeit hinzu:

 org.hibernate hibernate-ehcache 5.2.2.Final 

Hier finden Sie die neueste Version von hibernate-ehcache . Stellen Sie jedoch sicher , dass Hibernate-ehcache Version zu Hibernate Version entspricht , die Sie in Ihrem Projekt verwenden, zB bei Verwendung von Hibernate-ehcache 5.2.2.Final wie in diesem Beispiel, dann ist die Version von Hibernate auch sein sollte 5.2.2. Finale .

Das Hibernate-Ehcache- Artefakt hängt von der Ehcache-Implementierung selbst ab, die somit auch transitiv im Klassenpfad enthalten ist.

4. Aktivieren des Caching der zweiten Ebene

Mit den folgenden zwei Eigenschaften teilen wir Hibernate mit, dass das L2-Caching aktiviert ist, und geben ihm den Namen der Factory-Klasse der Region:

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

In persistence.xml würde es beispielsweise so aussehen:

 ...   ... 

Um das Caching der zweiten Ebene zu deaktivieren (z. B. zu Debugging-Zwecken), setzen Sie einfach die Eigenschaft hibernate.cache.use_second_level_cache auf false.

5. Eine Entität zwischenspeicherbar machen

Um eine Entität für das Caching der zweiten Ebene zu qualifizieren , kommentieren wir sie mit Hibernate-spezifischen @ org.hibernate.annotations.Cache- Annotationen und geben eine Cache-Parallelitätsstrategie an.

Einige Entwickler halten es für eine gute Konvention, auch die standardmäßige Annotation @ javax.persistence.Cacheable hinzuzufügen (obwohl dies von Hibernate nicht benötigt wird), sodass eine Implementierung einer Entitätsklasse möglicherweise folgendermaßen aussieht:

@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 }

Für jede Entitätsklasse verwendet Hibernate einen separaten Cache-Bereich, um den Status von Instanzen für diese Klasse zu speichern. Der Regionsname ist der vollständig qualifizierte Klassenname.

Beispielsweise werden Foo- Instanzen in einem Cache mit dem Namen com.baeldung.hibernate.cache.model.Foo in Ehcache gespeichert .

Um zu überprüfen, ob das Caching funktioniert, schreiben wir möglicherweise einen kurzen Test wie folgt:

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

Hier verwenden wir die Ehcache-API direkt, um zu überprüfen, ob der Cache com.baeldung.hibernate.cache.model.Foo nach dem Laden einer Foo- Instanz nicht leer ist .

Sie können auch die Protokollierung von SQL aktivieren, das von Hibernate generiert wurde, und fooService.findOne (foo.getId ()) mehrmals im Test aufrufen, um zu überprüfen, ob die select- Anweisung zum Laden von Foo nur einmal (beim ersten Mal) gedruckt wird ruft die Entitätsinstanz auf, die aus dem Cache abgerufen wird.

6. Cache-Parallelitätsstrategie

Based on use cases, we are free to pick one of the following cache concurrency strategies:

  • READ_ONLY: Used only for entities that never change (exception is thrown if an attempt to update such an entity is made). It is very simple and performant. Very suitable for some static reference data that don't change
  • NONSTRICT_READ_WRITE: Cache is updated after a transaction that changed the affected data has been committed. Thus, strong consistency is not guaranteed and there is a small time window in which stale data may be obtained from cache. This kind of strategy is suitable for use cases that can tolerate eventual consistency
  • READ_WRITE: This strategy guarantees strong consistency which it achieves by using ‘soft' locks: When a cached entity is updated, a soft lock is stored in the cache for that entity as well, which is released after the transaction is committed. All concurrent transactions that access soft-locked entries will fetch the corresponding data directly from database
  • TRANSACTIONAL: Cache changes are done in distributed XA transactions. A change in a cached entity is either committed or rolled back in both database and cache in the same XA transaction

7. Cache Management

If expiration and eviction policies are not defined, the cache could grow indefinitely and eventually consume all of available memory. In most cases, Hibernate leaves cache management duties like these to cache providers, as they are indeed specific to each cache implementation.

For example, we could define the following Ehcache configuration to limit the maximum number of cached Foo instances to 1000:

8. Collection Cache

Collections are not cached by default, and we need to explicitly mark them as cacheable. For example:

@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. Internal Representation of Cached State

Entities are not stored in second-level cache as Java instances, but rather in their disassembled (hydrated) state:

  • Id (primary key) is not stored (it is stored as part of the cache key)
  • Transient properties are not stored
  • Collections are not stored (see below for more details)
  • Non-association property values are stored in their original form
  • Only id (foreign key) is stored for ToOne associations

This depicts general Hibernate second-level cache design in which cache model reflects the underlying relational model, which is space-efficient and makes it easy to keep the two synchronized.

9.1. Internal Representation of Cached Collections

We already mentioned that we have to explicitly indicate that a collection (OneToMany or ManyToMany association) is cacheable, otherwise it is not cached.

Actually, Hibernate stores collections in separate cache regions, one for each collection. The region name is a fully qualified class name plus the name of collection property, for example: com.baeldung.hibernate.cache.model.Foo.bars. This gives us the flexibility to define separate cache parameters for collections, e.g. eviction/expiration policy.

Also, it is important to mention that only ids of entities contained in a collection are cached for each collection entry, which means that in most cases it is a good idea to make the contained entities cacheable as well.

10. Cache Invalidation for HQL DML-Style Queries and Native Queries

When it comes to DML-style HQL (insert, update and delete HQL statements), Hibernate is able to determine which entities are affected by such operations:

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

In this case all Foo instances are evicted from L2 cache, while other cached content remains unchanged.

However, when it comes to native SQL DML statements, Hibernate cannot guess what is being updated, so it invalidates the entire second level cache:

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

This is probably not what you want! The solution is to tell Hibernate which entities are affected by native DML statements, so that it can evict only entries related to Foo entities:

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

We have too fall back to Hibernate native SQLQuery API, as this feature is not (yet) defined in JPA.

Note that the above applies only to DML statements (insert, update, delete and native function/procedure calls). Native select queries do not invalidate cache.

11. Query Cache

Results of HQL queries can also be cached. This is useful if you frequently execute a query on entities that rarely change.

To enable query cache, set the value of hibernate.cache.use_query_cache property to true:

hibernate.cache.use_query_cache=true

Then, for each query you have to explicitly indicate that the query is cacheable (via an org.hibernate.cacheable query hint):

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

11.1. Query Cache Best Practices

Here are a some guidelines and best practices related to query caching:

  • As is case with collections, only ids of entities returned as a result of a cacheable query are cached, so it is strongly recommended that second-level cache is enabled for such entities.
  • There is one cache entry per each combination of query parameter values (bind variables) for each query, so queries for which you expect lots of different combinations of parameter values are not good candidates for caching.
  • Queries that involve entity classes for which there are frequent changes in the database are not good candidates for caching either, because they will be invalidated whenever there is a change related to any of the entity classed participating in the query, regardless whether the changed instances are cached as part of the query result or not.
  • By default, all query cache results are stored in org.hibernate.cache.internal.StandardQueryCache region. As with entity/collection caching, you can customize cache parameters for this region to define eviction and expiration policies according to your needs. For each query you can also specify a custom region name in order to provide different settings for different queries.
  • For all tables that are queried as part of cacheable queries, Hibernate keeps last update timestamps in a separate region named org.hibernate.cache.spi.UpdateTimestampsCache. Being aware of this region is very important if you use query caching, because Hibernate uses it to verify that cached query results are not stale. The entries in this cache must not be evicted/expired as long as there are cached query results for the corresponding tables in query results regions. It is best to turn off automatic eviction and expiration for this cache region, as it does not consume lots of memory anyway.

12. Conclusion

In diesem Artikel haben wir uns angesehen, wie Sie den Cache der zweiten Ebene im Ruhezustand einrichten. Wir haben gesehen, dass die Konfiguration und Verwendung ziemlich einfach ist, da Hibernate hinter den Kulissen alles daran setzt, die Cache-Nutzung der zweiten Ebene für die Geschäftslogik der Anwendung transparent zu machen.

Die Implementierung dieses Cache-Tutorials für den Ruhezustand der zweiten Ebene ist auf Github verfügbar. Dies ist ein Maven-basiertes Projekt, daher sollte es einfach zu importieren und auszuführen sein.