Redis in Microservices Architecture

Redis can be widely used in microservices architecture. It is probably one of the few popular software solutions that may be leveraged by your application in such many different ways. Depending on the requirements it can acts as a primary database, cache, message broker. While it is also a key/value store we can use it as a configuration server or discovery server in your microservices architecture. Although it is usually defined as an in-memory data structure, we can also run it in persistent mode.
Today, I’m going to show you some examples of using Redis with microservices built on top of Spring Boot and Spring Cloud frameworks. These application will communicate between each other asynchronously using Redis Pub/Sub, using Redis as a cache or primary database, and finally used Redis as a configuration server. Here’s the picture that illustrates described architecture.

redis-micro-2.png

Redis as Configuration Server

If have already built microservices with Spring Cloud, you probably have a touch with Spring Cloud Config. It is responsible for providing distributed configuration pattern for microservices. Unfortunately Spring Cloud Config does not support Redis as a property sources backend repository. That’s why I decided to fork Spring Cloud Config project and implement this feature. I hope my implementation will soon be included into official Spring Cloud release, but for now you may use my forked repo to run it. It is available on my GitHub account piomin/spring-cloud-config. How to use it? Very simple. Let’s see.
The current SNAPSHOT version of Spring Boot is 2.2.0.BUILD-SNAPSHOT, the same as for Spring Cloud Config. While building Spring Cloud Config Server we need to include only those two dependencies as shown below.

<parent>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-parent</artifactId>
	<version>2.2.0.BUILD-SNAPSHOT</version>
</parent>
<artifactId>config-service</artifactId>
<groupId>pl.piomin.services</groupId>
<version>1.0-SNAPSHOT</version>

<dependencies>
	<dependency>
		<groupId>org.springframework.cloud</groupId>
		<artifactId>spring-cloud-config-server</artifactId>
		<version>2.2.0.BUILD-SNAPSHOT</version>
	</dependency>
</dependencies>

By default, Spring Cloud Config Server uses Git repository backend. We need to activate redis profile to force it using Redis as a backend. If your Redis instance listens on another address than localhost:6379 you need to overwrite auto-configured connection settings with spring.redis.* properties. Here’s our bootstrap.yml file.

spring:
  application:
    name: config-service
  profiles:
    active: redis
  redis:
    host: 192.168.99.100

The application main class should be annotated with @EnableConfigServer.

@SpringBootApplication
@EnableConfigServer
public class ConfigApplication {

	public static void main(String[] args) {
		new SpringApplicationBuilder(ConfigApplication.class).run(args);
	}

}

Before running the application we need to start Redis instance. Here’s the command that runs it as a Docker container and exposes on port 6379.

$ docker run -d --name redis -p 6379:6379 redis

The configuration for every application has to be available under the key ${spring.application.name} or ${spring.application.name}-${spring.profiles.active[n]}.
We have to create hash with the keys corresponding to the names of configuration properties. Our sample application driver-management uses three configuration properties: server.port for setting HTTP listening port, spring.redis.host for changing default Redis address used as a message broker and database, and sample.topic.name for setting name of topic used for asynchronous communication between our microservices. Here’s the structure of Redis hash created for driver-management visualized with RDBTools.

redis-micro-3

That visualization is an equivalent of running Redis CLI command HGETALL that return all the fields and values in a hash.

>> HGETALL driver-management
{
  "server.port": "8100",
  "sample.topic.name": "trips",
  "spring.redis.host": "192.168.99.100"
}

After setting keys and values in Redis and running Spring Cloud Config Server with active redis profile, we need to enable distributed configuration feature on the client side. To do that we just need include spring-cloud-starter-config dependency to pom.xml of every microservice.

<dependency>
	<groupId>org.springframework.cloud</groupId>
	<artifactId>spring-cloud-starter-config</artifactId>
</dependency>

We use the newest stable version of Spring Cloud.

<dependencyManagement>
	<dependencies>
		<dependency>
			<groupId>org.springframework.cloud</groupId>
			<artifactId>spring-cloud-dependencies</artifactId>
			<version>Greenwich.SR1</version>
			<type>pom</type>
			<scope>import</scope>
		</dependency>
	</dependencies>
</dependencyManagement>

The name of application is taken from property spring.application.name on startup, so we need to provide the following bootstrap.yml file.

spring:
  application:
    name: driver-management

Redis as Message Broker

Now we can proceed to the second use case of Redis in microservices-based architecture – message broker. We will implement a typical asynchronous system, which is visible on the picture below. Microservice trip-management send notification to Redis Pub/Sub after creating new trip and after finishing current trip. The notification is received by both driver-management and passenger-management, which are subscribed to the particular channel.

micro-redis-1.png

Our application is very simple. We just need to add the following dependencies in order to provide REST API and integrate with Redis Pub/Sub.

<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

We should register bean with channel name and publisher. TripPublisher is responsible for sending messages to the target topic.

@Configuration
public class TripConfiguration {

	@Autowired
	RedisTemplate<?, ?> redisTemplate;

	@Bean
	TripPublisher redisPublisher() {
		return new TripPublisher(redisTemplate, topic());
	}

	@Bean
	ChannelTopic topic() {
		return new ChannelTopic("trips");
	}

}

TripPublisher uses RedisTemplate for sending messages to the topic. Before sending it converts every message from object to JSON string using Jackson2JsonRedisSerializer.

public class TripPublisher {

	private static final Logger LOGGER = LoggerFactory.getLogger(TripPublisher.class);

	RedisTemplate<?, ?> redisTemplate;
	ChannelTopic topic;

	public TripPublisher(RedisTemplate<?, ?> redisTemplate, ChannelTopic topic) {
		this.redisTemplate = redisTemplate;
		this.redisTemplate.setValueSerializer(new Jackson2JsonRedisSerializer(Trip.class));
		this.topic = topic;
	}

	public void publish(Trip trip) throws JsonProcessingException {
		LOGGER.info("Sending: {}", trip);
		redisTemplate.convertAndSend(topic.getTopic(), trip);
	}

}

We have already implemented the logic on the publisher side. Now, we can proceed to the implementation on subscriber sides. We have two microservices driver-management and passenger-management that listens for the notifications sent by trip-management microservice. We need to define RedisMessageListenerContainer bean and set message listener implementation class.

@Configuration
public class DriverConfiguration {

	@Autowired
	RedisConnectionFactory redisConnectionFactory;

	@Bean
	RedisMessageListenerContainer container() {
		RedisMessageListenerContainer container = new RedisMessageListenerContainer();
		container.addMessageListener(messageListener(), topic());
		container.setConnectionFactory(redisConnectionFactory);
		return container;
	}

	@Bean
	MessageListenerAdapter messageListener() {
		return new MessageListenerAdapter(new DriverSubscriber());
	}

	@Bean
	ChannelTopic topic() {
		return new ChannelTopic("trips");
	}

}

The class responsible for handling incoming notification needs to implement MessageListener interface. After receiving message DriverSubscriber deserializes it from JSON to object and change driver status.

@Service
public class DriverSubscriber implements MessageListener {

	private final Logger LOGGER = LoggerFactory.getLogger(DriverSubscriber.class);

	@Autowired
	DriverRepository repository;
	ObjectMapper mapper = new ObjectMapper();

	@Override
	public void onMessage(Message message, byte[] bytes) {
		try {
			Trip trip = mapper.readValue(message.getBody(), Trip.class);
			LOGGER.info("Message received: {}", trip.toString());
			Optional<Driver> optDriver = repository.findById(trip.getDriverId());
			if (optDriver.isPresent()) {
				Driver driver = optDriver.get();
				if (trip.getStatus() == TripStatus.DONE)
					driver.setStatus(DriverStatus.WAITING);
				else
					driver.setStatus(DriverStatus.BUSY);
				repository.save(driver);
			}
		} catch (IOException e) {
			LOGGER.error("Error reading message", e);
		}
	}

}

Redis as Primary Database

Although the main purpose of using Redis is in-memory caching or key/value store it may also act as a primary database for your application. In that case it is worth to run Redis in persistent mode.

$ docker run -d --name redis -p 6379:6379 redis redis-server --appendonly yes

Entities are stored inside Redis using hash operations and mmap structure. Each entity needs to have a hash key and id.

@RedisHash("driver")
public class Driver {

	@Id
	private Long id;
	private String name;
	@GeoIndexed
	private Point location;
	private DriverStatus status;

	// setters and getters ...
}

Fortunately, Spring Data Redis provides well-known repositories pattern for Redis integration. To enable it we should annotate configuration or main class with @EnableRedisRepositories. When using Spring repositories pattern we don’t have to build any queries to Redis by ourselves.

@Configuration
@EnableRedisRepositories
public class DriverConfiguration {
	// logic ...
}

With Spring Data repositories we don’t have build any Redis queries, but just name methods following Spring Data convention. For more details, you may refer to my previous article Introduction to Spring Data Redis. For our sample purposes we can use default methods implemented inside Spring Data. Here’s declaration of repository interface in driver-management.

public interface DriverRepository extends CrudRepository<Driver, Long> {}

Don’t forget to enable Spring Data repositories by annotating the main application class or configuration class with @EnableRedisRepositories.

@Configuration
@EnableRedisRepositories
public class DriverConfiguration {
	...
}

Conclusion

As I have mentioned in the preface there are various use cases for Redis in microservices architecture. I have just presented how you can easily use it together with Spring Cloud and Spring Data to provide configuration server, message broker and database. Redis is commonly considered as a cache, but I hope that after reading this article you will change your mind about it. The sample applications source code is as usual available on GitHub: https://github.com/piomin/sample-redis-microservices.git.

Advertisements

Introduction to Spring Data Redis

Redis is an in-memory data structure store with optional durability, used as database, cache and message broker. Currently, it is the most most popular tool in the key/value stores category: https://db-engines.com/en/ranking/key-value+store. The easiest way to integrate your application with Redis is through Spring Data Redis. You can use Spring RedisTemplate directly for that or you might as well use Spring Data Redis repositories. There are some limitations when you integrate with Redis via Spring Data Redis repositories. They require at least Redis Server version 2.8.0 and do not work with transactions. Therefore you need to disable transaction support for RedisTemplate, which is leveraged by Redis repositories.
Redis is usually used for caching data stored in a relational database. In the current sample it will treated as a primary database – just for simplification. Spring Data repositories do not require any deeper knowledge about Redis from a developer. You just need to annotate your domain class properly. As usual we will examine main features of Spring Data Redis basing on the sample application. Supposing we have the system, which consists of three domain objects: Customer, Account and Transaction, here’s the picture that illustrates relationships between elements of that system. Transaction is always related with two accounts: sender (fromAccountId) and receiver (toAccountId). Each customer may have many accounts.

redis-1 (1).png

Although the picture visible above shows three independent domain models, customer and account is stored in the same, single structure. All customer’s accounts are stored as a list inside customer object. Before proceeding to the sample application implementation details let’s begin from starting Redis database.

1. Running Redis on Docker

We will run Redis standalone instance locally using its Docker container. You can start it in in-memory mode or with persistence store. Here’s the command that run single, in-memory instance of Redis on Docker container. It is exposed outside on default 6379 port.

$ docker run -d --name redis -p 6379:6379 redis

2. Enabling Redis Repositories and Configuring Connection

I’m using Docker Toolbox, so each container is available for me under address 192.168.99.100. Here’s the only one property that I need to override inside configuration settings (application.yml).

spring:
  application:
    name: sample-spring-redis
  redis:
    host: 192.168.99.100

To enable Redis repositories for Spring Boot application we just need to include the single starter <code>spring-boot-starter-data-redis</code>.

<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>

We may choose between two supported connectors: Lettuce and Jedis. For Jedis I had to include one additional client’s library to dependencies, so I decided to use simpler option – Lettuce, that does not require any additional libraries to work properly. To enable Spring Data Redis repositories we also need to annotate the main or the configuration class with @EnableRedisRepositories and declare RedisTemplate bean. Although we do not use RedisTemplate directly, we still need to declare it, while it is used by CRUD repositories for integration with Redis.

@Configuration
@EnableRedisRepositories
public class SampleSpringRedisConfiguration {

    @Bean
    public LettuceConnectionFactory redisConnectionFactory() {
        return new LettuceConnectionFactory();
    }

    @Bean
    public RedisTemplate<?, ?> redisTemplate() {
        RedisTemplate<byte[], byte[]> template = new RedisTemplate<>();
        template.setConnectionFactory(redisConnectionFactory());
        return template;
    }

}

3. Implementing domain entities

Each domain entity has at least to be annotated with @RedisHash, and contains property annotated with @Id. Those two items are responsible for creating the actual key used to persist the hash. Besides identifier properties annotated with @Id you may also use secondary indices. To good news about it is that it can be not only with dependent single objects, but also on lists and maps. Here’s the definition of Customer entity. It is available on Redis under customer key. It contains list of Account entities.

@RedisHash("customer")
public class Customer {

    @Id private Long id;
    @Indexed private String externalId;
    private String name;
    private List<Account> accounts = new ArrayList<>();

    public Customer(Long id, String externalId, String name) {
        this.id = id;
        this.externalId = externalId;
        this.name = name;
    }

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getExternalId() {
        return externalId;
    }

    public void setExternalId(String externalId) {
        this.externalId = externalId;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public List<Account> getAccounts() {
        return accounts;
    }

    public void setAccounts(List<Account> accounts) {
        this.accounts = accounts;
    }

    public void addAccount(Account account) {
        this.accounts.add(account);
    }

}

Account does not have its own hash. It is contained by Customer has as list of objects. The property id is indexed on Redis in order to speed-up the search based on the property.

public class Account {

    @Indexed private Long id;
    private String number;
    private int balance;

    public Account(Long id, String number, int balance) {
        this.id = id;
        this.number = number;
        this.balance = balance;
    }

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getNumber() {
        return number;
    }

    public void setNumber(String number) {
        this.number = number;
    }

    public int getBalance() {
        return balance;
    }

    public void setBalance(int balance) {
        this.balance = balance;
    }

}

Finally, let’s take a look on Transaction entity implementation. It uses only account ids, not the whole objects.

@RedisHash("transaction")
public class Transaction {

    @Id
    private Long id;
    private int amount;
    private Date date;
    @Indexed
    private Long fromAccountId;
    @Indexed
    private Long toAccountId;

    public Transaction(Long id, int amount, Date date, Long fromAccountId, Long toAccountId) {
        this.id = id;
        this.amount = amount;
        this.date = date;
        this.fromAccountId = fromAccountId;
        this.toAccountId = toAccountId;
    }

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public int getAmount() {
        return amount;
    }

    public void setAmount(int amount) {
        this.amount = amount;
    }

    public Date getDate() {
        return date;
    }

    public void setDate(Date date) {
        this.date = date;
    }

    public Long getFromAccountId() {
        return fromAccountId;
    }

    public void setFromAccountId(Long fromAccountId) {
        this.fromAccountId = fromAccountId;
    }

    public Long getToAccountId() {
        return toAccountId;
    }

    public void setToAccountId(Long toAccountId) {
        this.toAccountId = toAccountId;
    }

}

4. Implementing repositories

The implementation of repositories is the most pleasant part of our exercise. As usual with Spring Data projects, the most common methods like save, delete or findById are already implemented. So we only have to create our custom find methods if needed. Since usage and implementation of findByExternalId method is rather obvious, the method findByAccountsId may be not. Let’s move back to a model definition to clarify usage of that method. Transaction contains only account ids, it does not have direct relationship with Customer. What if we need to know the details about customers being a sides of a given transaction? We can find customer by one of its account from the list.

public interface CustomerRepository extends CrudRepository {

    Customer findByExternalId(String externalId);
    List findByAccountsId(Long id);

}

Here’s implementation of repository for Transaction entity.

public interface TransactionRepository extends CrudRepository {

    List findByFromAccountId(Long fromAccountId);
    List findByToAccountId(Long toAccountId);

}

5. Building repository tests

We can easily test Redis repositories functionality using Spring Boot Test project with @DataRedisTest. This test assumes you have running instance of Redis server on the already configured address 192.168.99.100.

@RunWith(SpringRunner.class)
@DataRedisTest
@FixMethodOrder(MethodSorters.NAME_ASCENDING)
public class RedisCustomerRepositoryTest {

    @Autowired
    CustomerRepository repository;

    @Test
    public void testAdd() {
        Customer customer = new Customer(1L, "80010121098", "John Smith");
        customer.addAccount(new Account(1L, "1234567890", 2000));
        customer.addAccount(new Account(2L, "1234567891", 4000));
        customer.addAccount(new Account(3L, "1234567892", 6000));
        customer = repository.save(customer);
        Assert.assertNotNull(customer);
    }

    @Test
    public void testFindByAccounts() {
        List<Customer> customers = repository.findByAccountsId(3L);
        Assert.assertEquals(1, customers.size());
        Customer customer = customers.get(0);
        Assert.assertNotNull(customer);
        Assert.assertEquals(1, customer.getId().longValue());
    }

    @Test
    public void testFindByExternal() {
        Customer customer = repository.findByExternalId("80010121098");
        Assert.assertNotNull(customer);
        Assert.assertEquals(1, customer.getId().longValue());
    }
}

6. More advanced testing with Testcontainers

You may provide some advanced integration tests using Redis as Docker container started during the test by Testcontainer library. I have already published some articles about Testcontainers framework. If you would like read more details about it please refer to my previous articles: Microservices Integration Tests with Hoverfly and Testcontainers and Testing Spring Boot Integration with Vault and Postgres using Testcontainers Framework.

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@RunWith(SpringRunner.class)
public class CustomerIntegrationTests {

    @Autowired
    TestRestTemplate template;

    @ClassRule
    public static GenericContainer redis = new GenericContainer("redis:5.0.3").withExposedPorts(6379);

    @Before
    public void init() {
        int port = redis.getFirstMappedPort();
        System.setProperty("spring.redis.host", String.valueOf(port));
    }

    @Test
    public void testAddAndFind() {
        Customer customer = new Customer(1L, "123456", "John Smith");
        customer.addAccount(new Account(1L, "1234567890", 2000));
        customer.addAccount(new Account(2L, "1234567891", 4000));
        customer = template.postForObject("/customers", customer, Customer.class);
        Assert.assertNotNull(customer);
        customer = template.getForObject("/customers/{id}", Customer.class, 1L);
        Assert.assertNotNull(customer);
        Assert.assertEquals("123456", customer.getExternalId());
        Assert.assertEquals(2, customer.getAccounts().size());
    }

}

7. Viewing data

Now, let’s analyze the data stored in Redis after our JUnit tests. We may use one of GUI tool for that. I decided to install RDBTools available on site https://rdbtools.com. You can easily browse data stored on Redis using this tool. Here’s the result for customer entity with id=1 after running JUnit test.

redis-2

Here’s the similar result for transaction entity with id=1.

redis-3

Source Code

The sample application source code is available on GitHub in the repository sample-spring-redis.