结合redis订阅和发布消息,解决websocket多节点问题

                         结合redis订阅和发布消息,解决websocket多节点问题

(最近发现项目多节点下redis消息会丢失,建议改成用mq广播模式推送,保证数据)

单节点和多节点下,websocket会出现什么问题呢?看如下两个对比图:

这时候你会发现有部分连接在ws node2节点上用户收不到消息推送。而且websocket的session是无法共享的,加上session是有序无法存入到redis缓存中。

了解大概内容后,直接进入到代码模块

1.相关依赖

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-websocket</artifactId>
        </dependency>

        <dependency>
            <groupId>org.apache.tomcat.embed</groupId>
            <artifactId>tomcat-embed-websocket</artifactId>
            <version>8.5.23</version>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>

        <dependency>
            <groupId>redis.clients</groupId>
            <artifactId>jedis</artifactId>
            <version>2.9.0</version>
        </dependency>

2.在RedisConfig加入消息监听容器

@Configuration
public class RedisConfig{

    /**
     * 使CacheComponent的redisTemplate组件的key使用StringRedisSerializer而非默认的JdkSerializationRedisSerializer
     * 避免key出现字节码的情况
     * @param factory redis链接
     * @return RedisTemplate
     */
    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
        RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
        redisTemplate.setConnectionFactory(factory);
        RedisSerializer redisSerializer = new StringRedisSerializer();
        Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
        //key使用StringRedisSerializer序列化
        redisTemplate.setKeySerializer(redisSerializer);
        //value使用jackson2JsonRedisSerializer序列化
        redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);
        return redisTemplate;
    }

    /**
     * 创建Redis消息监听者容器
     * @param factory
     * @return
     */
    @Bean("redisMessageListenerContainer")
    public RedisMessageListenerContainer redisMessageListenerContainer(RedisConnectionFactory factory){
        RedisMessageListenerContainer container = new RedisMessageListenerContainer();
        container.setConnectionFactory(factory);
        return container;
    }
}

3.新建消息订阅监听类

import com.opp.util.CommonUtil;
import org.springframework.data.redis.connection.Message;
import org.springframework.data.redis.connection.MessageListener;
import org.springframework.stereotype.Component;

import javax.websocket.Session;
import java.io.IOException;

/**
 * @author: huangnenghuan
 * @create: 2019/12/03
 * @description: 创建消息订阅监听者类
 **/
@Component
public class RedisMessageListener implements MessageListener {

    private org.slf4j.Logger logger = org.slf4j.LoggerFactory.getLogger(RedisMessageListener.class);

    /**
     * websocket客户端连接会话对象
     */
    private Session session;

    public Session getSession() {
        return session;
    }

    public void setSession(Session session) {
        this.session = session;
    }

    @Override
    public void onMessage(Message message, byte[] bytes) {
        String msg = new String(message.getBody()).replace("\"", "");
        if(CommonUtil.isNotNull(session) && session.isOpen()){
            try {
                session.getBasicRemote().sendText(msg);
            }catch (IOException e){
                logger.error("RedisSubListener消息订阅监听异常:" + e.getMessage());
            }
        }
    }
}

4.对websocket进行修改

import com.opp.listener.RedisMessageListener;
import com.opp.util.CommonUtil;
import com.opp.util.SpringUtils;
import org.slf4j.LoggerFactory;
import org.springframework.data.redis.listener.PatternTopic;
import org.springframework.data.redis.listener.RedisMessageListenerContainer;
import org.springframework.web.bind.annotation.RestController;

import javax.websocket.*;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;
import java.io.IOException;
import java.util.concurrent.ConcurrentHashMap;

/**
 * @author: huangnenghuan
 * @create: 2019/11/25
 * @description: websocket
 **/
@RestController
@ServerEndpoint("/webSocket/{id}")
public class WebSocketController {

    private org.slf4j.Logger logger = LoggerFactory.getLogger(WebSocketController.class);

    /**
     * 用来记录当前连接数的变量
     */
    private static volatile int onlineCount = 0;

    public static ConcurrentHashMap<String, WebSocketController> webSocketSet = new ConcurrentHashMap<>();

    /**
     * 与某个客户端的连接会话,需要通过它来与客户端进行数据收发
     */
    private Session session;

    /**
     * 用来引入刚才在webcoketConfig注入的类
     */
    private RedisMessageListenerContainer container = SpringUtils.getBean("redisMessageListenerContainer");

    /**
     * 自定义的消息发送器
     */
    private RedisMessageListener listener;

    /**
     * 连接建立成功调用的方法
     * @param id
     * @param session
     * @throws Exception
     */
    @OnOpen
    public void onOpen(@PathParam("id") String id, Session session) throws Exception {
        try {
            this.session = session;
            webSocketSet.put(id, this);
            //在线人数增加1
            addOnlineCount();
            sendMessage("连接成功");
            listener = new RedisMessageListener();
            //放入session
            listener.setSession(session);
            container.addMessageListener(listener, new PatternTopic("liveBroadcast"));
        } catch (IOException e) {
            logger.error("websocket IO异常");
        }
    }

    /**
     * 连接关闭调用方法
     */
    @OnClose
    public void onClose() {
        webSocketSet.remove(this);
        subOnlineCount();
        logger.info("websocket有一连接关闭");
        container.removeMessageListener(listener);
    }

    /**
     * 收到客户端消息后调用方法
     * @param message
     * @param session
     */
    @OnMessage
    public void onMessage(String message, Session session) throws Exception{
        if (session.isOpen()) {
            session.getBasicRemote().sendText(message);
        }
    }

    /**
     * @param session
     * @param error
     */
    @OnError
    public void onError(Session session, Throwable error) {
        logger.error("websocket 发生异常", error);
    }

    /**
     * 向客户端发送消息推送
     * @param message
     * @throws Exception
     */
    public void sendMessage(String message) throws Exception {
        if (this.session.isOpen()) {
            this.session.getBasicRemote().sendText(message);
        }
    }

    /**
     * 发送信息给指定ID用户,如果用户不在线则返回
     * @param message
     * @param sendUserId
     * @throws IOException
     */
    public void sendUserMessage(String message,String sendUserId) throws Exception {
        if(CommonUtil.isNotNull(webSocketSet.get(sendUserId))){
            webSocketSet.get(sendUserId).sendMessage(message);
        }else{
            logger.info("当前用户不在线:" + sendUserId);
        }
    }

    /**
     * 推送信息给所有用户
     * @param message
     * @throws IOException
     */
    public void sendAllMessage(String message) throws IOException {
        for(String key : webSocketSet.keySet()){
            try {
                webSocketSet.get(key).sendMessage(message);
            }catch (Exception e){
                logger.info("推送信息给所有用户异常:" + e.getMessage());
            }
        }
    }

    public static synchronized int getOnlineCount() {
        return onlineCount;
    }

    public static synchronized void addOnlineCount() {
        WebSocketController.onlineCount++;
    }

    public static synchronized void subOnlineCount() {
        WebSocketController.onlineCount--;
    }
}
/**
 * @author: huangnenghuan
 * @create: 2019/12/03
 * @description:
 **/
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
import org.springframework.beans.factory.config.BeanFactoryPostProcessor;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.stereotype.Component;

//定义为final类
@Component
public final class SpringUtils implements BeanFactoryPostProcessor {
    //静态
    private static ConfigurableListableBeanFactory beanFactory; // Spring应用上下文环境

    @Override
    public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory)
            throws BeansException {
        SpringUtils.beanFactory = beanFactory;
    }

    /**
     * 获取对象
     *
     * @param name
     * @return Object 一个以所给名字注册的bean的实例
     * @throws org.springframework.beans.BeansException
     *
     */
    @SuppressWarnings("unchecked")
    public static <T> T getBean(String name) throws BeansException {
        return (T) beanFactory.getBean(name);
    }

    /**
     * 获取类型为requiredType的对象
     *
     * @param clz
     * @return
     * @throws org.springframework.beans.BeansException
     *
     */
    public static <T> T getBean(Class<T> clz) throws BeansException {
        @SuppressWarnings("unchecked")
        T result = (T) beanFactory.getBean(clz);
        return result;
    }

    /**
     * 如果BeanFactory包含一个与所给名称匹配的bean定义,则返回true
     *
     * @param name
     * @return boolean
     */
    public static boolean containsBean(String name) {
        return beanFactory.containsBean(name);
    }

    /**
     * 判断以给定名字注册的bean定义是一个singleton还是一个prototype。
     * 如果与给定名字相应的bean定义没有被找到,将会抛出一个异常(NoSuchBeanDefinitionException)
     *
     * @param name
     * @return boolean
     * @throws org.springframework.beans.factory.NoSuchBeanDefinitionException
     *
     */
    public static boolean isSingleton(String name) throws NoSuchBeanDefinitionException {
        return beanFactory.isSingleton(name);
    }

    /**
     * @param name
     * @return Class 注册对象的类型
     * @throws org.springframework.beans.factory.NoSuchBeanDefinitionException
     *
     */
    public static Class<?> getType(String name) throws NoSuchBeanDefinitionException {
        return beanFactory.getType(name);
    }

    /**
     * 如果给定的bean名字在bean定义中有别名,则返回这些别名
     *
     * @param name
     * @return
     * @throws org.springframework.beans.factory.NoSuchBeanDefinitionException
     *
     */
    public static String[] getAliases(String name) throws NoSuchBeanDefinitionException {
        return beanFactory.getAliases(name);
    }

}

最后redis发布通道消息

cacheComponent.sendMessage("liveBroadcast", "Barrage-" + String.valueOf(obj));

 

  • 1
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
好的,我可以回答这个问题。以下是一个简单的 Spring Boot 整合 Redis 实现发布订阅消息的例子: 1. 首先,在 pom.xml 文件中添加 Redis 相关依赖: ``` <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> ``` 2. 在 application.properties 文件中配置 Redis 连接信息: ``` spring.redis.host=localhost spring.redis.port=6379 ``` 3. 创建一个 Redis 发布者: ``` @Component public class RedisPublisher { @Autowired private RedisTemplate<String, Object> redisTemplate; public void publish(String channel, Object message) { redisTemplate.convertAndSend(channel, message); } } ``` 4. 创建一个 Redis 订阅者: ``` @Component public class RedisSubscriber { @Autowired private MessageListenerAdapter messageListenerAdapter; @PostConstruct public void init() { redisTemplate.execute((RedisConnection connection) -> { connection.subscribe(messageListenerAdapter, "channel"); return null; }); } @PreDestroy public void destroy() { redisTemplate.execute((RedisConnection connection) -> { connection.unsubscribe(messageListenerAdapter, "channel"); return null; }); } } ``` 5. 在需要发布消息的地方调用 RedisPublisher 的 publish 方法: ``` @Autowired private RedisPublisher redisPublisher; redisPublisher.publish("channel", "message"); ``` 6. 在需要订阅消息的地方实现 MessageListener 接口: ``` @Component public class MyMessageListener implements MessageListener { @Override public void onMessage(Message message, byte[] pattern) { String channel = new String(message.getChannel()); String messageBody = new String(message.getBody()); System.out.println("Received message: " + messageBody + " from channel: " + channel); } } ``` 以上就是一个简单的 Spring Boot 整合 Redis 实现发布订阅消息的例子。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值