解決Redis叢集條件下鍵空間通知伺服器接收不到訊息的問題

不學無數的程式設計師發表於2019-04-28

解決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伺服器傳送的訊息。

解決Redis叢集條件下鍵空間通知伺服器接收不到訊息的問題

小貼士:模式能匹配萬用字元,例如__keyspace@0__:blog*表示只接收blog開頭的key值的資訊,其他key值資訊不接收

完整程式碼

相關文章