遇到的问题

易登(个人微信登录解决方案) 官网在最初做登录功能的时候,是通过HTTP轮询的方式实现的,后来随着用户逐渐增加,这种方案的弊端逐步展现了,频繁的请求后端接口导致服务器负载增加,在不想增加服务器成本的情况下,将实现方案替换成了websocket技术。

系统最初是在单机状态下运行的,websocket实现起来没有什么问题。随着系统的运行,后来增加了一台服务实现了双服务的集群。但这时在登录系统的时候就遇到了websocket在集群环境下的问题。有时候明明扫码登陆成功了,但系统并未接收到登录状态,导致无法登录系统。

问题根源

前端与服务A建立websocket连接时,服务A会记录websocket的session会话信息,但是服务B并未与前端建立websocket连接。这时如果通过服务B发送消息给前端的时候,由于服务B未与前端建立websocket连接,导致websocket消息无法发送到前端。

类似于集群环境下如果http session会话不进行共享,用户信息在多个服务之间会丢失的情况。

问题解决

既然问题已经找到了,那就好办了,可以将需要发送的消息同时通知服务A和服务B,谁持有session会话信息就由谁发消息不就行了吗?

看到这个小伙伴是不是有些眉目了,这不就是典型的发布订阅模型嘛。

两个服务都订阅同一个渠道,只要这个渠道里有消息,两个服务都去发送这个消息。这样就可以保证消息是可以被发送出去的。

springboot 项目 集成socket springboot websocket集群_解决方案

解决方案

市面上有许多已经实现了发布订阅模型的方案,比如说MQ框架、Redis等。由于易登系统已经集成了Redis,就没有再引入MQ框架来解决这个问题。

下面说一下如何使用Redis的发布订阅功能来解决这个问题。

解决方案实现

  • 引入 Jedis依赖
<dependency>
	<groupId>redis.clients</groupId>
	<artifactId>jedis</artifactId>
	<version>3.7.0</version>
</dependency>
  • 创建redis连接池
@Slf4j
@Configuration
@Data
public class JedisConfig {

    @Value("${redis.host}")
    private String host;
    @Value("${redis.port}")
    private Integer port;
    @Value("${redis.user}")
    private String user;
    @Value("${redis.password}")
    private String password;

    @Bean
    public JedisPool jedisPool() {
        JedisPool jedisPool = new JedisPool(this.host, this.port, StringUtils.isEmpty(user) ? null : this.user, StringUtils.isEmpty(this.password) ? null : this.password);
        log.info("jedis init success.");
        return jedisPool;
    }
}
  • 自定义自己的redis订阅处理逻辑
@Slf4j
public class WsSubscriber extends JedisPubSub {

    // 当有消息发布到名称为 ws-channel 的渠道时会被该方法会监听到,服务A和服务B都是可以将听到这个方法内容的,我们需要在这里实现自己的逻辑
    @Override
    public void onMessage(String channel, String message) {
        log.info("jedis Subscriber channel={}, message={}", channel, message);
        // do sth... 这里可以调用websocket发送消息的方法就可以了,这时服务A和服务B都会去发送ws消息
    }

    // 当有订阅操作时会被该方法监听到
    @Override
    public void onSubscribe(String channel, int subscribedChannels) {
        log.info("jedis Subscriber channel={}, subscribedChannels={}", channel, subscribedChannels);
        super.onSubscribe(channel, subscribedChannels);
    }
}
  • 在服务启动的时候先订阅一个渠道
@Component
@Slf4j
public class RedisSubscribeConfig {

    @Resource
    private ExecutorService executorService;

    @PostConstruct
    public void config() {
	// 这里另起一个线程来完成订阅操作是为了不影响服务的其他配置的初始化
        executorService.execute(() -> {
	    // 订阅 ws-channel 渠道,该渠道有发布消息时,用我们自定义的订阅类处理
            jedisPool.getResource().subscribe(new WsSubscriber(), "ws-channel");
        });
    }
}

经过上面的处理,最初的问题就被解决了