Introducing Spring Data Redis

Author: Brian Sam-Bodden

Objectives#

Configure a RedisTemplate and learn how to access different operation bundles to read and write data to and from Redis in a Spring REST controller.

Agenda#

In this lesson, students will learn:

  • How to configure the connection to Redis from the application
  • How to access and configure the Spring Data RedisTemplate
  • How to use opsForXXX to read and write data to Redis
  • The basis of Spring’s @RestController

If you get stuck:

Introducing Spring Data Redis#

Spring Data Redis provides the abstractions of the Spring Data platform to Redis.

  • Spring Data makes it easy to use data access technologies, relational and non-relational databases, map-reduce frameworks, and cloud-based data services. To learn more about Spring Data, see https://spring.io/projects/spring-data.
  • Spring Data Redis provides access to Redis from Spring applications. It offers both low-level and high-level abstractions for interacting with Redis.

Redis template#

We will start by configuring a RedisTemplate, a class that provides a thread-safe bridge between Spring and Redis Commands. It handles connection management, freeing the developer from opening and closing Redis connections.

Start from the main application class, Redi2readApplication, located at src/main/java/com/redislabs/edu/redi2read/Redi2readApplication.java.

Add a @Bean annotated method called redisTemplate, which takes a RedisConnectionFactory and returns a RedisTemplate. This method will provide a configured bean of type RedisTemplate in the Spring container. We can inject this bean wherever we need to access Redis.

@Bean
public RedisTemplate<?, ?> redisTemplate(RedisConnectionFactory connectionFactory) {
RedisTemplate<?, ?> template = new RedisTemplate<>();
template.setConnectionFactory(connectionFactory);
return template;
}

This requires the following imports:

import org.springframework.context.annotation.Bean;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;

Notice that while the template types are generic, it is up to the serializers/deserializers to convert the given Objects to-and-from binary data correctly.

We could also configure the Redis host and port programmatically by defining a @Bean annotated method that returns a RedisConnectionFactory (either a JedisConnectionFactory or LettuceConnectionFactory) and use the setHostName and setPort methods.

But since Spring Data Redis can configure the beans using a properties file (either Java Properties or YAML), we will use the applications.properties file instead.

Spring Data Redis properties are prefixed with spring.redis.. In the file src/main/resources/application.properties add the following properties:

spring.redis.host=localhost
spring.redis.port=6379

While we're at it, let’s also add an exclusion for the autoconfiguration of Spring Security. Since we’ve included the Maven dependency for Spring Security but have not yet configured it, Spring defaults on the safe side and protects all endpoints on the application. So, for now, we’ll disable security autoconfiguration until we decide to secure our services.

spring.redis.host=localhost
spring.redis.port=6379
spring.autoconfigure.exclude=org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration

Using the redis template#

To test the RedisTemplate, we’ll create a REST controller and use it to perform some operations against our Redis instance.

We will add the controller under the src/main/java/com/redislabs/edu/redi2read/controllers folder, which means it’ll live in the com.redislabs.edu.redi2read.controllers package.

package com.redislabs.edu.redi2read.controllers;
public class HelloRedisController {
}

Next, let’s annotate the class with the @RestController and the @RequestMapping annotations. The controller will now listen to requests rooted at http://localhost:8080/api/redis.

@RestController
@RequestMapping("/api/redis")
public class HelloRedisController {
}

Add the necessary import as shown next:

import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

Next, let’s inject an @Autowired instance of RedisTemplate. Notice that we will use concrete classes for the K and V parameter classes in RedisTemplate<K,V>. K is the Redis key type (usually a String) and V, the Redis value type (i.e., something that maps to a Redis data structure).

@RestController
@RequestMapping("/api/redis")
public class HelloRedisController {
@Autowired
private RedisTemplate<String, String> template;
}

Add the necessary import as shown next:

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;

Now, all we need is a controller method to run some Redis commands. We will use the Redis SET command (https://redis.io/commands/set) as we previously demonstrated using the Redis CLI.

First, we’ll create a String that will serve to prefix the keys that we write to Redis:

private static final String STRING_KEY_PREFIX = "redi2read:strings:";

The method is annotated with the @PostMapping with a path of /strings, making the effective path for our post /api/redis/strings. The @Request body and the return value of the method is a Map.Entry<String, String> which is convenient when dealing with name-value pairs.

@PostMapping("/strings")
@ResponseStatus(HttpStatus.CREATED)
public Map.Entry<String, String> setString(@RequestBody Map.Entry<String, String> kvp) {
return kvp;
}

Add the necessary import as shown next:

import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.ResponseStatus;

If we launch the application now with:

./mvnw clean spring-boot:run

We can use curl to invoke our api/redis/strings endpoint:

$ curl --location --request POST 'http://localhost:8080/api/redis/strings' \
--header 'Content-Type: application/json' \
--data-raw '{ "database:redis:creator": "Salvatore Sanfilippo" }'
{"database:redis:creator":"Salvatore Sanfilippo"}

This results in the JSON payload being echoed back. Let's complete the implementation of the setString method so that we can write a Redis string to the database:

@PostMapping("/strings")
@ResponseStatus(HttpStatus.CREATED)
public Map.Entry<String, String> setString(@RequestBody Map.Entry<String, String> kvp) {
template.opsForValue().set(STRING_KEY_PREFIX + kvp.getKey(), kvp.getValue());
return kvp;
}

We will use the RedisTemplate instance template opsForValue() method to get an instance of ValueOperations, which provides methods to execute operations performed on simple values (or Strings in Redis terminology). The Redis SET method is implemented using the (you guessed it!) set() method, which takes a key name and a value. We are prepending the KEY_SPACE_PREFIX to the key that's provided as an argument. Before you fire up another curl request, let’s start a Redis CLI instance with the MONITOR flag so that we can watch what transpires when we hit the server.

$ redis-cli MONITOR

Now, when you issue the POST request again, you should see output similar to:

1617346602.221390 [0 172.19.0.1:58396] "SET" "redi2read:strings:database:redis:creator" "Salvatore Sanfilippo"

We can launch another Redis CLI to query Redis ourselves:

127.0.0.1:6379> KEYS *
1) "redi2read:strings:database:redis:creator"
127.0.0.1:6379> TYPE "redi2read:strings:database:redis:creator"
string
127.0.0.1:6379> GET "redi2read:strings:database:redis:creator"
"Salvatore Sanfilippo"
127.0.0.1:6379>

If we use the KEYS * command, we can see all of the keys stored in Redis (don’t do this on a production box with a lot of data, as you’ll block your Redis instance while serving a massive response). The redi2read:strings:database:redis:creator key has been created, and it is a Redis String with a value of Salvatore Sanfilipo We can now write strings to Redis through our REST controller. Next, let’s add a corresponding GET method to our controller to read string values:

@GetMapping("/strings/{key}")
public Map.Entry<String, String> getString(@PathVariable("key") String key) {
String value = template.opsForValue().get(STRING_KEY_PREFIX + key);
if (value == null) {
throw new ResponseStatusException(HttpStatus.NOT_FOUND, "key not found");
}
return new SimpleEntry<String, String>(key, value);
}

With imports:

import java.util.AbstractMap.SimpleEntry;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.server.ResponseStatusException;

We can now issue a GET request to retrieve String keys:

$ curl --location --request GET 'http://localhost:8080/api/redis/strings/database:redis:creator'
{"database:redis:creator":"Salvatore Sanfilippo"}

On the Redis CLI monitor you should see:

1617347871.901585 [0 172.19.0.1:58284] "GET" "redi2read:strings:database:redis:creator"

Note that in order to return an error on a key not found, we have to check the result for null and throw an appropriate exception.

{
"timestamp": "2021-04-02T07:45:10.303+00:00",
"status": 404,
"error": "Not Found",
"trace": "org.springframework.web.server.ResponseStatusException: 404...\n",
"message": "key not found",
"path": "/api/redis/strings/database:neo4j:creator"
}

Keep in mind that this is a “development time” exception, appropriate to be shown on an error page meant for developers. Likely, we would intercept this exception and create an API appropriate response (likely just the status and error fields above).