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();
    }
posted @ 2022-09-29 14:46  imissinstagram  Views(547)  Comments(0Edit  收藏  举报