spring cloud 同一服务多实例 websocket跨实例无法共享Session 的解决

思路>>使用redis发布消息,通知其他实例,查询符合条件的Session用于发送消息

复制代码
package com.ruoyi.common.redis.message;

import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.stereotype.Component;

/**
 * ApplicationContext 本身就实现了 ApplicationEventPublisher 接口
 */
@Component
public class ApplicationContextProvider implements ApplicationContextAware {
    private static ApplicationContext context;

    public static ApplicationContext getApplicationContext() {
        return context;
    }

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) {
        context = applicationContext;
    }

    public static ApplicationEventPublisher getEventPublisher() {
        return context;
    }
}

package com.ruoyi.common.redis.message;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.listener.PatternTopic;
import org.springframework.data.redis.listener.RedisMessageListenerContainer;
import org.springframework.data.redis.listener.adapter.MessageListenerAdapter;

@Configuration
public class RedisMessageConfig {
    /**
     * 创建消息监听器容器
     * @param connectionFactory Redis连接工厂
     * @param listenerAdapter 消息监听器适配器
     * @return RedisMessageListenerContainer
     */
    @Bean
    public RedisMessageListenerContainer container(RedisConnectionFactory connectionFactory,
                                            MessageListenerAdapter listenerAdapter) {
        RedisMessageListenerContainer container = new RedisMessageListenerContainer();
        container.setConnectionFactory(connectionFactory);
        // 将监听器适配器添加到容器,并订阅指定频道
        container.addMessageListener(listenerAdapter, new PatternTopic("*"));
        // 你也可以订阅多个频道,或者使用PatternTopic进行模式匹配订阅
        // container.addMessageListener(listenerAdapter, new PatternTopic("news.*"));
        return container;
    }

    /**
     * 将我们实现了MessageListener接口的订阅者包装成MessageListenerAdapter
     * Spring会默认寻找名为"onMessage"的方法
     * @param subscriber 我们的订阅者实例
     * @return MessageListenerAdapter
     */
    @Bean
    public MessageListenerAdapter listenerAdapter() {
        return new MessageListenerAdapter(new  RedisMessageSubscriber(),"onMessage");
    }
}

package com.ruoyi.common.redis.message;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;

@Component
public class RedisMessagePublisher {
    @Autowired
    private RedisTemplate redisTemplate;

    /**
     * 发布消息
     * @param channel
     * @param message
     */
    public void publish(String channel, Object message) {
        redisTemplate.convertAndSend(channel, message);
    }
}

package com.ruoyi.common.redis.message;

import com.ruoyi.common.core.utils.SpringUtils;
import com.ruoyi.common.core.utils.StringUtils;
import com.ruoyi.common.redis.message.event.Commemorate3dCemeteryEvent;
import com.ruoyi.common.redis.message.event.Commemorate3dHallEvent;
import com.ruoyi.common.redis.message.event.KinMemberChangeEvent;
import com.ruoyi.common.redis.message.event.MemorialHallEvent;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.data.redis.connection.Message;
import org.springframework.data.redis.connection.MessageListener;
import org.springframework.stereotype.Component;

import java.nio.charset.StandardCharsets;
@Slf4j
@Component
public class RedisMessageSubscriber implements  MessageListener {

    @Override
    public void onMessage(Message message, byte[] pattern) {
        ApplicationEventPublisher eventPublisher = ApplicationContextProvider.getEventPublisher();


        String actualChannel = new String(message.getChannel(), StandardCharsets.UTF_8);

        System.out.println("redisMessage频道: " + actualChannel);
        System.out.println("redisMessage消息: " + message);
        //System.out.println("消息类型: " + message.getClass().getSimpleName());

        //举例 module:business:key websocket:kinMemberChange:kinMemberChange-userid
        String[] split = StringUtils.split(actualChannel, ':');
        if (split.length != 3) {
            log.error("redis onMessage channel error");
        }
        if(StringUtils.equals(split[0], "websocket")){
            //同一模块部署多实例的时候,任何一个模块发送websocket消息,都要通知其他实例,在内存中找session对象,进行发送消息
            if(StringUtils.equals(split[1], "kinMemberChange")){
                //其他实例,kinMemberChange 业务,发布了新消息
                String[] split1 = StringUtils.split(split[2], "_");
                eventPublisher.publishEvent(new KinMemberChangeEvent(this,split1[0],split1[1],""));
            }else if(StringUtils.equals(split[1], "commemorate3dCemetery")){
                String[] split1 = StringUtils.split(split[2], "_");

                byte[] bodyBytes = message.getBody();
                String messageBody = new String(bodyBytes);
                messageBody = removeExtraQuotes(messageBody);
                messageBody = StringUtils.replace(messageBody,"\\r\\n","");
                messageBody = StringUtils.replace(messageBody,"\\\"","\"");
                eventPublisher.publishEvent(new Commemorate3dCemeteryEvent(this,split1[0],split1[1],messageBody));
            }else if(StringUtils.equals(split[1], "commemorate3dHall")){
                String[] split1 = StringUtils.split(split[2], "_");

                byte[] bodyBytes = message.getBody();
                String messageBody = new String(bodyBytes);
                messageBody = removeExtraQuotes(messageBody);
                messageBody = StringUtils.replace(messageBody,"\\r\\n","");
                messageBody = StringUtils.replace(messageBody,"\\\"","\"");
                eventPublisher.publishEvent(new Commemorate3dHallEvent(this,split1[0],split1[1],messageBody));
            }else if(StringUtils.equals(split[1], "memorialHall")){
                String[] split1 = StringUtils.split(split[2], "_");

                byte[] bodyBytes = message.getBody();
                String messageBody = new String(bodyBytes);
                messageBody = removeExtraQuotes(messageBody);
                messageBody = StringUtils.replace(messageBody,"\\r\\n","");
                messageBody = StringUtils.replace(messageBody,"\\\"","\"");
                eventPublisher.publishEvent(new MemorialHallEvent(this,split1[0],split1[1],messageBody));
            }
        }
    }


    /**
     * 去除字符串首尾的多余引号
     */
    private String removeExtraQuotes(String str) {
        if (str == null || str.isEmpty()) {
            return str;
        }

        // 去除首尾的双引号
        if (str.startsWith("\"") && str.endsWith("\"")) {
            return str.substring(1, str.length() - 1);
        }

        // 去除首尾的单引号
        if (str.startsWith("'") && str.endsWith("'")) {
            return str.substring(1, str.length() - 1);
        }

        return str;
    }
}

在websocket收到消息时,发布redis消息,通知所有实例

复制代码
package com.ruoyi.foundation.apicontroller;

import com.google.gson.Gson;
import com.ruoyi.common.redis.message.RedisMessagePublisher;
import com.ruoyi.foundation.apicontroller.req.MemorialHallWebsocketReq;
import com.ruoyi.foundation.event.dto.AnimationBroadcastDto;
import com.ruoyi.foundation.webSocket.WebsocketUtil;
import io.seata.common.util.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.stereotype.Service;

import javax.websocket.*;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;

@Service
@ServerEndpoint(value = "/api/mingmenCommemorate3dHallWebsocket/{commemorate3d}/{userId}")
public class MmCommemorate3dHallWebsocketController {
    /**
     * 保存每日一念纪念馆中当前在线的用户ID
     */
    public static final Map<String, List<String>> memorialHallUsers = new ConcurrentHashMap<>();

    private Gson gson=new Gson();
    private static ApplicationEventPublisher eventPublisher;

    private static RedisMessagePublisher redisMessagePublisher;

    @Autowired
    public void setEventPublisher(ApplicationEventPublisher publisher) {
        MmCommemorate3dHallWebsocketController.eventPublisher = publisher;
    }

    @Autowired
    public void setRedisMessagePublisher(RedisMessagePublisher publisher) {
        MmCommemorate3dHallWebsocketController.redisMessagePublisher = publisher;
    }

    /*public MmMemorialHallWebsocketController() {
        // 从 Spring 容器中获取 ApplicationEventPublisher
        this.eventPublisher = SpringContextUtil.getBean(ApplicationEventPublisher.class);
    }*/

    @OnOpen
    public void onOpen(@PathParam(value = "commemorate3d") String commemorate3d,@PathParam(value = "userId") String userId, Session session) {
        WebsocketUtil.addSession("commemorate3d:"+userId, session);

        List<String> strings = memorialHallUsers.get(commemorate3d);
        if (strings == null){
            List<String> list=new ArrayList<>();
            list.add("commemorate3d:"+userId);
            memorialHallUsers.put(commemorate3d,list);
        }else{
            strings.add("commemorate3d:"+userId);
        }
    }

    @OnClose
    public void onClose(@PathParam(value = "commemorate3d") String commemorate3d,@PathParam(value = "userId") String userId, Session session) {
        WebsocketUtil.removeSession("commemorate3d:"+userId);

        List<String> strings = memorialHallUsers.get(commemorate3d);
        if(strings != null){
            strings.remove("commemorate3d:"+userId);
        }
    }

    @OnMessage
    public void onMessage(@PathParam(value = "commemorate3d") String commemorate3d,@PathParam(value = "userId") String userId, Session session, String message) {
        /*System.out.println(dailyMissId);
        System.out.println(userId);
        System.out.println(session);
        System.out.println(message);*/

        //发送redis消息,使消息扩散到每个spring boot 实例
        redisMessagePublisher.publish("websocket:commemorate3dHall:"+commemorate3d + "_" + userId,message);

        /*MemorialHallWebsocketReq memorialHallWebsocketReq = gson.fromJson(message, MemorialHallWebsocketReq.class);

        List<String> strings = memorialHallUsers.get(commemorate3d);
        if(strings == null || strings.isEmpty()){
            return;
        }

        Set<String> collect = strings.stream().filter(userId1 -> !StringUtils.equals(userId1, "commemorate3d:"+userId)).collect(Collectors.toSet());

        //对在线的用户广播前,记录动效已经播放
        if(StringUtils.isNotBlank(memorialHallWebsocketReq.getMingmenSacrificeRecordId()) && collect.size() > 0){
            List<Long> collect1 = collect.stream().map(u -> Long.valueOf(u.split(":")[1])).collect(Collectors.toList());
            eventPublisher.publishEvent(new AnimationBroadcastDto(this,Long.valueOf(memorialHallWebsocketReq.getMingmenSacrificeRecordId()),collect1));
        }


        //对同纪念馆的在线用户进行广播
        WebsocketUtil.sendMessageForUsers(collect,message);*/
    }

    @OnError
    public void onError(Session session, Throwable throwable) {
        try {
            session.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
        throwable.printStackTrace();
    }

    /**
     * 消息来自redis,通过redis可使同一模块所有实例都收到消息
     * @param commemorate3d
     * @param userId
     * @param message
     */
    public void onMessageFromRedis(String commemorate3d,String userId,String message){
        MemorialHallWebsocketReq memorialHallWebsocketReq = gson.fromJson(message, MemorialHallWebsocketReq.class);

        List<String> strings = memorialHallUsers.get(commemorate3d);
        if(strings == null || strings.isEmpty()){
            return;
        }

        Set<String> collect = strings.stream().filter(userId1 -> !StringUtils.equals(userId1, "commemorate3d:"+userId)).collect(Collectors.toSet());

        //对在线的用户广播前,记录动效已经播放
        if(StringUtils.isNotBlank(memorialHallWebsocketReq.getMingmenSacrificeRecordId()) && collect.size() > 0){
            List<Long> collect1 = collect.stream().map(u -> Long.valueOf(u.split(":")[1])).collect(Collectors.toList());
            eventPublisher.publishEvent(new AnimationBroadcastDto(this,Long.valueOf(memorialHallWebsocketReq.getMingmenSacrificeRecordId()),collect1));
        }


        //对同纪念馆的在线用户进行广播
        WebsocketUtil.sendMessageForUsers(collect,message);
    }
}

实例收到redis消息后,转化为ApplicationEventPublisher 消息,从基础层调用业务层

复制代码
package com.ruoyi.foundation.redisevents;

import com.ruoyi.common.redis.message.event.Commemorate3dHallEvent;
import com.ruoyi.foundation.apicontroller.MmCommemorate3dHallWebsocketController;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.event.EventListener;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;

@Component
@Slf4j
public class Commemorate3dHallListen {
    @Async
    @EventListener
    public void commemorate3dCemetery(Commemorate3dHallEvent event) {
        MmCommemorate3dHallWebsocketController mmCommemorate3dHallWebsocketController = new MmCommemorate3dHallWebsocketController();
        mmCommemorate3dHallWebsocketController.onMessageFromRedis(event.getCommemorate3d(),event.getUserId(),event.getMessage());
    }
}
相关推荐
草莓熊Lotso3 小时前
《算法闯关指南:优选算法--滑动窗口》--14找到字符串中所有字母异位词
java·linux·开发语言·c++·算法·java-ee
青云交3 小时前
Java 大视界 -- 基于 Java 的大数据实时流处理在金融高频交易数据分析中的应用
java·大数据·量化交易·异常检测·apache flink·实时流处理·金融高频交易
hhhhhshiyishi3 小时前
WLB公司内推|招联金融2026届校招|18薪
java·算法·机器学习·金融·求职招聘
韩立学长3 小时前
【开题答辩实录分享】以《城市网约车服务预约与管理小程序的设计与实现》为例进行答辩实录分享
java·小程序·选题
yics.3 小时前
多线程——单例模式
java·单例模式·多线程·线程安全
别惹CC3 小时前
Spring AI 进阶之路03:集成RAG构建高效知识库
java·人工智能·spring
舒一笑4 小时前
PandaCoder 1.1.8 发布:中文开发者的智能编码助手全面升级
java·后端·intellij idea
这周也會开心4 小时前
Spring-MVC
java·spring·mvc
SimonKing4 小时前
跨域,总在发OPTIONS请求?这次终于搞懂CORS预检了
java·后端·程序员