In this tutorial, we will be discussing the integration of Redis cache with Spring Boot by developing a CRUD operation example using Jedis and spring boot starter data redis. In the tutorial, we will explore 2 different ways to perform CRUD operations. First, we will be defining our custom RedisTemplate and use HashOperations to perform get and put operations on Redis server. Next, we will be using @Cacheable annotation to enable implicit caching mechanism by Spring Boot and upon cache miss, the lookup will be done in the DB with spring boot starter data jpa.
What is Redis
Redis is an in-memory data structure store implementing a distributed, in-memory key-value database with optional durability. It can be used as a database, cache or as a message broker. Redis supports different kinds of abstract data structures, such as strings, lists, maps, sets, sorted sets, HyperLogLogs, bitmaps, streams, and spatial indexes.
Redis is very fast as it is written in ANSI C and works in most POSIX systems like Linux, *BSD, OS X without external dependencies. Redis has built-in replication, Lua scripting, LRU eviction, transactions and different levels of on-disk persistence.
Redis is best suited for caching in a distributed system and in this age of microservices Redis cache is very useful.
Setting up Redis Server Locally
First of all, we need to have Redis server running and we should have the connection properties such as host, port, password, etc. For this example, I have installed Redis server on my local machine. You can follow my Redis server installation on Windows and Mac tutorial to setup Redis server locally.
Once, the Redis server setup is done you can actually run below commands from the redis-cli.
Spring Boot Redis Cache Maven Configuration
We can use spring-boot-starter-data-redis maven artifact provided by Spring boot to integrate Redis with spring boot project. Below are the maven dependencies that we require for the integration.
<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> <dependency> <groupId>redis.clients</groupId> <artifactId>jedis</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> </dependencies>
Jedis is a blazingly small and sane Redis java client.
spring-boot-starter-data-redis
provides Redis based operations and integrations similar to spring data.
spring-boot-starter-web is for exposing the REST endpoints in order to test our CRUD operations.
Spring Boot Redis Configuration
Java Based Configuration
We require to define 2 Spring beans, JedisConnectionFactory and RedisTemplate to configure Redis with Spring Boot. Below are the beans definition:
@Configuration public class BeanConfig { @Bean JedisConnectionFactory jedisConnectionFactory() { RedisStandaloneConfiguration redisStandaloneConfiguration = new RedisStandaloneConfiguration("localhost", 6379); redisStandaloneConfiguration.setPassword(RedisPassword.of("password")); return new JedisConnectionFactory(redisStandaloneConfiguration); } @Bean public RedisTemplateredisTemplate() { RedisTemplate template = new RedisTemplate<>(); template.setConnectionFactory(jedisConnectionFactory()); return template; } }
JedisConnectionFactory setHostName is deprecated and hence we require to use RedisStandaloneConfiguration to configure JedisConnectionFactory with our custom hostname, port and password.
Property File Based Configuration
We can also provide these configurations via properties file configuration. Below are the application.properties file entries required to configure redis cache in aSpring Boot app.
spring.redis.host=127.0.0.1 spring.redis.password=password spring.redis.port=6379
We can define connection pool related configurations too in the above properties file.
Spring Boot Redis Crud Operations
Defining Redis Repository
Let us define our Redis repository class that will have all the implementations for put, get, delete, etc. We have injected RedisTemplate and HashOperations here.
@Repository
public class RedisUserRepository {
private HashOperations hashOperations;
private RedisTemplate redisTemplate;
public RedisUserRepository(RedisTemplate redisTemplate){
this.redisTemplate = redisTemplate;
this.hashOperations = this.redisTemplate.opsForHash();
}
...
}
This implementation is useful when we do not require any persistent storage of any object in DB. There are many use cases which requires in-memoery calculations and operations and store those results for specific period. We will discuss about the persistent stoarge and cache hit and cache miss related topics in later sections below.
Redis PUT Operation
Any insert operation such as put, putIfAbsent, and putAll requires 3 parameters. Below id a simple syntax for PUT operation.
void put(H key, HK hashKey, HV value);
You can assume key as a kind of partition in the Redis cache and this functionality is very handy in caching varities of data for quick operations. In our below examples, we are using the key as USER
to save the user info.
Now, the hashKey is the unique queue that is used to save the value. In our case, the value is user info.
PUT operation sets the value of the supplied hash key. Below is the implementation that adds user object to the Redis cache with a unique hashkey of user id under the key USER
.
public void save(User user) { hashOperations.put("USER", user.getId(), user); }
Redis GET Operation
GET operation fetches the value for given hashKey (id) and key as USER. This operation returns a Java object. Hence, we need to expilicitly cast the object to user. Make sure, the User class implements Serializable
public User findById(String id){ return (User) hashOperations.get("USER", id); }
Redis DELETE Operation
DELETE operation removes the cached object with unique key(id) under the key USER. This operation can only delete the unique entries under the key USER
. To delete the USER key itself, we need to use RedisTemplate which we will discuss later.
public void delete(String id){ hashOperations.delete("USER", id); }
Redis VALUES Operation
VALUES operation retrieves all the values from the key. Below implementation returns a list object but there are operations to return key value pairs.
public ListfindAll(){ return hashOperations.values("USER"); }
Redis MultiGet Operation
With multiGet()
, we can supply multiple keys at once and get the cached result. The point to remember here is that returned object by this operation is a list of list and hence while parsing the result to a meaningful collection, we need to follow the keys order that we supplied. The list will be returned in the same order.
Below is an example of parsing the result of multiGet() operation of HashOperation.
public Map<String, List<User>> multiGetUsers(List<String> userIds){ Map<String, List<User>> userMap = new HashMap<>(); List<Object> users = hashOperations.multiGet("USER", userIds); for (int i = 0; i < userIds.size(); i++) { userMap.put(userIds.get(i), (List<User>) users.get(i)); } return userMap; }
Putting it All Together
@Repository public class RedisUserRepository { private HashOperations hashOperations; private RedisTemplate redisTemplate; public RedisUserRepository(RedisTemplate redisTemplate){ this.redisTemplate = redisTemplate; this.hashOperations = this.redisTemplate.opsForHash(); } public void save(User user){ hashOperations.put("USER", user.getId(), user); } public ListfindAll(){ return hashOperations.values("USER"); } public User findById(String id){ return (User) hashOperations.get("USER", id); } public void update(User user){ save(user); } public void delete(String id){ hashOperations.delete("USER", id); } }
Controller Implementation
Below is the controller implementation for testing all the CRUD operations.
@RestController @RequestMapping("/users") public class UserController { @Autowired private RedisUserRepository userRepository; @PostMapping public User save(@RequestBody User user){ userRepository.save(user); return user; } @GetMapping public Listlist(){ return userRepository.findAll(); } @GetMapping("/{id}") public User getUser(@PathVariable String id){ return userRepository.findById(id); } @PutMapping public User update(@RequestBody User user){ userRepository.update(user); return user; } @DeleteMapping("/{id}") public String deleteUser(@PathVariable String id){ userRepository.delete(id); return id; } }
Redis Sentinel Configuration
For the high availability of Redis in a production system, we use Sentinel configuration. In a sentinel configuration, we have a Master and Slave instances running together in a cluster. So, whenever the Master node is down, the sentinel will automatically promote the SLAVE to Master and other SLAVE instances are automatically configured to use the new Master instance.
Now, let us configure the same in a Spring Boot app to use Redis Sentinel. We have below entries in our application.properties file for Redis Sentinel.
redis.sentinel.master.name=devglan redis.sentinel.host.and.ports=13.232.155.79:26379;13.232.155.88:26379;13.232.154.78:26379
With above configurattion, we can define our JedisConnectionFactory as below:
@Bean(name = "jedisConnectionFactory") public JedisConnectionFactory jedisConnectionFactory() { RedisSentinelConfiguration redisSentinelConfiguration = new RedisSentinelConfiguration(); redisSentinelConfiguration.master(environment.getProperty("redis.sentinel.master.name")); String redisSentinelHostAndPorts = environment.getProperty("redis.sentinel.host.and.ports"); HostAndPort hostAndPort; if (redisSentinelHostAndPorts.contains(";">)) { for (String node : redisSentinelHostAndPorts.split(";")) { if (null != node & node.contains(":")) { String [] nodeArr = node.split(":"); hostAndPort = new HostAndPort(nodeArr[0], Integer.parseInt(nodeArr[1])); redisSentinelConfiguration.sentinel(hostAndPort.getHost(), hostAndPort.getPort()); } } } else { if (redisSentinelHostAndPorts.contains(":")) { String [] nodeArr = redisSentinelHostAndPorts.split(":"); hostAndPort = new HostAndPort(nodeArr[0], Integer.parseInt(nodeArr[1])); redisSentinelConfiguration.sentinel(hostAndPort.getHost(), hostAndPort.getPort()); } } return new JedisConnectionFactory(redisSentinelConfiguration, poolConfig()); } @Bean public JedisPoolConfig poolConfig() { final JedisPoolConfig jedisPoolConfig = new JedisPoolConfig(); jedisPoolConfig.setTestOnBorrow(true); jedisPoolConfig.setMaxTotal(100); jedisPoolConfig.setMaxIdle(100); jedisPoolConfig.setMinIdle(10); jedisPoolConfig.setTestOnReturn(true); jedisPoolConfig.setTestWhileIdle(true); return jedisPoolConfig; }
Testing the Spring Boot Redis Cache
Below are the endpoints that can be used to test our above implementation.
Redis PUT Operation Test
Method : POST URL : http://localhost:8080/users Body : { "name":"dhirjjaj", "age": 23, "id":"1234" }
Redis GET Operation Test
Method : GET URL : http://localhost:8080/users
Redis UPDATE Operation Test
HTTP Method : PUT URL : http://localhost:8080/users Body : { "name":"dhirjjaj", "age": 23, "id":"1234" }
Redis DELETE Operation Test
HTTP Method : DELETE URL : http://localhost:8080/users/1234
Spring Boot Data Redis Implementation
In the above implementation, Redis itself is acting as a store house for our user entity. Above use case is useful when we don't require any persistent storage. For a persistent storage, we need to have a DB and upon a cache miss, our integration should perform a looup in the DB. For this we need to have spring-boot-starter-data-jpa module from spring boot.
Let us not discuss this integration here as it is a seperate topic of integration of spring data with spring boot. Here, we assume we have already spring data integrated and repository implementation is done. The complete implementation of spring boot data can be found here.
Hence, let us directly implement our service class that will have caching enabled with method level annotation @Caheable and upon cache miss a DB lookup is made.
Spring Boot Data Repository
Once, we have below dependencies added in the pom, we can define our repository.
ltdependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency>
@Cacheable in Spring Boot Data Redis
The @Cacheable annotation can be applied at method level. When applied at method level, then the annotated method’s return value is cached with key as the method parameter. You can use other params such as cacheName, cacheManager, conditions with it.
You need to annotate your spring boot main class with @EnableCaching
to enable caching.
Below is an example of using @Cacheable. If the cache is missed then the method will be executed and it will fetch user from the repository and the same will be cached in Redis.
@Cacheable(value = "users", key = "#userId") public User getUser(String userId) { return userRepository.findOne(Long.valueOf(userId)); }
Conclusion
In this tutorial, we will discussed the integration of Redis cache with Spring Boot by developing a CRUD operation example using Jedis and spring boot starter data redis.