美文网首页Java技术分享IT@程序员猿媛程序园
解决Redis集群条件下键空间通知服务器接收不到消息的问题

解决Redis集群条件下键空间通知服务器接收不到消息的问题

作者: 不学无数的程序员 | 来源:发表于2019-04-28 15:28 被阅读1次

    解决Redis集群条件下键空间通知服务器接收不到消息的问题

    键空间通知介绍

    键空间通知使得客户端可以通过订阅频道或模式, 来接收那些以某种方式改动了 Redis 数据集的事件。

    可以通过对redis的redis.conf文件中配置notify-keyspace-events参数可以指定服务器发送哪种类型的通知。下面对于一些参数的描述。默认情况下此功能是关闭的。

    字符 通知
    K 键空间通知,所有通知以 __keyspace@<db>__ 为前缀
    E 键事件通知,所有通知以 __keyevent@<db>__ 为前缀
    g DELEXPIRERENAME 等类型无关的通用命令的通知
    $ 字符串命令的通知
    l 列表命令的通知
    s 集合命令的通知
    h 哈希命令的通知
    z 有序集合命令的通知
    x 过期事件:每当有过期键被删除时发送
    e 驱逐(evict)事件:每当有键因为 maxmemory 政策而被删除时发送
    A 参数 g$lshzxe 的别名

    所以当你配置文件中配置为AKE时就表示发送所有类型的通知。

    在程序中接入

    使用SpringData可以轻松的实现对于redis键空间通知的接收操作。只需要作如下配置即可

    所使用的jar包

    'org.springframework.boot:spring-boot-starter-data-redis'
    
    

    配置监听器

    @Configuration
    @ConditionalOnExpression("!'${spring.redis.host:}'.isEmpty()")
    public static class RedisStandAloneAutoConfiguration {
        @Bean
        public RedisMessageListenerContainer customizeRedisListenerContainer(
                RedisConnectionFactory redisConnectionFactory,MessageListener messageListener) {
            RedisMessageListenerContainer redisMessageListenerContainer = new RedisMessageListenerContainer();
            redisMessageListenerContainer.setConnectionFactory(redisConnectionFactory);
            redisMessageListenerContainer.addMessageListener(messageListener,new PatternTopic("__keyspace@0__:*"));
            return redisMessageListenerContainer;
        }
    }
    
    

    其中PatternTopic构造器里面填写的是你所要监听哪一个通道。

    例如在redis中执行set blog buxuewushu。我配置文件中配置的AKE所以所有消息都会发送,他就会发送两条信息。

    PUBLISH __keyspace@0__:blog set
    PUBLISH __keyevent@0__:set blog
    
    

    所以我在上面配置的监听规则__keyspace@0__:*就是监听0号库发送的所有space信息都会接收到。

    配置处理器

    上面我们配置了监听Redis的哪条通道,现在我们需要配置接收到了信息以后如何处理的事情。所以此时我们需要在程序中写处理器

    
    @Slf4j
    @Component
    public class KeyExpiredEventMessageListener implements MessageListener {
        @Override
        public void onMessage(Message message, byte[] pattern) {
            log.info("监听失效的redisKey:{},值是:{}", new String(message.getChannel()), new String(message.getBody()));
        }
    }
    
    

    只需要实现MessageListener即可。我们只是将监听到的键和发送的信息打印出来。

    效果展示

    此时我们启动本地的redis,然后执行set blog buxuewushu命令,可以在程序中看到。下面的输出。即我们已经监听到了redis发送的消息了。

    c.e.s.r.KeyExpiredEventMessageListener   : 监听到的信息:__keyspace@0__:blog,值是:set
    
    

    此时如果我们将规则变成__key*__:*那么会收到什么呢?还是执行set blog buxuewushu命令

    c.e.s.r.KeyExpiredEventMessageListener   : 监听到的信息:__keyspace@0__:blog,值是:set
    c.e.s.r.KeyExpiredEventMessageListener   : 监听到的信息:__keyevent@0__:set,值是:blog
    
    

    我们看到执行一个set命令可以收到两个消息,一个是space消息一个是event消息。

    集群条件下

    我们刚才的测试都是在单机Redis下测试的,当将Redis转为集群模式时,会发现接收不到了消息了。此时我们启动本机的redis的集群。关于如何在本机利用docker一键部署集群可以参考我的一篇文章Mac上最简单明了的利用Docker搭建Redis集群。启动完redis集群以后我们还是启动程序进行测试。

    redis集群配置如下,监听规则改为如下

    spring:
      redis:
        cluster:
          nodes:
          - 127.0.0.1:7000
          - 127.0.0.1:7001
          - 127.0.0.1:7002
          - 127.0.0.1:7003
          - 127.0.0.1:7004
          - 127.0.0.1:7005
    

    我们redis中如下的命令

    127.0.0.1:7002> set blog buxuwshu
    -> Redirected to slot [7653] located at 127.0.0.1:7001
    OK
    127.0.0.1:7001> set blog1 buxuwshu
    -> Redirected to slot [2090] located at 127.0.0.1:7000
    OK
    127.0.0.1:7000> set blog2 buxuwshu
    -> Redirected to slot [14409] located at 127.0.0.1:7002
    OK
    127.0.0.1:7002> set blog3 buxuwshu
    -> Redirected to slot [10344] located at 127.0.0.1:7001
    OK
    127.0.0.1:7001> set blog4 buxuwshu
    OK
    127.0.0.1:7001> set blog5 buxuwshu
    -> Redirected to slot [2222] located at 127.0.0.1:7000
    OK
    
    

    在程序中打印如下

    c.e.s.r.KeyExpiredEventMessageListener   : 监听到的信息:__keyspace@0__:blog,值是:set
    c.e.s.r.KeyExpiredEventMessageListener   : 监听到的信息:__keyspace@0__:blog3,值是:set
    c.e.s.r.KeyExpiredEventMessageListener   : 监听到的信息:__keyspace@0__:blog4,值是:set
    

    我们看到只打印了blogblog1blog4的键,而我们通过上面观察,打印的键都是分布在7001端口上的。因此我们预测程序只是监听了7001端口发送的消息。而通过N次测试,程序不是每次都在监听7001端口,而是随机的。但是每次只会监听一个端口。

    问题所在

    接下来让我们通过找寻源码,看看到底是哪出的问题。

    JedisSlotBasedConnectionHandlergetConnection方法中

    public Jedis getConnection() {
        // In antirez's redis-rb-cluster implementation,
        // getRandomConnection always return valid connection (able to
        // ping-pong)
        // or exception if all connections are invalid
    
        List<JedisPool> pools = cache.getShuffledNodesPool();
    
        for (JedisPool pool : pools) {
          Jedis jedis = null;
          try {
            jedis = pool.getResource();
    
            if (jedis == null) {
              continue;
            }
    
            String result = jedis.ping();
    
            if (result.equalsIgnoreCase("pong")) return jedis;
    
            jedis.close();
          } catch (JedisException ex) {
            if (jedis != null) {
              jedis.close();
            }
          }
        }
        throw new JedisNoReachableClusterNodeException("No reachable node in cluster");
      }
    
    

    可以看到注释中写着会获得一个随机的有效连接。也可以通过代码看到,获得连接池的信息以后遍历,直到有一个信息能够ping-pong通就直接返回此连接进行监听。而Redis的消息发送是在本地发送的。因此默认只能监听到集群中一台机器发送的消息。

    本地发送解释:例如有三个主机01,02,03。此时如果有个set键buxuewushu落到了主机01上,那么此消息就会通过01这台主机发送,因此如果此时服务监听的02机器,那么这个消息就会监听不到。

    本地发送

    解决办法

    既然我们知道了在集群条件下,每次监听只会随机取一个端口进行监听。那么我们就自己写监听机制,监听集群条件下的所有主机的端口就行了。

    我们可以看到在SpringData中提供了RedisMessageListenerContainer类来与Redis服务器进行通信。
    此类中有个start方法,可以看到是建立了与Redis的异步通信操作。所以我们的改造点就放在这就行。思路如下。

    • 程序启动时,获得集群的配置信息
    • 根据集群配置的Master数配置相同的RedisMessageListenerContainer进行监听

    主要代码如下

     public void onApplicationEvent(ContextRefreshedEvent contextRefreshedEvent) {
            RedisClusterConnection redisClusterConnection = redisConnectionFactory.getClusterConnection();
            if (redisClusterConnection != null) {
                Iterable<RedisClusterNode> nodes = redisClusterConnection.clusterGetNodes();
                for (RedisClusterNode node : nodes) {
                    if (node.isMaster()) {
                        String containerBeanName = "messageContainer" + node.hashCode();
                        if (beanFactory.containsBean(containerBeanName)) {
                            return;
                        }
                        JedisConnectionFactory factory = new JedisConnectionFactory(
                                new JedisShardInfo(node.getHost(), node.getPort()));
                        BeanDefinitionBuilder containerBeanDefinitionBuilder = BeanDefinitionBuilder
                                .genericBeanDefinition(RedisMessageListenerContainer.class);
                        containerBeanDefinitionBuilder.addPropertyValue("connectionFactory", factory);
                        containerBeanDefinitionBuilder.setScope(BeanDefinition.SCOPE_SINGLETON);
                        containerBeanDefinitionBuilder.setLazyInit(false);
                        beanFactory.registerBeanDefinition(containerBeanName,
                                containerBeanDefinitionBuilder.getRawBeanDefinition());
    
                        RedisMessageListenerContainer container = beanFactory
                                .getBean(containerBeanName, RedisMessageListenerContainer.class);
                        String listenerBeanName = "messageListener" + node.hashCode();
                        if (beanFactory.containsBean(listenerBeanName)) {
                            return;
                        }
                        container.addMessageListener(messageListener, new PatternTopic("__key*__:*"));
                        container.start();
                    }
                }
            }
        }
    
    

    此时我们再启动程序,还是在Redis中如下的输入

    127.0.0.1:7002> set blog0 buxuewushu
    -> Redirected to slot [6155] located at 127.0.0.1:7001
    OK
    127.0.0.1:7001> set blog1 buxuewushu
    -> Redirected to slot [2090] located at 127.0.0.1:7000
    OK
    127.0.0.1:7000> set blog2 buxuewushu
    -> Redirected to slot [14409] located at 127.0.0.1:7002
    OK
    127.0.0.1:7002> set blog3 buxuewushu
    -> Redirected to slot [10344] located at 127.0.0.1:7001
    OK
    127.0.0.1:7001> set blog4 buxuewushu
    OK
    127.0.0.1:7001> set blog5 buxuewushu
    -> Redirected to slot [2222] located at 127.0.0.1:7000
    OK
    
    

    这时我们可以看到在程序中我们接收到了所有端口的信息了。

    c.e.s.r.KeyExpiredEventMessageListener   : 监听到的信息:__keyspace@0__:blog0,值是:set
    c.e.s.r.KeyExpiredEventMessageListener   : 监听到的信息:__keyspace@0__:blog1,值是:set
    c.e.s.r.KeyExpiredEventMessageListener   : 监听到的信息:__keyspace@0__:blog2,值是:set
    c.e.s.r.KeyExpiredEventMessageListener   : 监听到的信息:__keyspace@0__:blog3,值是:set
    c.e.s.r.KeyExpiredEventMessageListener   : 监听到的信息:__keyspace@0__:blog4,值是:set
    c.e.s.r.KeyExpiredEventMessageListener   : 监听到的信息:__keyspace@0__:blog5,值是:set
    
    

    此时相当于我们建立了三个连接来监听三个redis服务器发送的消息。

    image

    小贴士:模式能匹配通配符,例如__keyspace@0__:blog*表示只接收blog开头的key值的信息,其他key值信息不接收

    完整代码

    相关文章

      网友评论

        本文标题:解决Redis集群条件下键空间通知服务器接收不到消息的问题

        本文链接:https://www.haomeiwen.com/subject/ezadnqtx.html