最近项目组要做个IM即时通讯工具,用于渠道营销人员使用。拿到需求后,一时茫然不知如何实现,之前也没做过IM的经验,于是花了2天研究业界,设计个方案,拉上项目组(项目经理、技术经理和几个核心研发人员)评审通过,开始组建团队开工干活。今天主要介绍下用户上线后触发聊天列表的推送机制。
聊天列表主要是:发送者、未读消息条数、最近一条消息内容、最近一条消息发送时间、消息全局流水号ID。由于用户上线,要触发消息的推送,推送走NIO框架Netty发送。负责消息推送的是连接服务器(用户终端和连接服务器建立socket链接),而链接服务器是分布式集群部署,用户上线后的路由信息全局存储至Redis,这里如何做到新消息到来,立即推送未读消息条数给用户终端,用到了Redis的发布订阅机制。消息到来,存储至消息Redis,同时向指定channel发布消息,通知订阅方,订阅方接收消息后根据接收账号和发送账号读取消息Redis库,计算接收者有多少条未读消息,利用webscoket协议推送给用户终端。
上面是业务场景,下面讲解具体代码如何实现,项目使用Springboot框架:
- RedisConfig配置类
@Configuration
public class RedisConfig {
@Value("${server.host}")
private String serverip; //连接服务器IP
@Bean
@SuppressWarnings("all")
public RedisTemplate redisTemplate(RedisConnectionFactory factory) {
RedisTemplate template = new StringRedisTemplate();
template.setConnectionFactory(factory);
Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
ObjectMapper om = new ObjectMapper();
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
jackson2JsonRedisSerializer.setObjectMapper(om);
StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
// key采用String的序列化方式
template.setKeySerializer(stringRedisSerializer);
// hash的key也采用String的序列化方式
template.setHashKeySerializer(stringRedisSerializer);
// value序列化方式采用jackson
template.setValueSerializer(jackson2JsonRedisSerializer);
// hash的value序列化方式采用jackson
template.setHashValueSerializer(jackson2JsonRedisSerializer);
template.afterPropertiesSet();
return template;
}
@Bean(name = "redisUtils")
public RedisUtils redisUtils(RedisTemplate<?,?> redisTemplate){
RedisUtils redisUtils = new RedisUtils();
redisUtils.setRedisTemplate(redisTemplate);
return redisUtils;
}
//初始化监听器
@Bean
public RedisMessageListenerContainer container(RedisConnectionFactory connectionFactory,
MessageListenerAdapter listenerUserOnlineAdapter) {
RedisMessageListenerContainer container = new RedisMessageListenerContainer();
container.setConnectionFactory(connectionFactory);
String channel_useronline = String.format(Constants.RedisKey.channel_useronline, serverip);
container.addMessageListener(listenerNewMessageAdapter, new PatternTopic(channel_newMessage));
return container;
}
@Bean
MessageListenerAdapter listenerUserOnlineAdapter(RedisUserOnlineReceiver receiver) {
return new MessageListenerAdapter(receiver, "receiveMessage");
}
}
Note:
1、其中String channel_useronline = String.format(Constants.RedisKey.channel_useronline, serverip);container.addMessageListener(listenerUserOnlineAdapter, new PatternTopic(channel_useronline));即订阅该channel。 其中RedisMessageListenerContainer是消息监听器容器,可以添加多个监听不同话题的redis监听器,比如:
container.addMessageListener(listenerNewMessageAdapter1, new PatternTopic(channel_newMessage1));
container.addMessageListener(listenerNewMessageAdapter2, new PatternTopic(channel_newMessage2));
只需要把消息监听器和相应的消息订阅处理器绑定,该消息监听器通过Reflect调用消息订阅处理器的相关方法进行一些业务处理。
2、listenerUserOnlineAdapter则是消息监听器适配器,绑定消息处理器RedisUserOnlineReceiver,利用反射技术调用消息处理器的业务方法receiveMessage。
- 订阅处理器类RedisUserOnlineReceiver
/** * redis订阅用户上线事件,接收事件后进行未读消息的推送 **/ @Service public class RedisUserOnlineReceiver { private final Logger logger = LoggerFactory.getLogger(RedisUserOnlineReceiver.class); public void receiveMessage(String message) { //编写订阅后的业务逻辑处理 logger.info("-----------RedisUserOnlineReceiver subscribe new message=" + message); } }
结合我的业务背景,此处业务逻辑是:访问消息Redis库,查询指定接收账号的未读消息条数,组织报文通过webscoket协议走Netty的Channe.writeAndFlush方法推送给用户终端!
-
采用Spring session时,如果消息通知频繁,并发比较高,接收事件的线程池可能应付不了,需要配置线程池,来复用线程,减少创建线程的开销,提高响应速度,代码如下:
@Bean public ThreadPoolTaskExecutor springSessionRedisTaskExecutor(){ ThreadPoolTaskExecutor springSessionRedisTaskExecutor = new ThreadPoolTaskExecutor(); springSessionRedisTaskExecutor.setCorePoolSize(20); springSessionRedisTaskExecutor.setMaxPoolSize(20); springSessionRedisTaskExecutor.setKeepAliveSeconds(10); springSessionRedisTaskExecutor.setQueueCapacity(1000); springSessionRedisTaskExecutor.setThreadNamePrefix("Spring session redis executor thread: "); return springSessionRedisTaskExecutor; }
其实本质原因是Spring session(redis存储方式)监听导致创建大量redisMessageListenerContailner-X线程。默认情况下监听器线程池不配置就采用框架自带的SimpleAsyncTaskExecutor线程池,而SimpleAsyncTaskExecutor每次都将创建新的线程处理请求,其实它应该算伪线程池,但它可以设置最大并发线程数量。具体源码请见
-
具体细节请读者自行查看下源码并分析下原理。