一、pubsub底层数据结构
Redis中的发布和订阅功能允许服务器向指定的频道发送消息,以及客户端可以订阅感兴趣的频道来接收消息。发布和订阅功能的实现主要由如下几个命令实现:
publish :用于服务器向指定的频道发送消息,格式为:publish channel message
subscribe:用于客户端订阅服务器指定具体名字的频道,格式为: subscribe channel_name
psubscribe:用于客户端订阅服务器指定匹配模式的频道,格式为:psubscribe channel_apttern。
实际上订阅服务器指的匹配模式,可以简单看成 channel_apttern对channel_name的模糊匹配,比如:订阅以 news_开头的channel。如果我向news_sports、news_musice、news_book这几个channel中发送消息,那么订阅了news_的客户端 就是收到这3个 channel的消息。
发布和订阅功能相关的状态都保存在RedisServer中的pubsub_channels和pattern两个字段中:
struct redisServer{
// ...
dict *pubsub_channels; // 保存所有订阅的频道关系
list *pattern; // 保存所有订阅的频道的匹配模式
// ....
}
其中dict类型的pubsub_channels保存所有订阅的频道关系,key就是对应的频道名,value就是所有订阅该频道的客户端。由于订阅频道的客户端可能有多个,这里采用了链表的形式进行保存。如下所示:
这种结构类似于hashMap。
1、channel的订阅与退订
当客户端发不一个不存的channel中发送服务时,会返回0。说明这个channel不存在
10.0.1.1:6379> publish "test_tttt" "testtet"
(integer) 0
当客户端订阅某个频道时,如果pubsub_channels中已经有了该频道名,说明该频道已经有订阅者,将其直接添加到订阅者链表的尾部即可。如果pubsub_channels并没有指定的频道名,需要先将频道添加到pubsub_channels中,然后该客户端作为链表的头节点存在。
向已经有客户端订阅的channel中发送消息,当消息发送成功后,返回1。
10.0.1.1:6379> publish "news_it" "redis good"
(integer) 1
退订频道和订阅频道的动作相反,如果某个客户端想要退订某个频道,服务器会遍历pubsub_channels,找到对应频道的订阅者链表,将其从链表中删除。此外,如果该客户端是这个频道的唯一订阅者,删除订阅者后,还需要将该频道从pubsub_channels中删除。
2、模式订阅与退订
模式的订阅和退订实现与频道的订阅和退订实现类似,RedisServer的pubsub_patterns维护一个由pubsubPattern项组成的链表,其中pubsubPattern又包含client和pattern两个属性,用于表示具体的客户端和订阅的模式。
struct pubsubPattern{
redisClient client;
robj *pattern;
}
当客户端订阅某个模式时,服务器会创建一个pubsubPattern结构,将其pattern属性设置为被订阅模式,最后将其添加到链表的尾部。如果客户端退订某个模式时,服务器会在链表中找到对应的pubsubPattern结构,并删除这些结构。
3、向channel发送消息
当服务器对某个频道发布消息时,需要执行如下的两个动作:
在pubsub_channels中找到具体的频道,向订阅者列表中的每个客户端发送消息
遍历pubsub_patterns找到匹配的模式,然后向对应的客户端发送消息
Redis提供了PUBSUB命令来用于服务器获取订阅信息,其中又分为如下的三个子命令:
获取服务器当前被订阅的频道,格式为:PUBSUB CHANNELS [pattern]
返回指定频道的订阅者数量,格式为:PUBSUB NUMSUB [channel-1 channel-2 ... channel-n]
获取服务器当前被订阅模式的数量,格式为:PUBSUB NUMPAT
如果所有客户端与redis断开连接,那么客户端订阅的这个channel就会被删除,相当于所有客户端退订了channel。
获取所有channels
10.0.0.1:6379> pubsub channels
1) "news_11"
2) "news_22"
3) "news_33"
4) "musics_11"
获取模式cache的channels
10.0.0.1:6379> pubsub channels cache*
1) "cache:11"
2) "cache:ma:1"
3) "cache:bill:2"
4) "cache:customer:3"
5) "cache:ma:5"
6) "cache:999"
获这个channel所有订阅者数量
10.0.0.1:6379> pubsub numsub cache:11
1) "cache:11"
2) (integer) 3
二、发布订阅中的一些其它注意
1、一些需要注意的命令
1、如果向一个不存在的channel中发送消息,那么会返回0,说明这个channel不存在,发送消息失败。没有客户端订阅这个channel。
2、当所有客户端断开redis连接,那么就相当于退订阅了它所订阅的channel。所有之前订阅了一个channel的客户端与redis断开连接,那么相当于所有客户端退订了这个channel,这个channel就会被删除。
3、新订阅的客户端,是无法收到这个频道之前的消息,这是因为 Redis 并不会对发布的消息持久化的。
相比于很多专业 MQ,比如 kafka、rocketmq 来说, redis 发布订阅功能就显得有点简陋了。不过 redis 发布订阅功能胜在简单,如果当前场景可以容忍这些缺点,还是可以选择使用的。
2、原生jedis.subscribe 是一个阻塞的方法
不过需要注意的是,jedis#subscribe 是一个阻塞方法,调用之后将会阻塞主线程的,所以如果需要在正式项目使用需要使用异步线程运行,这里就不演示具体的代码了。
可基于基于 Spring-Data-Redis 开发发布订阅
三、redis发布订阅的应用
1、当订单支付成功后
使用 Redis 发布订阅这种机制,对于上面业务,下单支付业务只需要向支付结果这个频道发送消息,其他下游业务订阅支付结果这个频道,就能收相应消息,然后做出业务处理即可。
这样就可以解耦系统上下游之间调用关系。
1、Redis Sentinel 节点发现
Redis Sentinel 是 Redis 一套高可用方案,可以在主节点故障的时候,自动将从节点提升为主节点,从而转移故障。
Redis Sentinel 节点主要使用发布订阅机制,实现新节点的发现,以及交换主节点的之间的状态。
如下所示,每一个 Sentinel 节点将会定时向 sentinel:hello 频道发送消息,并且每个 Sentinel 都会订阅这个节点。
每次往这个频道发送消息内容可以包含节点的状态信息,这样可以作为后面 Sentinel 领导者选举的依据。
通过订阅实例的HELLO频道,接收其他哨兵通过”PUBLISH”命令发布的信息,从而得到监控同一主节点的所有其他哨兵的信息。
以上都是对于 Redis 服务端来讲,对于客户端来讲,我们也可以用到发布订阅机制。
2、当sentinel完成故障转移,选出新master时通知各个客户端
对于我们客户端来讲,比较关心切换之后的主节点,这样我们及时切换主节点的连接(旧节点此时已故障,不能再接受操作指令),
客户端可以订阅 +switch-master频道,一旦 Redis Sentinel 结束了对主节点的故障转移就会发布主节点的的消息。
3、redission 分布式锁用了发布订阅
今天我们来看下 Redis 的实现分布式锁中如何使用 Redis 发布订阅机制,提高加锁的性能。
首先我们来看下 redission 加锁的方法:
RedissonLock redisson = ...
RLock redissonLock = redisson.getLock("xxx");
redisson.lock();
RLock
继承自 Java
标准的 Lock接口
,调用 lock 方法
,如果当前锁已被其他客户端获取,那么当前加锁的线程将会被阻塞,直到其他客户端释放这把锁。
3.1、其它客户端如何感知锁被释放
这里其实有个问题,当前阻塞的线程如何感知分布式锁已被释放呢?
这里其实有两种实现方法:
第一种:定时查询分布式锁的状态,一旦查到锁已被释放(redis中不存在这个键值),那么就去加锁。实现伪代码如下:
while (true){
boolean result = lock();
if(!result){
Thread.sleep(N);
}
}
这种方式实现起来起来简单,不过缺点也比较多。
如果定时任务时间过短,将会导致查询次数过多,其实这些都是无效查询。
如果定时任务休眠时间过长,那又会导致加锁时间过长,导致加锁性能不好。
第二种实现方案,就是采用服务通知的机制,当分布式锁被释放之后,客户端可以收到锁释放的消息,然后第一时间再去加锁。
这个服务通知的机制我们可以使用 Redis 发布订阅模式。
当线程加锁失败之后,线程将会订阅 redisson_lock__channel_xxx
(xx 代表锁的名称) 频道,使用异步线程监听消息,然后利用 Java 中 Semaphore 使当前线程进入阻塞。
一旦其他客户端进行解锁,redission
就会往这个redisson_lock__channel_xxx
发送解锁消息。
等异步线程收到消息,将会调用 Semaphore
释放信号量,从而让当前被阻塞的线程唤醒去加锁。
通过发布订阅机制,被阻塞的线程可以及时被唤醒,减少无效的空转的查询,有效的提高的加锁的效率。
redisson获锁源码查看另一篇文章Redisson框架实现Redis分布式锁的实现原理