前言
上一篇介绍了 Redis 是如何进行通信的。这节开始介绍一个常见的具体通信例子:消息队列。
消息队列(Message Queue)是一种常见的通信方式,它是一种异步通信模式,主要由发送者(也称生产者)、消息队列、接收者(也称消费者)组成,发送者负责将消息发送到消息队列中,消息队列负责对消息进行存储,接收者负责从消息队列中取出消息并进行处理。
乍一看好像很简单,但是想要实现一个稳定的消息队列,有很多细节都需要保证,比如以下三个基本要求:
- 消息保序:消费者需要按照生产者发送消息的顺序来处理信息;
- 处理重复消息:如果因为网络堵塞而出现消息重传,对于重复的消息只能处理一次;
- 保证消息可靠性:如果因为故障导致消息没有处理完成,需要保证消息可靠性执行;
除此之外,还需要有很多扩展功能以应对不同的应用场景,所以小小的一个消息队列实现起来就成了一个小系统了。
市面上有很多成熟的消息队列系统,RabbitMQ、Kafka、ActiveMQ 等,Redis 也提供了三种消息队列的实现方式,来应对不同的场景。接下来我将简单介绍一下这三种实现方式:基于链表实现、发布订阅机制、Stream。
基于链表实现
参考消息队列的工作模式,最简单的实现方式就是使用链表来实现了,只需要用一个链表来存储数据,然后发送者在链表一端添加数据,而消费者在另一端取出数据即可。
为了解决重复消息的问题,可以给每一个数据添加一个全局唯一标识,来判断该消息是否已经处理过;为了保证消息的可靠性,可以开启 Redis 的持久化(后面会细说)。
可见,基于链表实现消息队列非常方便,而且可以满足于许多简单的应用场景。但是还有几个明显的问题:
- 多个消费者不能消费同一条消息,一个消息消费完就会被删除;
- 如果生产者消息发送很快,而消费者处理消息的速度比较慢,这就导致链表中的消息越积越多,给内存带来很大压力。
发布/订阅机制
为了解决上述提到的两个问题,Redis 实现了发布/订阅(pub/sub)机制,允许多个客户端通过订阅特定的频道来接收消息。
Redis 的 SUBSCRIBE 命令可以让客户端订阅任意数量的频道, 每当有新信息发送到被订阅的频道时, 信息就会被发送给所有订阅指定频道的客户端。
一旦客户端订阅了某个频道,它就会一直监听该频道,直到取消订阅或连接断开。在这个过程中,Redis 实例和客户端会保持一个 TCP 长连接用于传递消息。
基于频道的发布/订阅
发布者可以向指定的频道(channel)送消息;订阅者可以订阅一个或者多个频道,所有订阅此频道的订阅者都会收到此消息。
发布者发布消息的命令是 publish,用法是 publish channel message,该命令的返回值表示接收这条消息的订阅者数量。
订阅频道的命令是 subscribe,用法是 subscribe channel1 [channel2 …],客户端进入订阅状态后,只能使用 subscribe
、unsubscribe
、psubscribe
、punsubscribe
这四个属于"发布/订阅"的命令,否则会报错。
subscribe 订阅的频道只能通过 unsubscribe 取消订阅;unsubscribe 如果没有参数会取消订阅所有频道;
psubscribe/punsubscribe 表示订阅/取消“订阅模式”,也就是下文将会介绍的基于模式的发布/订阅;
进入订阅状态后客户端可能收到 3 种类型的回复,每种类型的回复都包含 3 个值:
- subscribe:表示订阅成功的反馈信息。第二个值是订阅成功的频道名称,第三个是当前客户端订阅的频道数量。
- message:表示接收到的消息,第二个值表示产生消息的频道名称,第三个值是消息的内容。
- unsubscribe:表示成功取消订阅某个频道。第二个值是对应的频道名称,第三个值是当前客户端订阅的频道数量,当此值为0时客户端会退出订阅状态,之后就可以执行其他非"发布/订阅"模式的命令了。
具体如下所示:
127.0.0.1:6379> subscribe test
1) "subscribe" #订阅成功
2) "test" #订阅成功的频道
3) (integer) 1 #当前客户端订阅的频道数量
#当发布者发布消息 publish test Hello,订阅者读取到的消息如下
1) "message" #收到消息
2) "test" #产生消息的频道
3) "Hello" #消息内容
实现
底层是通过字典实现的,这个字典(pubsub_channels)就用于保存订阅频道的信息:字典的键为正在被订阅的频道, 而字典的值则是一个链表, 链表中保存了所有订阅这个频道的客户端。
频道订阅:订阅频道时先检查字段内部是否存在;不存在则为当前频道创建一个字典且创建一个链表存储客户端 ID;否则直接将客户端 ID 插入到链表中。
取消频道订阅:取消时将客户端 ID 从对应的链表中删除;如果删除之后链表已经是空链表了,则将会把这个频道从字典中删除。
发送数据:频道接收到消息后,会遍历链表,然后将消息推送给所有订阅该频道的客户端。
基于模式的发布/订阅
除了按照频道名订阅频道外,发布/订阅机制还支持基于模式的发布/订阅,即允许消费者通过指定一个模式来订阅多个频道。模式可以匹配通配符,例如 “?” 表示 1 个占位符,“*” 表示任意个占位符,“?*” 表示 1 个以上占位符。
具体使用方法与订阅频道时类似,只是命令前面加上了 p
,如 psubscribe/punsubscribe 表示订阅/取消订阅频道。
使用 psubscribe 命令可以重复订阅同一个频道,如客户端执行了 psubscribe c? c?*
。这时向 c1 发布消息客户端会接受到两条消息。同样的,如果有另一个客户端执行了 subscribe c1
和 psubscribe c?*
的话,向 c1 发送一条消息该客户顿也会受到两条消息(但是是两种类型:message 和 pmessage)同时 publish 命令也返回 2。
通过订阅模式接收到的信息, 和通过订阅频道接收到的信息的格式不太一样:
- pmessage :表示接收到的信息,第二个值表示被匹配的频道的名字,第三个值表示信息的实际内容。
实现
底层是 pubsubPattern 节点的链表。
typedef struct pubsubPattern {
redisClient *client; //订阅模式的客户端
robj *pattern; //被订阅的模式
} pubsubPattern;
模式订阅:新增一个 pubsubPattern 数据结构添加到链表的最后尾部,同时保存客户端 ID。
取消模式订阅:从当前的链表 pubsubPatterns 结构中删除需要取消的模式订阅。
发送数据:当发布者发送消息时,会遍历所有的订阅模式进行模式匹配,再向匹配的客户端发送数据。如果订阅的模式数量很多,会耗费很多的计算资源,降低性能。
Redis 的发布/订阅机制,相较于链表实现,有以下优点:
- 实现了多对多的消息传递:一个消息可以被多个订阅者接收,一个订阅者也可以订阅多个频道;
- 推送消息:Redis 实现的推送消息,可以让订阅者不用轮询等待消息,避免了不必要的开销;
但是仍然存在着一些缺陷:依靠 Redis 自身的持久化不可靠、无法重复消费消息、无法实现历史消息、无法保证消息的传递等等。
Stream
之前在 Redis 的九大数据类型中就介绍过 Stream,是专门为消息队列设计的数据结构,具体的就不在这里再赘述了,总得来说就是 Stream 支持消息的持久化、自动生成全局唯一 ID、ack 确认消息的模式、消费组模式等,让消息队列更加的稳定和可靠。
但是 Stream 实现的消息队列与 Kafka、RabbitMQ 等专业的消息队列系统相比,仍然有一些缺陷:无法解决消息堆积的问题、无法保证消息的可靠性、不能支持一些如消息转发、过滤等高级功能。
但是,任何实现方式都有其优点和缺点,我们应该根据实际的业务场景来选择最合适的实现方式。
最后
本文介绍了 Redis 中的三种实现消息队列的方式。下一节将介绍 Redis 是如何实现数据可靠性的,即 Redis 的持久化。