Implement a Scalable WebSocket Server With Spring Boot, Redis Pub/Sub, and Redis Streams | by KBryan | Jun, 2022

Scaling WebSocket server horizontally using Spring Boot, Redis Pub/Sub, and Redis Streams

Photo by Andreas Wagner on Unsplash

This is a follow-up to my previous article on the design considerations for scaling the WebSocket server horizontally.

In this article, I will go into detail on how we can implement that using Redis Pub/Sub and Redis Streams.

Full design for scaling WebSocket servers in a microservice architecture using publish-subscribe pattern

In the last article, we identified two issues that will occur when horizontally scaling the WebSocket server and backend microservices:

  • Issue #1: Message Loss due to Load Balancer
  • Issue #2: Duplicated Message Processing due to Multiple Subscribers

The solutions were to apply publish-subscribe messaging patterns with consumer groups’ concepts to the architecture design. For more information, refer to the previous article.

Follow along to build a scalable WebSocket server using Spring Boot, Stomp, Redis Pub/Sub, and Redis Streams.

Step 1: Build a WebSocket server

Follow Steps 1 and 2 of my previous article to initialize a WebSocket server using Spring Boot and STOMP messaging protocol.

Step 2: Start Redis server

For a quick setup, run the Redis server locally using docker.

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

Step 3: Configure connection to Redis Server

Add the following configuration to the application.yml file of the WebSocket server to connect to the Redis server.

# application.yml
spring.redis:
host: localhost
port: 6379
Design for Unidirectional Real-Time Communication using APIs and Pub/Sub (Broadcast)

In step 4, we will create APIs for unidirectional real-time communication between backend microservices and web applications (frontend). The WebSocket server receives messages from backend microservices via APIs and broadcasts the messages to all WebSocket server instances using Redis Pub/Sub. The messages are then forwarded to the web applications via the established WebSocket connections.

Step 4.1: Create BroadcastEvent class

BroadcastEvent is a custom object for broadcasting the message from one instance of the WebSocket server to all instances of the WebSocket server.

Step 4.2: Configure Redis Pub/Sub — ReactiveRedisTemplate

ReactiveRedisTemplate is a helper class that simplifies Redis data access code. In our configuration, we are publishing/subscribing the value BroadcastEvent and using Jackson2JsonRedisSerializer to perform automatic serialization/deserialization of the value.

Step 4.3: Configure Redis Pub/Sub — Broadcast Service

RedisBroadcastService contains logic for publishing and subscribing to a custom channel ( BROADCAST-CHANNEL ). This is the channel for broadcasting messages from one instance of the WebSocket server to all instances of the WebSocket server.

Whenever the WebSocket servers receive a message from the BROADCAST-CHANNELthe message is forwarded to the web applications (frontend) that have established a WebSocket connection with it.

Note: @PostConstruct is a Spring annotation that allows us to attach custom actions to bean creation and the methods are only run once. In our case, we are subscribing to the BROADCAST-CHANNEL on bean creation.

Step 4.4: Creating APIs endpoints

The code below creates a REST controller with a POST request endpoint that takes in a request body NewMessageRequest. The topic is the STOMP destination that the client (frontend) subscribes to and the message is the actual message in String format.

The API requests will be broadcasted to all instances of the WebSocket servers as configured in Step 4.3 above.

Step 4.5: Testing Unidirectional real-Time communication via APIs

Spin up the WebSocket server, and connect to the WebSocket server ws://localhost:8080/stomp over STOMP protocol using the WebSocket debugger tool developed by jiangxy. Once connected, configure the WebSocket debugger tool to subscribe to the topic /topic/frontend.

Next, send an HTTP POST request to the WebSocket server using the curl command below:

curl -X POST -d '{"topic": "/topic/frontend", "message": "testing API endpoint" }' -H 'Content-Type: application/json' localhost:8080/api/notification

The WebSocket debugger tool should have the output shown below:

Screenshot of the output for WebSocket Debugger Tool

This shows that we have successfully configured the WebSocket server with Redis Pub/Sub for scalable unidirectional real-time communication between backend microservices and web applications (frontend).

Step 5: Implement Pub/Sub with Consumer Groups for Bi-direction Real-Time Communication

Design for Bi-directional Real-Time Communication using Pub/Sub and Consumer Groups

In step 5, we will use Redis Streams as our Pub/Sub System for bidirectional real-time communication between backend microservices and web applications (frontend). We are not using Redis Pub/Sub as it does not support the consumer groups concept.

Step 5.1: Create a StreamDataEvent class

StreamDataEvent is a custom object for data exchange between subscribers and publishers. The message is the actual message in String format and the topic is a required field for the WebSocket server to know which STOMP destination to send the message to.

Step 5.2: WebSocket server — Implement Redis stream consumer

The consumer consumes the message from Redis streams and broadcasts the message to all WebSocket server instances using the RedisBroadcastService that we configured in step 4.

Step 5.3: WebSocket server — Implement Redis stream config

The following code contains configurations for subscribing to Redis streams where the messages will be processed by the RedisStreamConsumer which we configured in Step 5.2.

Here, we are configuring the WebSocket server to listen to the stream identified by the key TEST_EVENT_TO_WEBSOCKET_SERVER. You can create more subscriptions depending on your use cases.

Step 5.4: WebSocket server — Implement Redis stream producer

The producer provides a method publishEvent for publishing data to the Redis streams. In our example, there is a scheduled job that is publishing periodically (every five seconds, ten seconds after the WebSocket server starts) to Redis streams using the key TEST_EVENT_TO_BACKEND.

Step 5.5 WebSocket Server — Implement WebSocket Configuration

Create a Controller that processes the messages from the web application (frontend) which are sent to the WebSocket server with the prefix /app. In the example below, messages sent to /app/test will be forwarded (published) to the Redis streams at key TEST_EVENT_TO_BACKEND.

Note: There isn’t a need to broadcast the message to all WebSocket instances as publishing to Redis Streams already ensures all backend microservices receive the message. Refer to the diagram in Step 5 for more details.

Step 5.6: Backend microservice— Implement Redis stream consumer

Similarly, in the sample backend microservice, implements the Redis stream consumer.

Step 5.6: Backend microservice — Implement Redis stream config

The configuration here is similar to the WebSocket server’s configuration. The only difference is that we added the consumer group ( CONSUMER_GROUP ) which ensures that only one instance of the backend microservice will consume the data from Redis streams.

In order for the configuration to work, we will have to manually create the consumer group for the stream TEST_EVENT_TO_BACKEND in Redis first using the command below.

Note: It is possible to implement this using codes as well, but I will keep it simple by using Redis CLI command instead.

docker exec redis redis-cli XGROUP CREATE TEST_EVENT_TO_BACKEND CONSUMER_GROUP $ MKSTREAM

Step 5.7: Backend microservice — Implement Redis stream producer

The producer configuration is similar to the WebSocket server configuration.

Note that the microservice has a scheduled job that periodically publishes to the Redis streams and the message is crafted to be sent to the web application (frontend) at the destination topic /topic/to-frontend as part of our example.

Step 5.8: Testing bidirectional real-time communication via Pub/Sub

We have configured both the WebSocket server and the sample backend microservice. Let’s test the publishing and subscribing of data from Redis streams using the scheduled data publishing configuration we made in both RedisStreamProducer.

Spin up the WebSocket server and two instances of the sample backend microservices. You should notice that the output logs are similar to the ones below.

Output Logs for Backend Microservice A
Output Logs for Backend Microservice B
Output Logs for WebSocket Server

If you are to connect to the WebSocket server using the WebSocket debugger tool and subscribe to the topic /topic/to-frontendyou should see the following logs:

Output Logs for WebSocket Debugger Tool (Frontend)

This shows that we have successfully configured the WebSocket server with Redis Streams for scalable bidirectional real-time communication between backend microservices and web applications (frontend).

That’s it! You can find the sample code here on GitHub. My implementation is not perfect but the purpose is to give you an idea of ​​how you can scale WebSocket servers in a microservice architecture easily with publish-subscribe messaging patterns.

Leave a Comment