This article provides a detailed guide on implementing a scalable WebSocket server using Spring Boot, Redis Pub/Sub, and Redis Streams.
Abstract
The article begins by discussing the design considerations for scaling WebSocket servers horizontally using a publish-subscribe pattern. It then proceeds to provide a step-by-step guide on implementing a scalable WebSocket server using Spring Boot, Redis Pub/Sub, and Redis Streams. The guide covers building a WebSocket server, starting the Redis server, configuring the connection to the Redis server, implementing Pub/Sub for unidirectional real-time communication, and implementing Pub/Sub with consumer groups for bidirectional real-time communication. The article also includes code snippets and diagrams to illustrate the concepts discussed.
Opinions
The author believes that using Redis Pub/Sub and Redis Streams is an effective way to scale WebSocket servers horizontally.
The author emphasizes the importance of using consumer groups to avoid duplicated message processing.
The author suggests using a custom object for data exchange between subscribers and publishers.
The author recommends using a scheduled job to periodically publish data to Redis streams.
The author provides a sample code on GitHub for readers to follow along.
The author encourages readers to test the implementation using the WebSocket debugger tool.
The author concludes by expressing hope that readers learned something new from the article and encourages them to stay tuned for the next one.
Implement a Scalable WebSocket Server With Spring Boot, Redis Pub/Sub, and Redis Streams
Scaling WebSocket server horizontally using Spring Boot, Redis Pub/Sub, and Redis Streams
03: Implement a Scalable WebSocket server with Spring Boot, Redis Pub/Sub, and Redis Streams
04: TBA
Quick Recap
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.
Let’s Get Started
Follow along to build a scalable WebSocket server using Spring Boot, Stomp, Redis Pub/Sub, and Redis Streams.
Step 4: Implement Pub/Sub (Broadcast Channel) for Unidirectional Real-Time Communication
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.
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-CHANNEL, the 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 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 forwards the message to all web applications (frontend) via the established WebSocket connection.
Note: There isn’t a need to broadcast the message as all WebSocket server instances will receive the message from the Redis Streams.
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.
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 the Redis CLI command instead.
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 two instances of 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 (instance A)Output Logs for Backend Microservice (instance B)Output Logs for WebSocket Server (instance A)Output Logs for WebSocket (instance B)
If you are to connect to the WebSocket server using the WebSocket debugger tool and subscribe to the topic /topic/to-frontend, you 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).
Summary
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.
Thank you for reading until the end. I hope you learned something new from this article. Stay tuned for the next one, and happy learning!