使用Redis发布订阅模式实现 Session共享

其实并不是实现session共享,而是通过redis的发布订阅,让所有集群的服务器,都让自己的session发送一下消息。比如说userId在第35台服务器上, 有100台服务器,那么第1台服务器收到消息,需要通知userId,不是找到第35台服务器,而是通知所有的服务器,给userId发条消息,其他99台服务器没有userId,那就发送不成功!

1、配置redis

package com.kakarote.crm.config;

import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.kakarote.crm.constant.RedisConstants;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.pool2.impl.GenericObjectPoolConfig;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.connection.RedisStandaloneConfiguration;
import org.springframework.data.redis.connection.lettuce.LettuceClientConfiguration;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.data.redis.connection.lettuce.LettucePoolingClientConfiguration;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.listener.PatternTopic;
import org.springframework.data.redis.listener.RedisMessageListenerContainer;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;

import java.time.Duration;

@Configuration
public class CrmTemplateConfig {

    @Value("${spring.redis.host}")
    private String redisHost;

    @Value("${spring.redis.port}")
    private int redisPort;

    @Value("${spring.redis.password}")
    private String redisHasrdpwd;

    @Value("${spring.redis.database}")
    private Integer database;

    @Bean(name = "crmRedisTemplate")
    public RedisTemplate redisTemplate() {
        RedisTemplate<String, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(connectionFactory(database, redisHost, redisPort, redisHasrdpwd));
        template.setKeySerializer(new StringRedisSerializer());
        template.setValueSerializer(new GenericJackson2JsonRedisSerializer());
        template.setHashKeySerializer(new StringRedisSerializer());
        template.setHashValueSerializer(new GenericJackson2JsonRedisSerializer());
        return template;
    }

    public RedisConnectionFactory connectionFactory(int database, String hostName, int port, String password) {

        RedisStandaloneConfiguration configuration = new RedisStandaloneConfiguration();
        configuration.setHostName(hostName);
        configuration.setPort(port);
        if (StringUtils.isNotBlank(password)) {
            configuration.setPassword(password);
        }
        if (database != 0) {
            configuration.setDatabase(database);
        }

        GenericObjectPoolConfig genericObjectPoolConfig = new GenericObjectPoolConfig();
        genericObjectPoolConfig.setMaxIdle(10);
        genericObjectPoolConfig.setMinIdle(10);
        genericObjectPoolConfig.setMaxTotal(100);
        genericObjectPoolConfig.setMaxWaitMillis(3000);

        LettuceClientConfiguration clientConfig = LettucePoolingClientConfiguration.builder()
                .commandTimeout(Duration.ofMillis(8000))
                .poolConfig(genericObjectPoolConfig)
                .build();

        LettuceConnectionFactory lettuce = new LettuceConnectionFactory(configuration, clientConfig);
        lettuce.afterPropertiesSet();
        return lettuce;
    }

    /**
     * Redis消息监听器容器
     * 这个容器加载了RedisConnectionFactory和消息监听器
     * 可以添加多个监听不同话题的redis监听器,只需要把消息监听器和相应的消息订阅处理器绑定,该消息监听器
     * 通过反射技术调用消息订阅处理器的相关方法进行一些业务处理
     *
     * @return redis消息监听容器
     */
    @Bean
    @SuppressWarnings("all")
    public RedisMessageListenerContainer container(
            RedisMessageListener listener) {


        RedisMessageListenerContainer container = new RedisMessageListenerContainer();
        // 监听所有库的key过期事件
        container.setConnectionFactory(connectionFactory(database, redisHost, redisPort, redisHasrdpwd));
        // 所有的订阅消息,都需要在这里进行注册绑定,new PatternTopic(TOPIC_NAME1)表示发布的主题信息
        // 可以添加多个 messageListener,配置不同的通道
        container.addMessageListener(listener, new PatternTopic(RedisConstants.WEBSOCKET_REDIS_TOPIC));

        /**
         * 设置序列化对象
         * 特别注意:1. 发布的时候需要设置序列化;订阅方也需要设置序列化
         *         2. 设置序列化对象必须放在[加入消息监听器]这一步后面,否则会导致接收器接收不到消息
         */
        Jackson2JsonRedisSerializer seria = new Jackson2JsonRedisSerializer(Object.class);
        ObjectMapper objectMapper = new ObjectMapper();
        objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        objectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
        seria.setObjectMapper(objectMapper);
        container.setTopicSerializer(seria);

        return container;
    }
}

2、配置RedisMessageListener

package com.kakarote.crm.config;

import cn.hutool.json.JSONUtil;
import com.alibaba.fastjson.JSONObject;
import com.kakarote.crm.constant.CrmConst;
import com.kakarote.crm.entity.BO.MessageDto;
import com.kakarote.crm.websocket.TransferCallWebSocket;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.connection.Message;
import org.springframework.data.redis.connection.MessageListener;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;

@Slf4j
@Component
public class RedisMessageListener implements MessageListener {

    @Autowired
    private RedisTemplate<String, Object> crmRedisTemplate;

    @Override
    public void onMessage(Message message, byte[] pattern) {

        // 接收的topic
        log.info("RedisMessageListener-接收到消息1,channel:" + new String(pattern));
        try {
            //序列化对象(特别注意:发布的时候需要设置序列化;订阅方也需要设置序列化)
            MessageDto messageDto = (MessageDto) crmRedisTemplate.getValueSerializer().deserialize(message.getBody());
            log.info("RedisMessageListener-接收到消息2,channel = {}, messageDto = {}", new String(pattern), messageDto);
            if(messageDto == null){
                log.info("RedisMessageListener-messageDto = null,无消息进行发送! message = {}", JSONUtil.toJsonStr(message));
                return;
            }
            if(CrmConst.NOTICE_MSG.equals(messageDto.getTitle())){
                JSONObject content = messageDto.getContent();
                String toUserId = content.getString("toUserId");
                String fromUserId = content.getString("fromUserId");
                JSONObject msg = content.getJSONObject("msg");

                String resp = TransferCallWebSocket.sendMsgByUserId(fromUserId, toUserId, JSONUtil.toJsonStr(msg));
                if(!resp.equals("success")){
                    log.info("RedisMessageListener-发送弹框消息,resp = {},content = {}", resp, content);
                }
            }

        }catch (Exception e){
            log.info("RedisMessageListener-监听消息处理失败,失败原因 = {}, e = ", e.getMessage(), e);
        }
    }
}


3、静态类

/**
 * @description: 常量类
 * @dateTime: 2021/6/17 16:21
 */
public class RedisConstants {

    /**
     * UTF-8 字符集
     */
    public static final String UTF8 = "UTF-8";

    public final static String WEBSOCKET_REDIS_TOPIC = "websocket_topic";
 	public static final String TRANSFER_NOTICE = "transferCallNotice";	
    public static final String NOTICE_MSG = "noticeMessage";
}

4、消息体

package com.kakarote.crm.entity.BO;

import com.alibaba.fastjson.JSONObject;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.io.Serializable;

@AllArgsConstructor
@NoArgsConstructor
@Data
public class MessageDto implements Serializable {
    private String data;
    private String title;
    private JSONObject content;
}

5、业务类像通道发送消息

    /**
     * 向通道发布消息
     */
    public boolean convertAndSend(String channel, Object message) {
        if (StringUtil.isBlank(channel)) {
            return false;
        }
        try {
            crmRedisTemplate.convertAndSend(channel, message);
            log.info("发送消息成功,channel:{},message:{}", channel, message);
            return true;
        } catch (Exception e) {
            log.info("发送消息失败,channel:{},message:{}, 失败原因 = {}, e = ", channel, message, e.getMessage(), e);
            e.printStackTrace();
        }
        return false;
    }

6、websocket配置

@Configuration
@ComponentScan
@EnableAutoConfiguration
public class WebSocketConfiguration implements ServletContextInitializer {

    @Bean
    public ServerEndpointExporter serverEndpointExporter() {
        return new ServerEndpointExporter();
    }

    @Bean
    public TaskScheduler taskScheduler(){
        ThreadPoolTaskScheduler taskScheduler = new ThreadPoolTaskScheduler();
        taskScheduler.setPoolSize(10);
        taskScheduler.initialize();
        return taskScheduler;
    }

	@Override
	public void onStartup(ServletContext servletContext) throws ServletException {
		servletContext.addListener(WebAppRootListener.class);
        servletContext.setInitParameter("org.apache.tomcat.websocket.textBufferSize","52428800");
        servletContext.setInitParameter("org.apache.tomcat.websocket.binaryBufferSize","52428800");
	}
}

7、websocket Controller类

@ServerEndpoint("/crmDzhWebsocket/transferWebsocket/{userId}")
@Component
@Slf4j
public class TransferCallWebSocket {


    /**
     * 当前在线连接数
     */
    private static AtomicInteger onlineCount = new AtomicInteger(0);

    /**
     * 用来存放每个客户端对应的 WebSocketServer 对象
     */
    private static final ConcurrentHashMap<String, Session> webSocketMap = new ConcurrentHashMap<>();

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

    /**
     * 接收 userId
     */
    private String userIdKey = "";


    /**
     * 连接建立成功调用的方法
     */
    @OnOpen
    public void onOpen(Session session, @PathParam("userId") String userId) {
        this.session = session;
        this.userIdKey = userId;
        if (webSocketMap.containsKey(userId)) {
            webSocketMap.remove(userId);
            webSocketMap.put(userId, session);
        } else {
            webSocketMap.put(userId, session);
            addOnlineCount();
        }
        log.info("转接通知用户连接:" + userId + ",当前总在线人数为:" + getOnlineCount());
        try {
            sendMessage("success");
        } catch (IOException e) {
            log.error("转接通知用户:" + userId + ",网络异常!!!!!!");
            log.info("转接通知用户连接:" + userId + ",网络异常!!!!!!");
        }
    }

    /**
     * 连接关闭调用的方法
     */
    @OnClose
    public void onClose() {
        if (webSocketMap.containsKey(userIdKey)) {
            webSocketMap.remove(userIdKey);
            subOnlineCount();
        }
        log.info("转接通知用户退出:" + userIdKey + ",当前总在线人数为:" + getOnlineCount());
    }

    /**
     * 收到客户端消息后调用的方法
     *
     * @param message 客户端发送过来的消息
     */
    @OnMessage
    public void onMessage(String message, Session session) {
        try {
            if ("ping".equals(message)) {
                webSocketMap.get(this.userIdKey).getBasicRemote().sendText("pong");
                return;
            }
            log.info("this.userIdKey = {}, message = {}", this.userIdKey, message);
        } catch (IOException e) {
            log.error("转接通知发送消息失败,失败原因 = {}, e = ", e.getMessage(), e);
            e.printStackTrace();
        }
    }

    public static String sendMsgByUserId(String fromUserId, String toUserId, String msg) throws IOException {
        if(webSocketMap.get(toUserId) != null){
            try {
                webSocketMap.get(toUserId).getBasicRemote().sendText(msg);
                return "success";
            }catch (Exception e){
                log.error("发送消息失败,fromUserId = {}, toUserId = {}", fromUserId, toUserId);
                return e.getMessage();
            }
        }
        return "userId:" + toUserId + "当前不在会话中";
    }

    /**
     * 发生错误时调用
     *
     * @param session
     * @param error
     */
    @OnError
    public void onError(Session session, Throwable error) {
        log.info("用户错误:" + session.getId() + ",原因:" + error.getMessage());
    }

    /**
     * 实现服务器主动推送
     */
    public void sendMessage(String message) throws IOException {
        this.session.getBasicRemote().sendText(message);
    }

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

    public static synchronized void addOnlineCount() {
        TransferCallWebSocket.onlineCount.getAndIncrement();
    }

    public static synchronized void subOnlineCount() {
        TransferCallWebSocket.onlineCount.getAndDecrement();
    }


}
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
您可以按照以下步骤来实现Tomcat 8与Redis哨兵模式Session共享: 1. 首先,确保您已经安装了Tomcat 8和Redis,并且Redis已经配置为哨兵模式。您可以参考Redis官方文档来进行配置。 2. 在Tomcat的conf目录下,创建一个新的文件夹,命名为context。在该文件夹下创建一个新的XML文件,命名为yourappname.xml(将yourappname替换为您的应用程序名称)。 3. 在yourappname.xml文件中,添加以下内容: ```xml <?xml version="1.0" encoding="UTF-8"?> <Context> <Valve className="org.apache.catalina.session.PersistentManager" saveOnRestart="true" maxIdleBackup="30" expireSessionsOnShutdown="false"> <Store className="org.apache.catalina.session.RedisSessionHandlerValve"/> </Valve> </Context> ``` 4. 在Tomcat的lib目录下,创建一个新的文件夹,命名为redis-session-manager。将以下JAR文件复制到该文件夹中: - jedis.jar - commons-pool2.jar - tomcat-redis-session-manager.jar 5. 在Tomcat的conf目录下,打开context.xml文件,在`<Context>`标签内添加以下内容: ```xml <Manager className="de.javakaffee.web.msm.redis.RedisSessionManager" host="your_redis_host" port="your_redis_port" database="your_redis_database" password="your_redis_password" maxInactiveInterval="your_session_timeout"> </Manager> ``` 将上述内容中的your_redis_host、your_redis_port、your_redis_database、your_redis_password和your_session_timeout替换为您的Redis连接信息和会话超时时间。 6. 在Tomcat的catalina.properties文件中,添加以下内容: ```properties redis-session-manager.enabled=true redis-session-manager.config.path=/path/to/tomcat/conf/context/yourappname.xml ``` 将/path/to/tomcat/conf/context/yourappname.xml替换为您在步骤2中创建的XML文件的实际路径。 7. 重启Tomcat服务器,使配置生效。 现在,您的Tomcat 8应该与Redis哨兵模式实现Session共享。您可以在多个Tomcat节点上部署您的应用程序,并且它们将共享会话数据。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值