解决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的集群。

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机器,那么这个消息就会监听不到。

image.png

解决办法

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

我们可以看到在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.png

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

完整代码

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 162,306评论 4 370
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 68,657评论 2 307
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 111,928评论 0 254
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,688评论 0 220
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 53,105评论 3 295
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 41,024评论 1 225
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 32,159评论 2 318
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,937评论 0 212
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,689评论 1 250
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,851评论 2 254
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,325评论 1 265
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,651评论 3 263
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,364评论 3 244
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,192评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,985评论 0 201
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 36,154评论 2 285
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,955评论 2 279

推荐阅读更多精彩内容