Spring WebSocket and Redis Pub/Sub

Deniz G
5 min readSep 2, 2021

--

Since the WebSocket is a low-level protocol, there is needed lots of extra codes to write applications. By using subprotocol, it is possible at high-level by writing less code. Client and server agree on a messaging protocol over the “Sec-WebSocket-Protocol” header during handshake.

Spring Framework supports the use of STOMP (Simple Text-Oriented Messaging Protocol) subprotocol. With STOMP, data flow is provided via the broker as publish-subscribe.

In-Memory STOMP Broker

  • Spring provides STOMP broker as in-memory and server side acts as broker for clients in this case.
https://docs.spring.io/spring-framework/docs/current/reference/html/web.html#websocket-stomp-message-flow

This usage is not suitable if the application is running in the cluster.

Why?

Let’s consider an shopping example. The customer orders an book, and the next page shows the order status as PENDING. Then, stock control is done in the background and accordingly, it will go to OUT OF STOCK or READY status.

When stock service publish a message and the order instance-2 consumes this, the status of this order will not be instantly notified to the user. Because the websocket connection is opened to order instance-1.

A stomp supported broker should be used to avoid these situations.

STOMP-supported Broker

When the server wants to send a message over the WebSocket, this message is first sent to the STOMP broker. This STOMP broker sends the incoming message to all instances connected to it.

That is, the message is multiplexed between all instances.

What about Redis instead of STOMP Broker?

First of all, Redis doesn’t support the STOMP protocol.

My motivation in this post is to share the answer of how we can use Redis instead of STOMP broker.

Spring WebSocket and Redis can be integrated within the Cluster using Redis Pub/Sub. In-memory broker will be used as STOMP broker, but messages will be publish to Redis Pub/Sub before being sent directly to WebSocket. Each instance will be a subscriber of Redis Pub/Sub.

That is, the message is multiplexed between all instances, again.

Redis Pub/Sub

One of the use cases of Redis is that it can also be used as a Messaging Queue(MQ). Redis can be used as a Message Broker with its Redis Pub/Sub feature.

What is done with the Pub/Sub pattern? We are familiar with the Apache Kafka. Producer and consumer communicate with each other by adding their messages to a Message Queue, without knowing each other’s existence or addresses. Thus, a decoupled system is designed.

This pub/sub pattern can be implemented using Redis Pub/Sub.

There is a channel in Redis (corresponding to the topic in Apache Kafka). Producer publishes his message here, and consumer also subscribes to this channel. As a message is published to the channel, Redis sends the messages to the subscribers listening to this channel.

This is done by Pub/Sub Push Model. When a Redis client subscribes to a Pub/Sub channel, the protocol changes semantics and becomes a push protocol, that is, the client no longer requires sending commands, because the server will automatically send to the client new messages (for the channels the client is subscribed to) as soon as they are received. So, subscribers don’t poll channel periodically. (https://redis.io/topics/protocol#request-response-model)

Here are key features of Redis Pub/Sub

  • Fire and Forget: How is normal behavior in Apache Kafka? After the producer publishes his message to the Kafka, it completes its communication. At that moment, even if the consumer is not healthy, it can consumes the message produced when it is healthy. That is, asynchronous communication is made with Kafka. But in Redis Pub/Sub, there is synchronous communication. That is, for healthy communication between pub and sub, both sides must be active at the same time. Because the structure here is fire & forget. The message is being published to the channel. If there is no active subscriber, the message disappears. and this message is not saved anywhere. Pub/Sub channel is not in a persistent structure. Therefore, if the consumer doesn’t exist at that time when the message is published to the channel, the consumer will not be aware of the old message when it stands up later. Because when the message is published, it should be consumed immediately.
  • No Consumer Group: That is, the message is sent to all subscribers regardless of consumer instance. There is no such thing as a message being sent to only one of the subscribers.

So?

Normally, when we want to broadcast message to WebSocket, we use SimpMessagingTemplate provided by Spring as below.

private final SimpMessagingTemplate template;

public void doSomething(String message) {
//...
template.convertAndSend(topic, message);
}

Instead, we will use our own class which will publish messages to Redis Pub/Sub.

private final CustomWebSocketService service;

public void doSomething(String message) {
//...
service
.convertAndSend(topic, message);
}

Here is the our own service that publish message to Redis Channel using StringRedisTemplate.

@RequiredArgsConstructor
public class CustomWebSocketService {

private final StringRedisTemplate redisTemplate;
private final ObjectMapper mapper;

public void convertAndSend(String topic, Object message) {
var socketData = new WebSocketMessage(topic, message);
String data = mapper.writeValueAsString(socketData);
redisTemplate.convertAndSend("<channel-name>", data);
}

}

Here is the class to receive messages published in Redis Channel.

@RequiredArgsConstructor
public class RedisPubsubReceiver {

private final SimpMessagingTemplate template;
private final ObjectMapper mapper;

public void receiveMessage(String message) {
var data = mapper.readValue(message,WebSocketMessage.class);
template.convertAndSend(data.getTopic(), data.getMessage());
}

}

Finally, we register our class to which channels it will subscribe to messages.

@Configuration
@RequiredArgsConstructor
public class RedisPubsubConfiguration {

private final SimpMessagingTemplate template;
private final ObjectMapper mapper;
private final RedisConnectionFactory connectionFactory;

@Bean
public RedisPubsubReceiver redisPubsubReceiver() {
return new RedisPubsubReceiver(template, mapper);
}

@Bean
public MessageListenerAdapter redisPubsubListenerAdapter() {
return new MessageListenerAdapter(redisPubsubReceiver(), "receiveMessage");
}

@Bean
public RedisMessageListenerContainer redisPubsubContainer() {
RedisMessageListenerContainer container = new RedisMessageListenerContainer();
container.setConnectionFactory(connectionFactory);
container.addMessageListener(redisPubsubListenerAdapter(), new PatternTopic("<channel-name>"));
return container;
}

}

As a result, using In-memory Message Broker and Redis Pub/Sub, the websocket usage in the cluster is as follows and it works as expected.

--

--