The JPA and Hibernate second-level cache

Imagine having a tool that can automatically detect JPA and Hibernate performance issues. Wouldn’t that be just awesome?

Well, Hypersistence Optimizer is that tool! And it works with Spring Boot, Spring Framework, Jakarta EE, Java EE, Quarkus, or Play Framework.

So, enjoy spending your time on the things you love rather than fixing performance issues in your production system on a Saturday night!

You can earn a significant passive income stream from promoting my book, courses, tools, training, or coaching subscriptions.

If you're interested in supplementing your income, then join my affiliate program.

Introduction

In this article, I’m going to explain how the JPA and Hibernate second-level cache mechanism works and why they are very important when it comes to improving the performance of your data access layer.

JPA and Hibernate entity first-level and second-level cache

As I explained in this article, JPA and Hibernate feature a first-level cache as well. However, the first-level cache is bund to the currently executing Thread, so the cached entities cannot be shared by multiple concurrent requests.

On the other hand, the second-level cache is designed to be used by multiple concurrent requests, therefore increasing the likelihood of getting a cache hit.

When fetching a JPA entity:

Post post = entityManager.find(Post.class, 1L);

A Hibernate LoadEntityEvent is triggered, which is handled by the DefaultLoadEventListener like this:

Fetching and entity from the JPA and Hibernate first-level and second-level cache

First, Hibernate checks whether the first-level cache (a.k.a JPA EntityManager, Hibernate Session, or Persistence Context) already contains the entity, and if it does, the managed entity is returned.

If the JPA entity is not found in the first-level cache, Hibernate will check the second-level cache if it’s enabled.

If the entity cannot be fetched from the first or second-level cache, Hibernate will load it from the database using an SQL query. The JDBC ResultSet from the entity loading query is transformed into a Java Object[] that’s known as the entity loaded state.

The loaded state array is stored in the first-level cache along with the managed entity in order to help the Hibernate dirty checking mechanism discover if an entity has been modified:

JPA and Hibernate second-level cache entity loaded state

However, the very same entity loaded state is also what’s being loaded from the JPA and Hibernate second-level cache when bypassing the database.

The JPA and Hibernate second-level cache is the cache of the entity loaded state array, not of the actual entity object reference.

Why use the JPA and Hibernate second-level cache

Now that you have seen how the second level cache works when fetching entities, you might wonder why not fetch the entity directly from the database.

Scaling read-only transactions can be done fairly easily by adding more Replica nodes. However, that does not work for the Primary node since that can be only scaled vertically.

And that’s where the second-level cache comes into play. For read-write database transactions that need to be executed on the Primary node, the second-level cache can help you reduce the query load by directing it to the strongly consistent second-level cache:

Why use the JPA and Hibernate second-level cache

The JPA and Hibernate second-level cache can help you speed up read-write transactions by offloading the read traffic from the Primary node and serve it from the cache.

Scaling the JPA and Hibernate second-level cache

Traditionally, the second-level cache was stored in the memory of the application, and that was problematic for several reasons.

First, the application memory is limited, so the volume of data that can be cached is limited as well.

Second, when traffic increases and we want to start new application nodes to handle the extra traffic, the new nodes would start with a cold cache, making the problem even worse as they incur a spike in database load until the cache is populated with data:

JPA and Hibernate second-level cache per application node

To address this issue, it’s better to have the cache running as a distributed system, like Redis. This way, the amount of cached data is not limited by the memory size on a single node since sharding can be used to split the data among multiple nodes.

Scaling teh JPA and Hibernate second-level cache

And, when a new application node is added by the auto-scaler, the new node will load data from the same distributed cache. Hence, there’s no cold cache issue anymore.

JPA and Hibernate second-level cache options

There are several things that can be stored by the JPA and Hibernate second-level cache:

So, the second-level cache is not limited to fetching entities only.

If you enjoyed this article, I bet you are going to love my Book and Video Courses as well.

And there is more!

You can earn a significant passive income stream from promoting all these amazing products that I have been creating.

If you're interested in supplementing your income, then join my affiliate program.

Conclusion

The JPA and Hibernate second-level cache is very useful when having to scale rad-write transactions. Because the second-level cache is designed to be strongly consistent, you don’t have to worry that stale data is going to be served from the cache.

More, you don’t have to worry about keeping track of database modifications in order to schedule cache updates either because this is done transparently by Hibernate for you.

Transactions and Concurrency Control eBook

4 Comments on “The JPA and Hibernate second-level cache

  1. Hi Vlad,

    Thanks for all the hibernate and database articles that I’ve read and used over the years.

    We are in the process clustering our application which involves among other things picking an appropriate cache provider for hibernate. I’m curious if you have a little more info on your last suggestion to have a dedicated cache service like redis and have hibernate talk to that.

    My questions are:
    1) Wouldn’t naively connecting hibernate remote resource result incur and unbearable performance penalty? I don’t know how “chatty” hibernate is when talking to the cache provider but I imagine it is many calls per “transaction” (depending on how many entities are involved obviously).
    2) Given the question above, I’m assuming you are thinking of a cache provider that supports local caching. Do you have any suggestions here. We are thinking of Redis with Redisson PRO which supports local caching.
    3) Initially we though we would just use Infinispan with a cache invalidation strategy (to minimize network traffic). I think this is still a good approach provided the cache storage requirements don’t balloon too much. Infinispan might help us mitigate this with its off-heap storage solution. We just aren’t sure if Infinispan is actively used or how healthy the project is.

    Any thoughts and info would be very much appreciated.
    Cheers
    Oliver

    • Normally, you shouldn’t need the second-level cache, which is suitable only if you want to offload the Primary DB node. Most often, the 2nd-level cache is employed as a last resort for apps that have relied too much on the FetchType.EAGER strategy.

      As for options, I think that Infinispan is a good choice for a distributed cluster. I’m not aware of Redis with Redisson PRO for Hibernate caching.

      • Thanks Vlad,

        I’m a bit confused about your comment. This article is about using the hibernate 2nd-level cache and in your comment you seem to be implying that it’s not a good strategy to use it?

        I’m thinking that even if we have a hugely powerful database that has heaps of headroom you’d still want to employ the 2nd-level cache to minimize roundtrips to the database over the network. This would especially be true if you’ve defaulted to use FetchType.LAZY as we have. It’s those thousands of roundtrips to the database that kill performance aren’t they?

        It’s for the same reason that I think a remote 2nd-level cache (like redis as you suggest in your article) would kill performance too. Unless some kind of local cache was employed too before going to the remote service. But perhaps I’ve misunderstood what you meant in the last section of your article.

      • I’m a bit confused about your comment. This article is about using the hibernate 2nd-level cache and in your comment you seem to be implying that it’s not a good strategy to use it?

        The article explains exactly when the second-level cache is appropriate. It’s for a very specific use case. In 18 years of software development, I actually never needed it on any of my projects. That’s because I optimize my data access layer heavily and use Redis or ElasticSearch for anything else. But, some projects could benefit from it, like the ones that have a high ratio of read-write/read-only transactions.

        I’m thinking that even if we have a hugely powerful database that has heaps of headroom you’d still want to employ the 2nd-level cache to minimize roundtrips to the database over the network. This would especially be true if you’ve defaulted to use FetchType.LAZY as we have. It’s those thousands of roundtrips to the database that kill performance aren’t they?

        Not necessarily. Most likely, you’ll use Redis for that use case.

        It’s for the same reason that I think a remote 2nd-level cache (like redis as you suggest in your article) would kill performance too. Unless some kind of local cache was employed too before going to the remote service. But perhaps I’ve misunderstood what you meant in the last section of your article.

        A tool like Redis doesn’t hurt performance, even if it’s a distributed cache. You don’t need a local in-memory cache for performance.

        Check out the Caching part of my High-Performance Java Persistence video course for more details about this subject.

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.