Backend Development 10 min read

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.

Top Architect
Top Architect
Top Architect
Sharing WebSocket Sessions Across Distributed Servers with Redis Pub/Sub in Spring

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.

JavaRedisSpringWebSocketdistributedsession sharing
Top Architect
Written by

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.

0 followers
Reader feedback

How this landed with the community

login 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.