StackStalk
  • Home
  • Java
    • Java Collection
    • Spring Boot Collection
  • Python
    • Python Collection
  • C++
    • C++ Collection
    • Progamming Problems
    • Algorithms
    • Data Structures
    • Design Patterns
  • General
    • Tips and Tricks

Friday, December 3, 2021

Implement caching in a Spring Boot microservice using Redis

 December 03, 2021     Java, Microservices, Spring Boot     No comments   

In this article we will explore how to use Redis as a data cache for a Spring Boot microservice using PostgreSQL as the database. Idea is to optimize the data retrieval from cache instead of going to the database for each request. 

We will use Redis (Remote Dictionary Server) as a in-memory cache for the database and let us start to understand the basics of Redis. 

Redis Introduction

  • Redis is an in-memory data structure store, used as a database, cache and message broker. 
  • Redis is not a plain key-value store. 
  • Redis can be used to store complex data structure unlike key value stores. 
  • Redis delivers sub-millisecond response times and leveraged in real time applications in domains like financial services, IoT etc. 
Best want to understand is to install Redis and experiment with it. There are multiple approaches to quickly get started.

Option 1: Run Redis as Docker container

Easiest way to start with Redis is to run as a Docker container. This starts the Redis server listening on port 6379. 

# docker run -p 6379:6379 redis
1:C 02 Dec 2021 07:25:26.849 # oO0OoO0OoO0Oo Redis is starting oO0OoO0OoO0Oo
1:C 02 Dec 2021 07:25:26.850 # Redis version=6.2.6, bits=64, commit=00000000, modified=0, pid=1, just started
1:C 02 Dec 2021 07:25:26.850 # Warning: no config file specified, using the default config. In order to specify a config file use redis-server /path/to/redis.conf
1:M 02 Dec 2021 07:25:26.853 * monotonic clock: POSIX clock_gettime
1:M 02 Dec 2021 07:25:26.864 * Running mode=standalone, port=6379.
1:M 02 Dec 2021 07:25:26.864 # Server initialized
1:M 02 Dec 2021 07:25:26.867 * Ready to accept connections

Option 2: Install on MAC using brew

Install Redis via brew.
brew update
brew install redis
To start and stop Redis as below.
brew services start redis
brew services stop redis

Option 3: Install Redis from source

Install Redis from source.
wget http://download.redis.io/redis-stable.tar.gz
tar xvzf redis-stable.tar.gz
cd redis-stable
make
make install
Now start Redis as below.
$ redis-server
 
[1200] 02 Dec 21:29:28 # Warning: no config file specified, using the default config. In order to specify a config file use 'redis-server /path/to/redis.conf'
[1200] 02 Dec 21:29:28 * Server started, Redis version 2.2.12
[1200] 02 Dec 21:29:28 * The server is now ready to accept connections on port 6379
... more logs ...

Experiementing with Redis

We can leverage redis-cli to test if the installation is working.
$ redis-cli ping
PONG
Let us store a string value and read it from Redis. We can also set expiry on a specific key.
$ redis-cli
127.0.0.1:6379> 
127.0.0.1:6379> set mykey1 "somevalue"
OK
127.0.0.1:6379> get mykey1
"somevalue"
127.0.0.1:6379> expire mykey1 5
(integer) 1
127.0.0.1:6379> get mykey1
"somevalue"
127.0.0.1:6379> get mykey1
(nil)
127.0.0.1:6379>
Example to work with lists in Redis.
127.0.0.1:6379> lpush mylist 1 2 3 4
(integer) 4
127.0.0.1:6379> exists mylist
(integer) 1
127.0.0.1:6379> lpop mylist
"4"
127.0.0.1:6379> lpop mylist
"3"
127.0.0.1:6379> lpop mylist
"2"
127.0.0.1:6379> lpop mylist
"1"
127.0.0.1:6379> lpop mylist
(nil)
127.0.0.1:6379> exists mylist
(integer) 0
127.0.0.1:6379>
Example to work with hashes in Redis.
127.0.0.1:6379> hmset employee:100 name John join_date 13/10/2013 dept HR
OK
127.0.0.1:6379> 
127.0.0.1:6379> hmget employee:100 name
1) "John"
127.0.0.1:6379> hgetall employee:100
1) "name"
2) "John"
3) "join_date"
4) "13/10/2013"
5) "dept"
6) "HR"

PostgreSQL Installation

Another pre-requisite for this example is to have a PostgreSQL installation to be used as a database. Easiest approach is to run PostgreSQL as a Docker Container.
docker run -e POSTGRES_PASSWORD=postgres postgres

... logs ...

PostgreSQL init process complete; ready for start up.

2021-12-02 12:52:41.123 UTC [1] LOG:  starting PostgreSQL 14.1 (Debian 14.1-1.pgdg110+1) on x86_64-pc-linux-gnu, compiled by gcc (Debian 10.2.1-6) 10.2.1 20210110, 64-bit
2021-12-02 12:52:41.123 UTC [1] LOG:  listening on IPv4 address "0.0.0.0", port 5432
2021-12-02 12:52:41.123 UTC [1] LOG:  listening on IPv6 address "::", port 5432
2021-12-02 12:52:41.132 UTC [1] LOG:  listening on Unix socket "/var/run/postgresql/.s.PGSQL.5432"
2021-12-02 12:52:41.139 UTC [63] LOG:  database system was shut down at 2021-12-02 12:52:41 UTC
2021-12-02 12:52:41.145 UTC [1] LOG:  database system is ready to accept connections
Now let us create a database (userdb) and a user to access.
psql -h localhost -p 5432 -U postgres

postgres=# create database userdb;
CREATE DATABASE
postgres=# create user demo with encrypted password 'demo';
CREATE ROLE
postgres=# grant all privileges on database userdb to demo;
GRANT
postgres=#

Spring Boot microservice implementation

Now that we have both Redis and PostgreSQL running as containers let us dive into the Sprint Boot microservice implementation. In this example, we will write a microservice which exposes APIs to store and read user data. Data would be cached in-memory and served from Redis, and backed by the PostgreSQL database for persistence.
    

Spring Boot dependencies

Create a Spring starter project and include the JPA (Java Persistence API), Redis data store, Web application starter, PostgreSQL JDBC driver as dependencies to the pom.xml.
<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-jpa</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-redis</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.postgresql</groupId>
        <artifactId>postgresql</artifactId>
    </dependency>
</dependencies>
    

Configure application.properties

Update the application.properties to point to the PostgreSQL database and Redis cache. We are setting the Time to live (TTL) parameter to 10 seconds for the cache entries post which it would expire.
spring.jpa.database=POSTGRESQL
spring.datasource.url=jdbc:postgresql://localhost:5432/userdb
spring.datasource.username=demo
spring.datasource.password=demo
spring.jpa.generate-ddl=true
spring.jpa.database-platform=org.hibernate.dialect.PostgreSQLDialect

spring.data.redis.repositories.enabled=false
spring.cache.type=redis
spring.cache.redis.cache-null-values=true
spring.cache.redis.time-to-live=10000
    

Enable caching using annotation

Enable caching in the main application using annotation. The @EnableCaching annotation triggers a post-processor that inspects every Spring bean for the presence of caching annotations on public methods. If such an annotation is found, a proxy is automatically created to intercept the method call and handle the caching behavior accordingly.
package com.stackstalk.democache;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cache.annotation.EnableCaching;

@SpringBootApplication
@EnableCaching
public class DemoCacheApplication {

	public static void main(String[] args) {
		SpringApplication.run(DemoCacheApplication.class, args);
	}

}
    

Define the Entity class

Entity class should implement serializable and include serialversionUID.
package com.stackstalk.democache;

import java.io.Serializable;

import javax.persistence.Entity;
import javax.persistence.Id;

@Entity
public class UserDetails implements Serializable {
	private static final long serialVersionUID = -4439114469417994311L;
	
	@Id
	private int userId;
	private String userName;
	
	public UserDetails() {
		super();
	}	
	public UserDetails(int userId, String userName) {
		super();
		this.userId = userId;
		this.userName = userName;
	}
	public int getUserId() {
		return userId;
	}
	public void setUserId(int userId) {
		this.userId = userId;
	}
	public String getUserName() {
		return userName;
	}
	public void setUserName(String userName) {
		this.userName = userName;
	}	
}
    

Define the controller

In the controller class define the APIs of interest. Here we include APIs to get all users, get a specific user based on user identifier and add a new user.
package com.stackstalk.democache;

import java.util.List;

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.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class UserController {

	@Autowired
	private UserService userService;
	
	@GetMapping(path="/users")
	public List<UserDetails> getUsers() {
		return userService.getAllUsers();
	}
	
	@GetMapping(path="/users/{userId}")
	public UserDetails getUser(@PathVariable int userId) {
		return userService.getUser(userId);
	}
	
	@PostMapping(path="/users")
	public UserDetails addUser(@RequestBody UserDetails user) {
		return userService.addUser(user, user.getUserId());
	}
}
    

Define the repository

Now let us create the UserRepository as follows.
package com.stackstalk.democache;

import org.springframework.data.repository.CrudRepository;

public interface UserRepository extends CrudRepository<UserDetails, Integer> {
}
    

Define the service layer

Now let us implement the service logic. Spring provides caching abstraction and frees the developer from writing the actual caching code. Java annotations @Cacheable, @CachePut and @CacheEvict allows methods to trigger cache population and cache eviction.
  • @Cacheable - Demarcates methods that are cacheable, methods where the result is stored in cache and on subsequent request the values in the cache is returned without having to execute the method.
  • @CachePut - For cases where the cache needs to be updated without interfering with the method execution, one can use the @CachePut annotation. That is, the method will always be executed and its result placed into the cache
  • @CacheEvict - Demarcates methods that perform cache eviction, that is methods that act as triggers for removing data from the cache.
Since caches are essentially key-value stores, each invocation of a cached method needs to be translated into a suitable key for cache access. @Cacheable and @CachePut annotation allows the user to specify how the key is generated through its key attribute. Key definitions follow SpEL (Spring Expression Language) syntax.
package com.stackstalk.democache;

import java.util.ArrayList;
import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.CachePut;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;

@Service
public class UserService {

	@Autowired
	private UserRepository userRepository;

	@Cacheable(value="UserDetails", key="#root.method.name")
	public List<UserDetails> getAllUsers() {
		System.out.println("Invoked getAllUsers ..");
		List<UserDetails> userList = new ArrayList<UserDetails>();
		userRepository.findAll().forEach(userList::add);
		return userList;
	}
	
	@Cacheable(value="UserDetails", key="#userId")
	public UserDetails getUser(int userId) {
		System.out.println("Invoked getUser ..");
		return userRepository.findById(userId).orElse(null);
	}	

	@CachePut(value="UserDetails", key="#userId")
	public UserDetails addUser(UserDetails user, int userId) {
		System.out.println("Invoked addUser ..");
		return userRepository.save(user);
	}
}

Putting it all together and testing

Now, let us test the application and examine the cache and application behavior.

Add User

Make a POST request to add a user.
$ curl -X POST -H "Content-Type: application/json" -d '{"userId": 100, "userName": "StackStalk"}' http://localhost:8080/users
{"userId":100,"userName":"StackStalk"}
Examine the contents of the Redis cache. We see a new key created for the add user request. Check the cache after 10 secs (TTL) and we should see the cache empty.
$ redis-cli
127.0.0.1:6379> KEYS *
1) "UserDetails::100"

Try after 10 secs (TTL specified)
127.0.0.1:6379> KEYS *
(empty array)
Since the addUser method has @CachePut annotation, everytime the API is invoked the method should be executed.
Invoked addUser ..

Get specific user

Make a GET request for a specific user.
$ curl -X GET http://localhost:8080/users/100
{"userId":100,"userName":"StackStalk"}
Examine the contents of the Redis cache. We see a new key created for the get user request. Check the cache after 10 secs (TTL) and we should see the cache empty.
$ redis-cli
127.0.0.1:6379> KEYS *
1) "UserDetails::100"

Try after 10 secs (TTL specified)
127.0.0.1:6379> KEYS *
(empty array)
Since the getUser method has @Cacheable annotation we will see the method invoked once. Subsequent calls within specified TTL will return the values from the cache instead of executing the method.
Invoked getUser ..

Get all users

Make a GET request for all users.
$ curl -X GET http://localhost:8080/users
[{"userId":1,"userName":"John"},{"userId":3,"userName":"Lisa"},{"userId":4,"userName":"Chris"},{"userId":5,"userName":"Greg"},{"userId":2,"userName":"Joe"},{"userId":100,"userName":"StackStalk"}]
Examine the contents of the Redis cache. We see a new key created for the get users request. Check the cache after 10 secs (TTL) and we should see the cache empty. Key is created with the method name based on the key specified #root.method.name.
$ redis-cli
127.0.0.1:6379> KEYS *
1) "UserDetails::getAllUsers"

Try after 10 secs (TTL specified)
127.0.0.1:6379> KEYS *
(empty array)
Since the getAllUsers method has @Cacheable annotation we will see the method invoked once. Subsequent calls within specified TTL will return the values from the cache instead of executing the method.
Invoked getAllUsers ..
Get access to the full source code of this example in GitHub.
In conclusion, Spring Boot microservices working with a database can be easily augmented with in-memory cache using Redis just by adding additonal caching annotations. This improves the overall application performance and can return sub-millisecond response times.
  • Share This:  
Newer Post Older Post Home

0 comments:

Post a Comment

Follow @StackStalk
Get new posts by email:
Powered by follow.it

Popular Posts

  • Avro Producer and Consumer with Python using Confluent Kafka
    In this article, we will understand Avro a popular data serialization format in streaming data applications and develop a simple Avro Produc...
  • Monitor Spring Boot App with Micrometer and Prometheus
    Modern distributed applications typically have multiple microservices working together. Ability to monitor and manage aspects like health, m...
  • Server-Sent Events with Spring WebFlux
    In this article we will review the concepts of server-sent events and work on an example using WebFlux. Before getting into this article it ...
  • Implement caching in a Spring Boot microservice using Redis
    In this article we will explore how to use Redis as a data cache for a Spring Boot microservice using PostgreSQL as the database. Idea is to...
  • Python FastAPI microservice with Okta and OPA
    Authentication (AuthN) and Authorization (AuthZ) is a common challenge when developing microservices. In this article, we will explore how t...
  • Spring Boot with Okta and OPA
    Authentication (AuthN) and Authorization (AuthZ) is a common challenge when developing microservices. In this article, we will explore how t...
  • Getting started with Kafka in Python
    This article will provide an overview of Kafka and how to get started with Kafka in Python with a simple example. What is Kafka? ...
  • Getting started in GraphQL with Spring Boot
    In this article we will explore basic concepts on GraphQL and look at how to develop a microservice in Spring Boot with GraphQL support. ...

Copyright © StackStalk