Introduction
REmote DIctionary Server (Redis) is an in-memory data structure store. It can be used as a simple database, a message broker and for caching through its support for various data structures.
In this article, we'll be creating a simple CRUD application and integrating Redis with Spring Boot. To achieve CRUD functionality, we'll rely on the HashOperations
interface provided by the Spring Data Redis project.
Redis
Redis is an open-source in-memory data store written in C, which makes it blazingly fast. Because of its faster read/writes operations, it is commonly used for caching data. Data is stored in Redis in the form of key-values where a key is used to extract the values.
Redis can also persist data on a disk, rather than hold it in-memory, using "snapshots" - by copying its in-memory data store at regular intervals.
Prerequisites
Installing Redis
Redis can easily be installed on Linux and macOS. Windows requires a bit of hacking, though. We'll install Redis on an AWS EC2 instance running Ubuntu 18.04 LTS:
$ sudo apt install redis-server
macOS, you can install it via brew
:
$ brew install redis
After successfully installing the redis-server
package, let's check the status of the Redis process to verify the installation:
$ systemctl status redis
The result shows the status of the redis-server
, the bind-address and the port on which redis-server
is listening on:
● redis-server.service - Advanced key-value store
Loaded: loaded (/lib/systemd/system/redis-server.service; enabled; vendor preset: enabled)`
Active: active (running) since Tue 2020-03-10 10:06:30 UTC; 3min 2s ago
Docs: http://redis.io/documentation,
man:redis-server(1)
Main PID: 2227 (redis-server)
Tasks: 4 (limit: 1152)
CGroup: /system.slice/redis-server.service
└─2227 /usr/bin/redis-server 127.0.0.1:6379
By default, the Redis configuration will be available in the /etc/redis/redis.conf
file.
Enable Remote Connection to Redis
By default, Redis is accessible only from localhost
. To enable remote connection to our Redis server, update the bind address in Redis configuration to 0.0.0.0
:
bind 0.0.0.0 ::1
Once updated, restart it:
$ systemctl restart redis
Setting up Spring Boot
The easiest way to start off with a blank Spring Boot app is to use Spring Initializr:
Alternatively, you can also use the Spring Boot CLI to bootstrap the application:
$ spring init --dependencies=spring-boot-starter-data-redis redis-spring-boot-demo
We're starting off with the spring-boot-starter-data-redis
dependency as it includes spring-data-redis
, spring-boot-starter
and lettuce-core
.
If you already have a Maven/Spring application, add the dependency to your pom.xml
file:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<version>${version}</version>
</dependency>
Or if you're using Gradle:
compile group: 'org.springframework.data', name: 'spring-data-redis', version: '${version}'
Connecting to the Redis Server
As always, when working with services like Redis - we want to connect our application to the service. Multiple Java-based Redis connectors are available - Jedis and Lettuce are two popular options.
Connecting With Jedis
To use Jedis, we'll have to add it to our pom.xml
file:
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>${version}</version>
</dependency>
Or if you're using Gradle:
compile group: 'redis.clients', name: 'jedis', version: '${version}'
Once the dependencies are in place, we need to set up the JedisConnectionFactory
:
@Configuration
public class Config {
@Bean
public JedisConnectionFactory redisConnectionFactory() {
JedisConnectionFactory jedisConnectionFactory = new JedisConnectionFactory();
jedisConnectionFactory.setHostName("your_host_name_or_ip");
jedisConnectionFactory.setPort(6379);
jedisConnectionFactory.afterPropertiesSet();
return jedisConnectionFactory;
}
}
Needless to say, this setup is performed in a @Configuration
class. If you'd like to read more about Spring Framework's Core Annotations, we've got you covered!
Connecting With Lettuce
Lettuce is Netty-based and open-source Redis connector which is packaged with the starter dependency. The setup for a Lettuce connection factory is pretty much the same as for Jedis:
@Configuration
public class Config {
@Bean
public LettuceConnectionFactory redisConnectionFactory() {
LettuceConnectionFactory lcf = new LettuceConnectionFactory();
lcf.setHostName("your_host_name_or_ip");
lcf.setPort(6379);
lcf.afterPropertiesSet();
return lcf;
}
}
Although their setup is pretty much the same, their usage is different. Lettuce allows for asynchronous operations and is thread-safe, unlike Jedis, for example.
RedisTemplate
RedisTemplate
is an entry-class provided by Spring Data through which we interact with the Redis server.
We'll pass a RedisConnectionFactory
instance to the RedisTemplate
to establish a connection:
public static RedisTemplate<String, User> redisTemplate() {
RedisTemplate<String, User> redisTemplate = new RedisTemplate<String ,User>();
redisTemplate.setConnectionFactory(redisConnectionFactory());
redisTemplate.afterPropertiesSet();
return redisTemplate;
}
Check out our hands-on, practical guide to learning Git, with best-practices, industry-accepted standards, and included cheat sheet. Stop Googling Git commands and actually learn it!
Once established, RedisTemplate
becomes the main abstraction of Redis' operations that we can command. It also takes care of serialization and deserialization of objects to byte arrays.
By default, RedisTemplate
uses the JdkSerializationRedisSerializer
to serialize and deserialize objects.
The serialization mechanism of RedisTemplate
can be changed, and Redis offers several serializers in the org.springframework.data.redis.serializer
package.
StringRedisTemplate
is an extension of RedisTemplate
focused on string-based key-value pairs, for example.
To support various operations on different datatypes, RedisTemplate
provides operation classes like ValueOperations
, ListOperations
, SetOperations
, HashOperations
, StreamOperations
, etc.
For hash-related operations, which we'll use to store data in our Redis server, we'll use the HashOperations
class.
HashOperations
Redis Hashes can hold an n
number of key-value pairs and are designed to use less memory, making it a great way for storing objects in-memory. Through the HashOperations
helper class, we can manipulate them.
To use any of these, we pack the returned hash operations from the RedisTemplate
instance into the HashOperations
interface:
HashOperations hashOperations = redisTemplate.opsForHash();
These operations include basic hash map operations such as put()
, get()
, entries()
, etc:
// Sets user object in USER hashmap at userId key
hashOperations.put("USER", user.getUserId(), user);
// Get value of USER hashmap at userId key
hashOperations.get("USER", userId);
// Get all hashes in USER hashmap
hashOperations.entries("USER");
// Delete the hashkey userId from USER hashmap
hashOperations.delete("USER", userId);
Defining a User Repository
Now let us go ahead and create a user repository which will actually handle the CRUD operations:
@Repository
public class UserRepository {
private HashOperations hashOperations;
public UserRepository(RedisTemplate redisTemplate) {
this.hashOperations = redisTemplate.opsForHash();
}
}
By contrast, in a typical repository, the HashOperations
would be something like a SessionFactory
. Using Redis, you can also make a repository by extending the CrudRepository
interface and setting up a Jedis connection in a @Bean
.
In the constructor, we pass our redisTemplate
, which should be configured with a Redis connection factory.
Now, to put an entry, we'd use:
hashOperations.put("USER", hashKey, value);
A single key, such as the USER
can have multiple hashKey:value
pairs. Each value
can be accessed via hashKey
s for a given key.
Or to get an entry, we'd use:
value = hashOperations.get("USER", hashKey);
Let's define the User
:
public class User {
private int userId;
private String name;
// Constructor, getters and setters
}
With that in mind, let's implement the rest of the repository:
@Repository
public class UserRepository {
final Logger logger = LoggerFactory.getLogger(UserRepository.class);
private HashOperations hashOperations;
public UserRepository(RedisTemplate redisTemplate) {
this.hashOperations = redisTemplate.opsForHash();
}
public void create(User user) {
hashOperations.put("USER", user.getUserId(), user);
logger.info(String.format("User with ID %s saved", user.getUserId()));
}
public User get(String userId) {
return (User) hashOperations.get("USER", userId);
}
public Map<String, User> getAll(){
return hashOperations.entries("USER");
}
public void update(User user) {
hashOperations.put("USER", user.getUserId(), user);
logger.info(String.format("User with ID %s updated", user.getUserId()));
}
public void delete(String userId) {
hashOperations.delete("USER", userId);
logger.info(String.format("User with ID %s deleted", userId));
}
}
Now to test the application, let's use our userRepository
:
UserRepository userRepository = new UserRepository(redisTemplate());
userRepository.create(new User("1", "username", "emailid"));
User user = userRepository.get("1");
userRepository.update(user);
userRepository.delete(user.getUserId());
Running this piece of code will yield:
2020-03-30 11:34:11.260 INFO 8772 --- [ main] c.h.redistutorial.UserRepository : User with ID 1 saved
2020-03-30 11:34:11.260 INFO 8772 --- [ main] c.h.redistutorial.UserRepository : User with ID 1 updated
2020-03-30 11:34:11.260 INFO 8772 --- [ main] c.h.redistutorial.UserRepository : User with ID 1 deleted
Let's use a Redis client to see the data being inserted, updated and deleted.
-
Creating a user:
-
Updating a user:
-
Deleting a user:
Conclusion
Spring Boot is increasingly becoming preferred by Java/Spring developers because it's lightweight and simple to use. It greatly simplifies the process of bootstrapping applications and helps you concentrate on the real business logic rather than wiring things together.
Redis, on the other hand, is an extremely popular in-memory database which makes it a great companion for microservices.
Redis is often used for cache management by microservices to reduce the number of database calls to the server. In this new world of pay for usage charging systems, this can effectively bring down the operational costs for businesses and enterprises.