Pulkit Pushkarna
What is caching ?
It stores the recently used data. This helps to reduces the number of database hits as much as possible.
Why should we use the cache ?
What data should be cached ?
Types of Caching
Spring Boot Caching
Spring boot provides a Cache Abstraction API that allow us to use different cache providers to cache objects.
The below is the control flow of Spring Boot Caching.
When the caching is enabled then the application first looks for required object in the cache instead of fetching from database. If it does not find that object in cache then only it access from database.
Spring Boot Cache Providers
The following are the cache provider supported by Spring Boot framework :
Spring Boot Cache Annotations
@EnableCaching
It is a class level annotation. It is used to enable caching in spring boot application. By default it setup a CacheManager and creates in-memory cache using one concurrent HashMap.
It is also applied over a Spring configuration class as below :
@SpringBootApplication
@EnableCaching
public class SpringBootCachingApplication {
public static void main(String[] args) {
SpringApplication.run(SpringBootCachingApplication.class, args);
}
}@Configuration
@EnableCaching
public class CacheConfig {
// some code
}@Cacheable
We can provide cache name to this annotation as follow :
@Cacheable(“employees”)
public Employee findById(int id) {
// some code
}This annotation has the following attributes :
@Cacheable(cachNames=”employees”)
public Employee findById(int id) {
// some code
}@Cacheable(value=”employees”, key="#id")
public Employee findById(int id) {
// some code
}@CachePut
@CachePut(value=”employee”)
public Employee updateEmployee(Employee employee) {
// some code
}@CacheEvict
@CacheEvict(value=”employee”, key="#id")
public void deleteEmployee(int id) {
// some code
}@CacheEvict(value=”employee”, allEntries=true)
public void deleteEmployee(int id) {
// some code
}Spring Boot in-memory cache
Let us create a student microservice
values for application properties
spring.jpa.hibernate.ddl-auto=update
spring.datasource.url=jdbc:mysql://localhost:3306/mydb
spring.datasource.username=root
spring.datasource.password=password
hibernate.dialect=mysql
spring.datasource.driver-class-name=com.mysql.jdbc.Driverpackage com.springcache.springcachedemo.entity;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.Id;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Entity
public class Student {
@Id
@GeneratedValue
private Long studentId;
private String name;
private int age;
}
Create a student Entity class
package com.springcache.springcachedemo.repository;
import com.springcache.springcachedemo.entity.Student;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
@Repository
public interface StudentRepository extends JpaRepository<Student, Long> {
}
Create a Student repository
Create a Student Controller
package com.springcache.springcachedemo.controller;
import com.springcache.springcachedemo.entity.Student;
import com.springcache.springcachedemo.repository.StudentRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.CachePut;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.web.bind.annotation.*;
import java.util.Optional;
import java.util.logging.Logger;
@RestController
public class StudentController {
@Autowired
StudentRepository studentRepository;
Logger logger = Logger.getLogger(StudentController.class.getName());
@GetMapping("/students")
public Iterable<Student> getStudents(){
return studentRepository.findAll();
}
@GetMapping("/students/{id}")
public Student getStudent(@PathVariable Long id){
logger.info("Fetching data for student id ::"+id);
Optional<Student> studentOptional = studentRepository.findById(id);
if(studentOptional.isPresent()){
return studentOptional.get();
}else{
throw new RuntimeException("Student Not found");
}
}
@PostMapping("/students")
public Student postStudent(@RequestBody Student student){
return studentRepository.save(student);
}
@PutMapping("/students")
public Student putStudent(@RequestBody Student student){
logger.info("Updating student data for student id ::"+student.getId());
Optional<Student> studentOptional= studentRepository.findById(student.getId());
if(studentOptional.isPresent()){
return studentRepository.save(student);
}else{
throw new RuntimeException("Student Not found");
}
}
@DeleteMapping("/students/{id}")
public Student deleteStudent(@PathVariable Long id){
logger.info("Deleting data for student id ::"+id);
Optional<Student> optionalStudent = studentRepository.findById(id);
if(optionalStudent.isPresent()){
studentRepository.delete(optionalStudent.get());
return optionalStudent.get();
}else{
throw new RuntimeException("Student Not found");
}
}
}
Introducing caching
package com.springcache.springcachedemo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cache.annotation.EnableCaching;
@SpringBootApplication
@EnableCaching
public class SpringCacheDemoApplication {
public static void main(String[] args) {
SpringApplication.run(SpringCacheDemoApplication.class, args);
}
}
Introducing caching annotations in controller
package com.springcache.springcachedemo.controller;
import com.springcache.springcachedemo.entity.Student;
import com.springcache.springcachedemo.repository.StudentRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.CachePut;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.web.bind.annotation.*;
import java.util.Optional;
import java.util.logging.Logger;
@RestController
public class StudentController {
@Autowired
StudentRepository studentRepository;
Logger logger = Logger.getLogger(StudentController.class.getName());
@GetMapping("/students")
public Iterable<Student> getStudents(){
return studentRepository.findAll();
}
@GetMapping("/students/{id}")
@Cacheable(cacheNames = "student",key = "#id")
public Student getStudent(@PathVariable Long id){
logger.info("Fetching data for student id ::"+id);
Optional<Student> studentOptional = studentRepository.findById(id);
if(studentOptional.isPresent()){
return studentOptional.get();
}else{
throw new RuntimeException("Student Not found");
}
}
@PostMapping("/students")
public Student postStudent(@RequestBody Student student){
return studentRepository.save(student);
}
@PutMapping("/students")
@CachePut(cacheNames = "student",key = "#student.id")
public Student putStudent(@RequestBody Student student){
logger.info("Updating student data for student id ::"+student.getId());
Optional<Student> studentOptional= studentRepository.findById(student.getId());
if(studentOptional.isPresent()){
return studentRepository.save(student);
}else{
throw new RuntimeException("Student Not found");
}
}
@DeleteMapping("/students/{id}")
@CacheEvict(cacheNames = "student",key = "#id")
public Student deleteStudent(@PathVariable Long id){
logger.info("Deleting data for student id ::"+id);
Optional<Student> optionalStudent = studentRepository.findById(id);
if(optionalStudent.isPresent()){
studentRepository.delete(optionalStudent.get());
return optionalStudent.get();
}else{
throw new RuntimeException("Student Not found");
}
}
}
Distributed Cache in Spring Boot
What is Hazelcast ?
It supports the auto-discovery of nodes and intelligent synchronization.
Let us create executable jar of our microservice
Run 2 seperate instances
java -jar spring-cache-demo-0.0.1-SNAPSHOT.jar --server.port=8082
java -jar spring-cache-demo-0.0.1-SNAPSHOT.jar --server.port=8081Add hazelcast dependency in build.gradle
implementation group: 'com.hazelcast', name: 'hazelcast-all', version: '4.2.7'Add configuration for Multicast Discovery – the way by which each embedded cache server can communicate with all other embedded cache servers in the network.
Add a file “hazelcast.yaml” in src/main/resources folder with following content.
hazelcast:
network:
join:
multicast:
multicast-group: 224.0.0.1
enabled: trueNow rebuild the boot jar and run 3 instances again to check the distributed cache
Implement Serializable interface in Student class
package com.springcache.springcachedemo.entity;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.Id;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import java.io.Serializable;
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Entity
public class Student implements Serializable {
@Id
@GeneratedValue
private Long id;
private String name;
private int age;
}
Client-Server Topology
./hz start Hazlecast Management Center
java -Dhazelcast.mc.http.port=8083 -Dhazelcast.mc.contextPath='hazelcast-mc'
-jar
hazelcast-management-center-5.3.3.jarClick on the Add button
Enter the details of the cluster and press connect button
We can see 2 connected members from the cluster
Place hazelcast dependency in build.gradle
implementation group: 'com.hazelcast', name: 'hazelcast', version: '5.3.2'Create Hazelcast Client Instance
@Bean
public ClientConfig clientConfig() {
ClientConfig clientConfig = new ClientConfig();
clientConfig.setClusterName("dev");
clientConfig.getNetworkConfig().addAddress("127.0.0.1:5701", "127.0.0.1:5702");
return clientConfig;
}
@Bean
public HazelcastInstance hazelcastInstance(ClientConfig clientConfig) {
return HazelcastClient.newHazelcastClient(clientConfig);
}
Create a controller which fetch Student from Hazelcast Client
package com.springcache.springcachedemo.controller;
import com.hazelcast.core.HazelcastInstance;
import com.hazelcast.map.IMap;
import com.springcache.springcachedemo.entity.Student;
import com.springcache.springcachedemo.repository.StudentRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
import java.util.Objects;
import java.util.Optional;
import java.util.logging.Logger;
@RestController
public class StudentClientCacheController {
@Autowired
HazelcastInstance hazelcastInstance;
@Autowired
StudentRepository studentRepository;
Logger logger = Logger.getLogger(StudentClientCacheController.class.getName());
@GetMapping("/studentClient/{id}")
Student getStudent(@PathVariable Long id){
IMap<Long, Student> studentMap = hazelcastInstance.getMap("student");
Student student = studentMap.get(id);
if(Objects.nonNull(student)){
return student;
}else{
logger.info("Fetching data for student id ::"+id);
Optional<Student> studentOptional = studentRepository.findById(id);
if(studentOptional.isPresent()){
Student student1 = studentOptional.get();
studentMap.put(id,student1);
return student1;
}else{
throw new RuntimeException("No Student Found");
}
}
}
}
Create a boot jar and create run 2 instances of your microservice using the command below
java -jar spring-cache-demo-0.0.1-SNAPSHOT.jar --server.port=8082
java -jar spring-cache-demo-0.0.1-SNAPSHOT.jar --server.port=8084Now check for the distributed caching you will observe that
the caching will persist even if the both the intances go down