Spring Boot with Redis: HashOperations CRUD Functionality

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:

spring boot 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;
}

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 hashKeys 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:
    created user in redis

  • Updating a user:
    get user from redis

  • Deleting a user:
    user deleted from redis

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.

Author image
About Vipin KP