Event-Driven Architecture Using Redis Streams

Event-Driven Architecture Using Redis Streams

Harness implemented an event-driven architecture using Redis Streams to enhance microservices communication, achieving faster delivery and scalability with minimal infrastructure changes, suitable for thousands of messages per minute.

The modern software industry is advancing rapidly. In a monolithic world, it was easy to put everything on the same machine, store the application state in a couple of databases (relational/non-relational), and scale servers horizontally. As the scale and the variety of use cases increased, the industry started adapting and introduced microservices - one service does a concentrated job and communicates just enough to other services to get the ball rolling.

How does state transfer happen with multiple services? One easy answer would be to make every service expose REST endpoints and make HTTP calls as and when required to the relevant services. This will definitely work well for some kinds of applications, but not all. If your application communicates synchronously (user-service as well as service-service communication), there is no need for additional complexity and you can just rely on HTTP calls. However, dealing with such a pattern can be challenging in the longer run for reasons like:

These are some of the issues which event-driven architecture fundamentally tries to address by making sure each service is doing what it is supposed to do and when it is supposed to do.

Event-driven architecture uses events to communicate among various independent microservices. It enables applications to act on these events as they occur.

Implementing Event-Driven Architecture

There are various options available to implement event-driven architecture. Some of them are:

The most suitable one can be selected based on time to production, ease of adoption, message ordering, resilience, event replay, persistence, retries, at-least/at-most once delivery, cost, etc., of your use case.

We will discuss event-driven architecture at Harness using Redis Streams in this blog. Before diving into event-driven architecture, it is important to understand what Redis and Redis Streams are.

What Is Redis?

Redis is an open-source in-memory data structure store, used as a database, cache, and message broker.

Important features of Redis:

$ brew install redis $ brew services start redis $ redis-cli

Redis Streams

Redis Streams is an append-only data structure that helps in various streaming use cases like real-time messaging (RTM), message broker, etc. Before diving into Redis Streams, let us first go through one of the other constructs which Redis provides for inter-service communication: Redis Pub/Sub.

Pub/Sub

Redis Pub/Sub implements the Publish/Subscribe messaging paradigm. Let’s play around a bit with multiple terminal windows. Open four terminal tabs and enter redis-cli in all of them. We will PUBLISH to a channel from one window (Redis client) and SUBSCRIBE to that channel from the other three.

As you can see, the first client is publishing a message “hello world“ on channel1. The other three clients are endlessly subscribed to channel1 and receive the message sent via the publisher. A quick note here is that the subscribe command is a forever blocking operation and endlessly waits for a new message to appear on the channel.

Key things to note about Pub/Sub:

Streams

Redis Streams was introduced as a new data structure in Redis 5.0, which models an append-only log file-like construct. Note the key difference between Redis Streams and Apache Kafka here is, Streams is merely an append-only list data structure in Redis with advanced operations, while on the other hand, Kafka is an entire platform of various components. Let us discuss the operations that we can perform on a Stream:

  1. Add data to stream (returns a unique ID)

XADD * key1 value1 key2 value2

  1. Read data from stream

XREAD BLOCK COUNT STREAMS

  1. Consumer groups

  2. Create a group

XGROUP CREATE $

  1. Read as a consumer

XREADGROUP GROUP COUNT STREAMS >

  1. Acknowledge a message with a specific ID

XACK

  1. Range queries

XRANGE

  1. Truncate the stream

XTRIM MAXLEN

Pub/Sub With Streams

PUBLISH can be realized by XADD channel1 * message "hello world" and

SUBSCRIBE can be realized by XREAD BLOCK 5000 STREAMS channel1 $ and then keep passing the last fetched id in this loop.

Consumer Group Reading With Streams

  1. Push a message to a stream

XADD channel1 * message "hello world"

  1. Create a consumer group

XGROUP CREATE channel1 group1 0-0

  1. Read messages from the stream as a consumer of a group

XREADGROUP GROUP group1 consumer1 COUNT 5 STREAMS channel1 > # Read an unread message of group1

Other commands like XPENDING, XCLAIM, XTRIM, etc. are discussed in the Redis Stream Intro page.

Key things to note about Streams:

Event-Driven Architecture at Harness

We at Harness have recently moved to a microservices world for a coming release. Multiple services publish events, and the others selectively consume them by listening to a particular stream to drive end-user functionality.

Why Redis Streams Over Other Options?

Implementation

I will give you a sneak peek into how easy it is to get it up and running with just a Redis instance and a few lines of code:

Here's a gist with the above code.

Challenges

While implementing event-driven architecture at Harness using Redis Streams, these are some of the challenges that we faced:

Conclusion

We achieved an event-driven architecture by using Redis Streams and have deployed it to production for more than a few months now. Redis Streams is a more lightweight solution for implementing event-driven architecture, as compared to advanced solutions like Apache Kafka. You need to decide which would be the best implementation based on your use case and the features that you expect out of an event-driven architecture.