One of the greatest capabilities of HazelCast is the support for Hibernate’s second level cache.
JPA has two levels of caches. The first level cache caches an object’s state for the duration of a transaction. By querying the same object twice, you have to get the object you retrieved the first time. However, in case of complex queries, which include the object you retrieved and access your database, chances are that the results would be out of sync, since they will not reflect the changes you applied to the object in memory during the transaction. However, you can tackle this with flush(). Once a JPA session is initiated, its first-level cache is restricted to that session — it will not affect other sessions. A first-level cache is required as a part of JPA.
The second-level cache, on the contrary, is associated with the Session Factory, so thus the second-level cache is shared across sessions. Commonly used data can be stored in memory and retrieved faster.
Once you have the second level cache enabled, Hibernate will cache the entities retrieved in a Hibernate region. To do so, you have to set your entities as cacheable. Under the hood, the information that resides in an entity is cached in a dehydrated format.
Hazelcast can be used with second-level caches in two forms of architectures: client-server or cluster-only architecture.
To start, we will investigate a cluster-only architecture.
Hazelcast creates a separate, distributed map for each Hibernate cache region, therefore an entity. You can easily configure these regions via Hazelcast's map configuration. The name of the region has a corresponding Hazelcast map. For example, one of our entities is called User and the full package path is ‘com.gkatzioura.User’. So, our Hazelcast will have the name ‘com.gkatzioura.User’. Provided that this map is distributed across Hazelcast nodes, once the entity is retrieved from one node, the cached information will be shared to other Hazelcast nodes. Once an entity gets updated in a node, the cached information will be invalidated in the other nodes.
Hibernate also provides us with query caching. A query cache caches query results. For example, take this jpql query:
SELECT usr.username,usr.firstname FROM User usr
The cached result would be a map with a key composed of the query, the parameters, and the value the results retrieved. In the previous cache, the data retrieved included primitive values, and they are stored as-is.
However, there are cases in which a query might retrieve entities. For example:
SELECT c FROM Customer c
In such cases, instead of storing all the information retrieved, the entities are retrieved and cached in the second-level cache, while the query cache has an entry using the query and its parameters as a key and the entity ids as the value.
Once the same query is issued again, the query cache will fetch the ids and lookup the second-level cache for the corresponding entities. If an entity does not exist in the second-level cache, then a query is issued in order to fetch the missing entity.
When it comes to the second level-cache and query cache configuration, we need to pay attention to the eviction mechanisms of the second-level cache and query cache. You might stumble when it comes to ids being cached in the query cache, but their corresponding entities are evicted from the second-level cache. In such cases, there is a performance hit, as Hibernate will issue a query for each missing entity.
Hazelcast has support for the second-level cache, but it is local to the node and never distributed across Hazelcast clusters.
Although the results fetched from a query remain on the specific node, the entities specified from the cached query shall be retrieved from the distributed map, which is used as a second-level cache.
This is the theory we need so far. Next, we'll tackle some Spring Data JPA code and Hazelcast configurations.