Sharing WebSocket Sessions Across Distributed Servers with Redis Pub/Sub in Spring
This article explains how to solve the problem of non‑serializable WebSocket sessions in a load‑balanced Spring application by using Redis publish/subscribe and spring‑session‑redis to share session data across multiple server instances, providing complete configuration and code examples.
In a load‑balanced project, WebSocket sessions are created on the server that receives the request, causing session loss when subsequent requests are routed to different machines because the standard HttpSession is not serializable.
Tomcat uses org.apache.catalina.session.StandardManager to persist sessions to the file system and org.apache.catalina.session.PersistentManager to store sessions via a custom org.apache.catalina.Store implementation.
To enable session sharing in a distributed environment, spring-session-redis serializes HttpSession objects into Redis, using a filter and decorator pattern.
Solution
Use a message middleware (Redis) to share WebSocket session data.
Employ Redis publish/subscribe to broadcast messages.
Method 2 – Redis Pub/Sub
Send a message to a specific channel using StringRedisTemplate.convertAndSend :
this.execute((connection) -> {
connection.publish(rawChannel, rawMessage);
return null;
}, true);Redis command used: publish channel message
Add a listener container and adapter:
@Bean
RedisMessageListenerContainer container(RedisConnectionFactory connectionFactory, MessageListenerAdapter listenerAdapter) {
RedisMessageListenerContainer container = new RedisMessageListenerContainer();
container.setConnectionFactory(connectionFactory);
// can add multiple listeners for different topics
container.addMessageListener(listenerAdapter, new PatternTopic(Constants.REDIS_CHANNEL));
return container;
}
@Bean
MessageListenerAdapter listenerAdapter(RedisReceiver receiver) {
// message listener adapter
return new MessageListenerAdapter(receiver, "onMessage");
}Message receiver implementation:
@Component
public class RedisReceiver implements MessageListener {
Logger log = LoggerFactory.getLogger(this.getClass());
@Autowired
private WebSocketServer webSocketServer;
@Override
public void onMessage(Message message, byte[] pattern) {
String channel = new String(message.getChannel());
String msg = "";
try {
msg = new String(message.getBody(), Constants.UTF8);
if (!StringUtils.isEmpty(msg)) {
if (Constants.REDIS_CHANNEL.endsWith(channel)) {
JSONObject jsonObject = JSON.parseObject(msg);
webSocketServer.sendMessageByWayBillId(
Long.parseLong(jsonObject.get(Constants.REDIS_MESSAGE_KEY).toString()),
jsonObject.get(Constants.REDIS_MESSAGE_VALUE).toString());
} else {
// TODO other message handling
}
} else {
log.info("Message content is empty, ignore.");
}
} catch (Exception e) {
log.error("Message processing error:" + e.toString());
e.printStackTrace();
}
}
}WebSocket configuration class:
@Configuration
@EnableWebSocket
public class WebSocketConfiguration {
@Bean
public ServerEndpointExporter serverEndpointExporter() {
return new ServerEndpointExporter();
}
}WebSocket server endpoint implementation:
@ServerEndpoint("/websocket/{id}")
@Component
public class WebSocketServer {
private static final long sessionTimeout = 600000;
private static final Logger log = LoggerFactory.getLogger(WebSocketServer.class);
private static AtomicInteger onlineCount = new AtomicInteger(0);
private static ConcurrentHashMap
webSocketMap = new ConcurrentHashMap<>();
private Session session;
private Long id;
@Autowired
private StringRedisTemplate template;
@OnOpen
public void onOpen(Session session, @PathParam("id") Long id) {
session.setMaxIdleTimeout(sessionTimeout);
this.session = session;
this.id = id;
if (webSocketMap.containsKey(id)) {
webSocketMap.remove(id);
}
webSocketMap.put(id, this);
addOnlineCount();
log.info("id:" + id + " connected, online count:" + getOnlineCount());
try { sendMessage("Connection successful!"); } catch (IOException e) { log.error("id:" + id + ", network error"); }
}
@OnClose
public void onClose() {
if (webSocketMap.containsKey(id)) {
webSocketMap.remove(id);
subOnlineCount();
}
log.info("id:" + id + " disconnected, online count:" + getOnlineCount());
}
@OnMessage
public void onMessage(String message, Session session) {
log.info("id:" + id + ", received:" + message);
}
@OnError
public void onError(Session session, Throwable error) {
log.error("id error:" + this.id + ", cause:" + error.getMessage());
error.printStackTrace();
}
public void sendMessage(@NotNull String key, String message) {
Map
map = new HashMap<>();
map.put(Constants.REDIS_MESSAGE_KEY, key);
map.put(Constants.REDIS_MESSAGE_VALUE, message);
template.convertAndSend(Constants.REDIS_CHANNEL, JSON.toJSONString(map));
}
public void sendMessageByWayBillId(@NotNull Long key, String message) {
WebSocketServer ws = webSocketMap.get(key);
if (ws != null) {
try { ws.sendMessage(message); log.info("id:" + key + " sent message:" + message); }
catch (IOException e) { log.error("id:" + key + " send failed"); }
} else {
log.error("id:" + key + " not connected");
}
}
public void sendMessage(String message) throws IOException {
this.session.getBasicRemote().sendText(message);
}
public static synchronized AtomicInteger getOnlineCount() { return onlineCount; }
public static synchronized void addOnlineCount() { onlineCount.getAndIncrement(); }
public static synchronized void subOnlineCount() { onlineCount.getAndDecrement(); }
}Project structure screenshots and deployment instructions show how to start three instances on different ports (8080, 8081, 8082) and use Postman to send a request to http://localhost:8080/socket/456 . The message is then received by the WebSocket services on ports 8081 and 8082 via Redis subscription.
Finally, the article provides a Gitee repository link for the complete source code and mentions additional resources such as interview questions and related articles.
Top Architect
Top Architect focuses on sharing practical architecture knowledge, covering enterprise, system, website, large‑scale distributed, and high‑availability architectures, plus architecture adjustments using internet technologies. We welcome idea‑driven, sharing‑oriented architects to exchange and learn together.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.