How to Share WebSocket Sessions Across Load‑Balanced Servers with Redis Pub/Sub
Learn how to resolve WebSocket session sharing issues in load‑balanced Java applications by leveraging Redis’s publish/subscribe mechanism and Spring Session, with step‑by‑step configuration, code examples, and deployment instructions for multiple distributed environments.
In a load‑balanced deployment, a WebSocket connection is tied to the server that first handles the request, so the session stored on that server cannot be found when subsequent requests are routed to another instance.
WebSocket sessions are not serializable, so they cannot be stored in Redis.
HttpSession serialization in Spring is achieved via Tomcat's StandardManager and PersistentManager.
org.apache.catalina.session.StandardManager
org.apache.catalina.session.PersistentManager StandardManageris Tomcat's default manager; when the web application stops, it persists all in‑memory HttpSession objects to a file under
<tomcat_home>/work/Catalina/<host>/<app>/sessions.ser. PersistentManager is more flexible: if a device provides a driver implementing org.apache.catalina.Store, the manager can store sessions to that device.
Spring‑Session‑Redis solves the distributed session problem by serializing sessions into Redis and using a filter‑decorator pattern to share HttpSession across instances.
Solution
Use a messaging middleware to share WebSocket sessions.
Use Redis publish/subscribe mode.
Method two
Send a message to a specific channel with StringRedisTemplate.convertAndSend:
this.execute((connection) -> {
connection.publish(rawChannel, rawMessage);
return null;
}, true);Redis command: publish channel message Add a listener container and a listener 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");
}Add a message receiver:
/**
* Message listener object, receives subscribed messages
*/
@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()); // subscribed channel name
String msg = "";
try {
msg = new String(message.getBody(), Constants.UTF8); // keep encoding consistent
if (!StringUtils.isEmpty(msg)) {
if (Constants.REDIS_CHANNEL.endsWith(channel)) { // latest message
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 exception:" + e.toString());
e.printStackTrace();
}
}
}WebSocket configuration class:
@Configuration
@EnableWebSocket
public class WebSocketConfiguration {
@Bean
public ServerEndpointExporter serverEndpointExporter() {
return new ServerEndpointExporter();
}
}WebSocket server component:
@ServerEndpoint("/websocket/{id}")
@Component
public class WebSocketServer {
private static final long sessionTimeout = 600000;
private static final Logger log = LoggerFactory.getLogger(WebSocketServer.class);
private static final AtomicInteger onlineCount = new AtomicInteger(0);
private static final ConcurrentHashMap<Long, WebSocketServer> 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 + ", message:" + message);
}
@OnError
public void onError(Session session, Throwable error) {
log.error("id:" + this.id + ", error:" + error.getMessage());
error.printStackTrace();
}
/** Publish a message via Redis */
public void sendMessage(@NotNull String key, String message) {
Map<String, String> 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));
}
/** Send a message to a specific client */
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 (illustrated in the image below):
Start three services on different ports.
Use the demo site http://www.easyswoole.com/wstool.html for testing.
Open the following URLs in two browser tabs:
ws://127.0.0.1:8081/websocket/456
ws://127.0.0.1:8082/websocket/456
Send a request to http://localhost:8080/socket/456 via Postman; the message will be received by both 8081 and 8082 services.
From the 8082 service, send a JSON message like {"KEY":456,"VALUE":"aaaa"}; all other services will also receive it.
Thus, using Redis publish/subscribe resolves the distributed WebSocket session sharing problem.
Gitee repository: https://gitee.com/jack_whh/dcs-websocket-sessio
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Programmer DD
A tinkering programmer and author of "Spring Cloud Microservices in Action"
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.
