解決Redis叢集條件下鍵空間通知伺服器接收不到訊息的問題
鍵空間通知介紹
鍵空間通知使得客戶端可以通過訂閱頻道或模式, 來接收那些以某種方式改動了 Redis 資料集的事件。
可以通過對redis的redis.conf
檔案中配置notify-keyspace-events
引數可以指定伺服器傳送哪種型別的通知。下面對於一些引數的描述。預設情況下此功能是關閉的。
字元 | 通知 |
---|---|
K |
鍵空間通知,所有通知以 __keyspace@<db>__ 為字首 |
E |
鍵事件通知,所有通知以 __keyevent@<db>__ 為字首 |
g |
DEL 、 EXPIRE 、 RENAME 等型別無關的通用命令的通知 |
$ |
字串命令的通知 |
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
複製程式碼
我們看到只列印了blog
、blog1
、blog4
的鍵,而我們通過上面觀察,列印的鍵都是分佈在7001埠上的。因此我們預測程式只是監聽了7001埠傳送的訊息。而通過N次測試,程式不是每次都在監聽7001埠,而是隨機的。但是每次只會監聽一個埠。
問題所在
接下來讓我們通過找尋原始碼,看看到底是哪出的問題。
JedisSlotBasedConnectionHandler
的getConnection
方法中
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伺服器傳送的訊息。
小貼士:模式能匹配萬用字元,例如
__keyspace@0__:blog*
表示只接收blog開頭的key值的資訊,其他key值資訊不接收