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.
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 redisTo 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 installNow 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 PONGLet 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 connectionsNow 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
<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
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
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
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
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
package com.stackstalk.democache; import org.springframework.data.repository.CrudRepository; public interface UserRepository extends CrudRepository<UserDetails, Integer> { }
Define the service layer
- @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.
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 ..
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.
0 comments:
Post a Comment