Agenda of this blog:
In this blog we will do the demo of L1 and L2 cache in hibernate/JPA. For that we will be using Spring Boot Data JPA and H2 database. For L2 cache demo we will use all different ways – Spring Cache, EHCache, and Redis.
In a typical application you would be having a UI-Web layer talking to a Service layer which talks to a Repository (Data) layer and that in turn talks to the actual database.
Talking to a database is a costly affair, there is a network round trip involved since the database is generally present in some other system. To avoid this round trip we can cache some of the data which are not going to change frequently.
Caching in hibernate/JPA is of 2 types:
- L1 cache (1st level cache)
- L2 cache (2nd level cache)
First Level cache is saved at the session (PersistentContext) level, whereas the Second Level Cache is saved at the application level.
During a transaction the data is first searched in the First Level cache which is present in the session (PersistenceContext), if it does not get the respective data then only it will seek from Second Level cache which then fetch data from the database. First Level Cache is within the boundary of a Transaction.
Suppose at a particular instance of time 4 transactions are running, they would be having their own PersistenceContext object and so they would interact first with own First Level Cache (FLC). If the necessary data is not available with the FLC then only it will ask the Second Level Cache.
First Level (L1) Cache:
Lets see the demo. And for that we will use the same REST CRUD application that we used in our demo earlier.
Suppose we make a database call from any method like below:
@Autowired MyRepository myRepository; public Person getPersonById(String id) { Person person = myRepository.findById(id).get(); return person; }
In the console log you will see an “select” hibernate query being fired.
Now, if we do 2 similar call like below, do you think the select query will be fired 2 times ?
@Autowired MyRepository myRepository; public Person getPersonById(String id) { Person person = myRepository.findById(id).get(); Person person2 = myRepository.findById(id).get(); return person; }
No, it will not be called twice, it will be fetched from the cache, this is the First Level Cache.
First Level Cache is active by default.
Second Level (L2) Cache:
Its not enabled by default, you have to enable it.
This is applicable across the application and not limited to a session(PersistenceContext).
As discussed earlier we will use all 3 ways to demo the 2nd level cache – using Spring Cache, EHCache and Redis.
For Spring Cache add the below dependency from Spring Initializr:
So, below lines of code will be added in pom.xml :
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-cache</artifactId> </dependency>
For EHCache you can add below dependency manually as its not available in Spring Initializr:
<dependency> <groupId>javax.cache</groupId> <artifactId>cache-api</artifactId> </dependency> <dependency> <groupId>org.ehcache</groupId> <artifactId>ehcache</artifactId> </dependency>
In many tutorials they had mentioned to add all the above 3 dependencies (Spring Cache + EHCache) to demo a 2nd level cache in Spring applications, which is redundant, since these are 2 different set of dependencies for the same purpose.
For Redis as Cache you can go to this section.
Create a config class. If you are using Spring Cache dependency you can use below configuration file.
You can add a @EnableCaching annotation either in this class or in the main class of the project (both place will work fine).
package com.heapsteep.config; import org.springframework.cache.CacheManager; import org.springframework.cache.concurrent.ConcurrentMapCacheManager; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration //@EnableCaching public class SpringCacheConfig { @Bean CacheManager myCache(){ return new ConcurrentMapCacheManager("cacheStore"); } }
If you are using EHcache dependencies, then you can add below configuration file where you can mention these attributes – cache duration, cache memory, cache name, etc.
package com.heapsteep.config; import com.heapsteep.model.Person; import org.ehcache.config.CacheConfiguration; import org.ehcache.config.builders.CacheConfigurationBuilder; import org.ehcache.config.builders.ExpiryPolicyBuilder; import org.ehcache.config.builders.ResourcePoolsBuilder; import org.ehcache.config.units.MemoryUnit; import org.ehcache.jsr107.Eh107Configuration; import org.springframework.cache.annotation.EnableCaching; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import javax.cache.CacheManager; import javax.cache.Caching; import javax.cache.spi.CachingProvider; import java.time.Duration; @Configuration //@EnableCaching public class EHCacheConfig { @Bean public CacheManager myCacheManager() { CacheConfiguration<String, Person> cachecConfig = CacheConfigurationBuilder .newCacheConfigurationBuilder(String.class, Person.class, ResourcePoolsBuilder.newResourcePoolsBuilder() .offheap(10, MemoryUnit.MB) .build()) .withExpiry(ExpiryPolicyBuilder.timeToIdleExpiration(Duration.ofSeconds(10))) .build(); CachingProvider cachingProvider = Caching.getCachingProvider(); CacheManager cacheManager = cachingProvider.getCacheManager(); javax.cache.configuration.Configuration<String, Person> configuration = Eh107Configuration.fromEhcacheCacheConfiguration(cachecConfig); cacheManager.createCache("cacheStore", configuration); return cacheManager; } }
On top of the entity class add @Cacheable and implement Serializable interface:
@Data @NoArgsConstructor @AllArgsConstructor @Entity @Cacheable public class Person implements java.io.Serializable{ @Id @GeneratedValue private int id; private String name; private Integer age; }
In the service methods you can add @Cacheable with the cache name and the key:
@Cacheable(cacheNames = "cacheStore", key = "#id") public Person getPersonById(String id) { Person person = myRepository.findById(id).get(); logger.info("person: " + person); return person; }
Now run the application.
If you do a REST call to the above service method you will see in the console log a Hibernate query is being fired once. If you do the same REST call again the same service method itself will not be called again at all, the data will be fetched from the cache mentioned on top of that method, and hence there won’t be any 2nd Hibernate call. This is 2nd Level Caching.
Source code of the above example: https://github.com/heapsteep/hibernate-cache-demo
@CacheEvict and @CachePut :
In addition to @Cacheable, Spring Boot provides two more powerful caching annotations: @CacheEvict and @CachePut.
The @CacheEvict annotation allows you to evict or remove specific entries from the cache. It’s particularly useful when you need to ensure that stale or outdated data is not served from the cache. You can use it with DELETE http methods call.
example:
@CacheEvict(value = "cacheStore", key = "#id") public void deletePerson(String id) { myRepository.deleteById(id); }
@CachePut forcefully update the cache, regardless of whether the entry already exists in the cache.
example:
@CachePut(value = "cacheStore", key = "#id") public Person updatePerson(Person person, String id) { Person person2 = myRepository.findById(id).get(); person2.setName(person.getName()); person2.setAge(person.getAge()); return myRepository.save(person2); }
Source code of the above demo: https://github.com/heapsteep/hibernate-cache-demo.git
Redis as Cache:
Redis can be used with a Spring Boot application for below usage:
- As a cache.
- As a database.
- As a message broker.
In this tutorial we will demo only the Redis Cache.
All the steps mentioned here are similar to the above example, the only change is in the Config file.
Lets start creating a spring boot application having below dependencies:
- Spring Web
- Spring Data JPA
- H2
- Spring Redis
- Lombok
Spring Redis dependency is the main dependency:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency>
Config file:
package com.heapsteep.config; import org.springframework.boot.autoconfigure.cache.RedisCacheManagerBuilderCustomizer; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.data.redis.cache.RedisCacheConfiguration; import java.time.Duration; @Configuration public class MyConfig { @Bean public RedisCacheManagerBuilderCustomizer redisCacheManagerBuilderCustomizer() { return (builder) -> builder .withCacheConfiguration("cacheStore", RedisCacheConfiguration.defaultCacheConfig().entryTtl(Duration.ofMinutes(5))); } }
In many tutorials they had mentioned to add below properties in application.properties file, but its not required.
Seems Spring auto configuration picks the running Redis server instance automatically.
spring.cache.type= redis spring.cache.host= localhost spring.cache.port= 6379 spring.cache.redis.time-to-live= 60000
Make sure to start the Docker Desktop. Then install the Redis server by running below command:
docker run -d --name redis-stack-server -p 6379:6379 redis/redis-stack-server:latest
Below files are as usual:
Model class:
package com.heapsteep.model; import jakarta.persistence.Cacheable; import jakarta.persistence.Entity; import jakarta.persistence.GeneratedValue; import jakarta.persistence.Id; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; @Data @NoArgsConstructor @AllArgsConstructor @Entity @Cacheable public class Person implements java.io.Serializable{ @Id @GeneratedValue private int id; private String name; private Integer age; }
Repository:
@Repository public interface MyRepository extends CrudRepository<Person,String>{ }
Service:
@Cacheable(cacheNames = "cacheStore", key = "#id") public Person getPersonById(String id) { Person person = myRepository.findById(id).get(); //Person person2 = myRepository.findById(id).get(); logger.info("person: " + person); //logger.info("person2: " + person2); return person; }
Source code : https://github.com/heapsteep/redis-jpa-demo.git