Redis Stream实现全部节点机器推送消息
背景
有时候,在微服务时代,我们需要对全部的机器节点进行通知。在常规情况下,一个请求经过负载均衡只有一个机器可以收到。那么,如何能让全部的机器都收到同样的请求呢?需要借助消息队列的监听机制,让每个节点都监听一个队列,让消息发送到所有的队列中。
rabbit MQ的fanout交换机可以实现这种功能。
那么,如果想用redis去实现这个功能,有没有什么好的选择呢?毕竟仅仅为了一个全部节点的推送,就引入另外一个中间件,不是一个很经济选择。
那么Redis的Stream结构就是一个可以选择的了。
实现
对于Redis的Stream结构,诞生之初就是为了用作消息队列的。具体用法如下:
- 发送消息
public void sendConfigMessage() {
MapRecord<String, String, String> entries = StreamRecords.mapBacked(Collections.singletonMap("msg", "plsGet")).withStreamKey(RedisConfig.stream);
// 将消息添加至消息队列中
redisTemplate.opsForStream().add(entries);
}
- 建立监听
@Bean
public Subscription subscription(RedisConnectionFactory factory) {
Set<String> keys = redisTemplate.keys(streamPattern);
if (keys != null && keys.size() != 0) {
keys = keys.stream().filter(key -> !key.equals(stream)).collect(Collectors.toSet());
if (keys.size() != 0) {
redisTemplate.delete(keys);
}
}
group = UUID.randomUUID().toString();
if (Boolean.FALSE.equals(redisTemplate.hasKey(stream))) {
StringRecord stringRecord = StreamRecords.string(Collections.singletonMap("msg", "init")).withStreamKey(RedisConfig.stream);
redisTemplate.opsForStream().add(stringRecord);
}
redisTemplate.opsForStream().createGroup(stream, group);
StreamMessageListenerContainer.StreamMessageListenerContainerOptions<String, MapRecord<String, String, String>> options = StreamMessageListenerContainer
.StreamMessageListenerContainerOptions
.builder()
.pollTimeout(Duration.ofSeconds(1))
.build();
StreamMessageListenerContainer<String, MapRecord<String, String, String>> listenerContainer = StreamMessageListenerContainer.create(factory, options);
Subscription subscription = listenerContainer.receiveAutoAck(Consumer.from(group, "consumer1"), StreamOffset.create(configStream, ReadOffset.lastConsumed()), configStreamListener);
listenerContainer.start();
return subscription;
}
对于每一台机器,都让它关联唯一的消费组,而这个功能关联唯一的stream key。Redis的stream机制在于,每一条给stream key发送的消息都会推送给所有的消费组,这样所有的机器都会收到这条消息。
一些问题
Redis的连接和MQ还是有一些区别。当Redis连接超时之后,之前建立的监听就不能用了,因为之前的长连接断开了。
一个解决档案就是手动维持一个Netty的心跳机制,不停轮训判断当前的订阅是否还处于活跃状态。一旦不处于活跃状态就要重新建立长连接:
@Autowired
Subscription subscription;
@Autowired
RedisConnectionFactory factory;
@Autowired
StreamListener streamListener;
@Autowired
RedisTemplate<String, Object> redisTemplate;
//当Redis连接超时,自动重置stream队列。否则监听失效
@Bean
public ClientResources clientResources(){
NettyCustomizer nettyCustomizer = new NettyCustomizer() {
@Override
public void afterChannelInitialized(Channel channel) {
channel.pipeline().addLast(
new IdleStateHandler(0, 0, 10));
channel.pipeline().addLast(new ChannelDuplexHandler() {
@Override
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) {
if (subscription != null && !subscription.isActive()) {
synchronized ("resetStreamLock") {
if (!subscription.isActive()) {
StreamMessageListenerContainer.StreamMessageListenerContainerOptions<String, MapRecord<String, String, String>> options = StreamMessageListenerContainer
.StreamMessageListenerContainerOptions
.builder()
.pollTimeout(Duration.ofSeconds(1))
.build();
StreamMessageListenerContainer<String, MapRecord<String, String, String>> listenerContainer0 = StreamMessageListenerContainer.create(factory, options);
Subscription subscription0 = listenerContainer0.receiveAutoAck(Consumer.from(RedisConfig.group, "consumer1"), StreamOffset.create(RedisConfig.stream, ReadOffset.lastConsumed()), streamListener);
listenerContainer0.start();
subscription = subscription0;
log.info("reset getStream!");
}
}
}
if (evt instanceof IdleStateEvent) {
ctx.disconnect();
}
}
});
}
@Override
public void afterBootstrapInitialized(Bootstrap bootstrap) {
}
};
return ClientResources.builder().nettyCustomizer(nettyCustomizer ).build();
}
本文来自博客园,作者:imissinstagram,转载请注明原文链接:https://www.cnblogs.com/LostSecretGarden/p/16741552.html