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.

Programmer DD
Programmer DD
Programmer DD
How to Share WebSocket Sessions Across Load‑Balanced Servers with Redis Pub/Sub

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
StandardManager

is 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
Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

Distributed SystemsJavaredisspringWebSocketsession sharing
Programmer DD
Written by

Programmer DD

A tinkering programmer and author of "Spring Cloud Microservices in Action"

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.