Hibernate, Redis, and L2 Cache Performance
Redis is a great platform for caching. Unfortunately, free options for integrating it into Hibernate as L2 cache are lacking. Let's see if we can fix that.
Join the DZone community and get the full member experience.
Join For Free
Hibernate is the de-facto standard for ORM (Object-Relational Mapping) in Java. But, what happens when Hibernate doesn't offer the performance you need? That's where cache comes into play.
By default, Hibernate offers L1 cache. It also allows L2 cache to be configured. L1 cache is session cache, which is to say that it is cache that's maintained independently by each session. Before querying the database for an object, the session first checks the L1 cache to see if the object is already available (i.e. it has been recently accessed). Hibernate's optional L2 cache is at the SessionFactory level; it spans sessions - and in the case of distributed cache, it can also span JVMs.
L2 Cache Options
Out of the box, Hibernate has L2 cache support for JCache and EHCache. JCache, being the Java caching standard has several implementations, including Hazelcast, Coherence and Infinispan to name a few. Both EHCache and JCache implementations can support local and distributed caching, with Terracotta being a popular choice for distributed caching with EHCache.
Aside from Hibernate's out-of-the-box L2 cache implementations for JCache and EHCache, there are also other L2 caching integrations. One such integration is Redisson, which offers Hibernate L2 caching support for Redis.
When L2 Caching Is Needed
Read heavy applications can especially benefit from additional caching. This is because cache provides the most benefit for successive reads. If there is a lot of locality when fetching entities from the database, that is a great case for L2 caching.
L2 caching would not be as beneficial if the application was geared more towards serving presentation (e.g. web page fragments) with a high degree of temporal locality and very little interaction with underlying data, save to generate that presentation. In that case, caching at the controller or the service level would be more impactful than database caching, which could be taken out of the critical path.
However, with more and more emphasis on single-page web applications that fetch data from services and render their presentation on the client (browser), there is an increased emphasis on data speed, and therefore appropriate use of L2 cache. It should, however, be noted that although L2 cache may help performance (especially for data read-heavy applications with high temporal locality), it is not a substitute for good design. It may, however, help a well-engineered system perform even better.
Redis as a Potential Solution
Redis is an in-memory data structure store that can be easily distributed. Redis claims 8x the throughput and 50% the latency compared to popular NoSQL solutions. It's safe to say that as a distributed data source, Redis is darn fast. Of course, actual speeds will be affected by network bandwidth and proximity to the application(s) interacting with Redis.
Given Redis's purported performance and how simple it is to set up and use, one might think that there would be a selection of Hibernate L2 Redis integrations to choose from. However, it appears as if there is only one: Redisson. That's not to say there aren't other Java-based Redis cache clients. But only Redisson provides a library for Hibernate L2 cache integration.
Digging a little deeper into Redisson, however, it seems as if local cache support, its most compelling feature, only has cursory support with its open source (non-paid) offering. Also, it appears that the performance of Redisson without local caching lags a bit behind other popular Java-based Redis clients like Jedis. Even with local caching, Redisson's read performance edge only exists after the local cache hit rate climbs near 50%.
Local Cache or No Local Cache?
I'm going to be the Devil's advocate here and make the claim that for database caching, local cache is not all it's cracked up to be. I base that claim on what you should be trying to achieve with L2 caching and how local cache is managed. The purpose of L2 caching is to improve the performance of data reads by shifting the burden from the database to a faster (and simpler) shared data retrieval mechanism.
The purpose of L2 caching is to improve the performance of data reads by shifting the burden from the database to a faster (and simpler) shared data retrieval mechanism.
Given that, an L2 caching solution should be (1) faster and less taxed by reads than the database it is supporting and (2) simple to use, scale and deploy. Redis definitely fits those requirements. And the distributed nature of Redis means that as a cache store, its data can be easily shared.
Local cache adds additional complexity with the promise of more performance, but only after a sufficient amount of data is cached locally. If that data is cached locally, then it's at least in part managed by the JVM, which likely means additional memory overhead for the application. That begs the question, "Is local cache worth the additional complexities and overhead it brings if I already have a reasonably fast distributed caching solution?" I think in some cases that answer is yes - for example, if reads were frequent, updates were infrequent and the cache could be sufficiently pre-loaded. In most other cases, however, I think that answer is no.
Using Redis as L2 Cache
Redis is fast, distributed and easy to setup. Redisson provides a Redis L2 cache integration. Let's take a look at how that might work with a Spring Boot application.
First, start the Redis server. If you have Redis installed locally, just type redis-server
and that should be enough to start Redis in standalone mode. Next, add the Reddison dependency and configure Redisson as the L2 caching provider in the Spring Boot application config (e.g. application.yml).
In the Maven pom.xml:
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson-hibernate-53</artifactId>
<version>3.15.6</version>
</dependency>
In the application.yml:
spring.jpa.hibernate.cache.use_second_level_cache: true
spring.jpa.hibernate.cache.region.factory_class: org.redisson.hibernate.RedissonRegionFactory
spring.jpa.hibernate.cache.redisson.config: /redisson.yaml
The redisson.yaml file referenced above should be in the classpath. It is detailed here.
Finally, any entities that are to be cached in Hibernate's L2 cache must be declared as Cacheable as follows.
@Entity
@Cacheable
@org.hibernate.annotations.Cache(usage = CacheConcurrencyStrategy.READ_WRITE)
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private long id;
@Column(unique = true)
@NotNull
private String username;
@Column(name = "first_name")
private String firstName;
@Column(name = "last_name")
private String lastName;
}
Not too bad. However, there are a few issues to note. First, Redisson's configuration is external to Spring's - and that is not optional. Second, we are not getting local caching, which is Redisson's big differentiator. We could specify org.redisson.hibernate.RedissonLocalCachedRegionFactory, which would give us local caching. But in that implementation, local caching is only available via Maps, 100% managed by the JVM. If you want to step up to a more robust local caching solution, then you'll have to get the paid version of Redisson.
A New Alternative
It's true that Redisson provides the only known integration for Redis as Hibernate L2 cache. But there's nothing preventing us from writing our own Redis L2 cache integration using, say, Jedis. Using Jedis, we could have a faster solution without local cache and we could piggyback off of Hibernate's configuration - everything could be in Spring Boot's application.yml config. The only question is how hard is it to implement?
Turns out that it's not that hard.
Here's the basic rundown of what needs to be done to write a Hibernate L2 cache integration in Hibernate 5.
- Implement a class that extends
org.hibernate.cache.spi.support.RegionFactoryTemplate
- Add that class to the Hibernate configuration
spring.jpa.hibernate.cache.region.factory_class: com.mycompany.JedisRegionFactory
That's it!
And since we get to define how the properties are set, we can also make the configuration in application.yml look something like this.
spring:
jpa:
properties:
hibernate:
cache:
use_second_level_cache: true
region:
factory_class: com.mycompany.JedisRegionFactory
redis:
standalone:
host: localhost
port: 6379
No need for additional/external configuration!
See Redis L2 Cache Integration Using Jedis for additional details and a complete working example.
Some Jedis Gotchas
The biggest Jedis gotcha is that it only supports String data types. All the keys must be Strings and all the objects stored must also be Strings. Not surprisingly, Hibernate supplies keys as objects and of course, the data stored (and retrieved) is in the form of objects. So, storing and retrieving objects from Jedis requires some conversion. This can be achieved in a number of ways. But before keys can be used to query using Jedis, they must be converted to Strings, and before objects are stored by Jedis, they must also be converted to Strings. Conversely, when objects are retrieved from Jedis, they must be converted from their String form into their original object form.
Probably the easiest way to achieve this conversion is through the serialization and deserialization of objects. Objects can be serialized into bytes and those bytes can then be base64 encoded to become visible Strings. The reverse can be done to convert stored Strings into their original object form.
Example:
static String convertObjectToString(Object obj) throws IOException {
try (ByteArrayOutputStream bos = new ByteArrayOutputStream(); ObjectOutputStream out = new ObjectOutputStream(bos)){
out.writeObject(obj);
out.flush();
return Base64.getEncoder().encodeToString(bos.toByteArray());
}
}
static Object convertStringToObject(String str) throws IOException, ClassNotFoundException {
try (ByteArrayInputStream bis = new ByteArrayInputStream(Base64.getDecoder().decode(str.getBytes())); ObjectInput in = new ObjectInputStream(bis)){
return in.readObject();
}
}
As stated previously, a complete working example and additional details can be found in Redis L2 Cache Integration Using Jedis.
Performance Considerations
By all accounts, Jedis is a pretty snappy Redis client. However, it relies solely on Redis. A major performance consideration must therefore be the latency between Redis and the Jedis client.
Prior to fetching from the database, Hibernate will first check its session cache (L1), then its L2 cache, then it will query the database. In an extreme situation, excessive latency between Redis and the Jedis client could make database queries even slower than they were prior to using L2 cache. A good design must target very low latency between Redis and the Jedis client, keeping them in close proximity to each other.
Conclusions
Redis's speed and simplicity make it a good candidate to support Hibernate's L2 caching functionality. Unfortunately, available integration options are limited. However, Hibernate does make it relatively simple to implement Redis L2 caching using the popular Java Redis client, Jedis. By leveraging Jedis and Hibernate's L2 cache support, simple and performant (and free) Redis L2 caching is possible.
Opinions expressed by DZone contributors are their own.
Comments