MQ类型:根据consumer消费的流程,分为push和pull
push:当producer将消息投递到mq,当mq负责将消息以推送的形式主动发送给了各个建立了订阅关系的消费方 优势:流程实时性强,消息来了就执行推送;契合发布订阅模式 劣势:对consumer保护力度不够
pull:当mq存在消息时,由consumer主动执行拉取消息的操作 优势:下游主动权,选择在和合适时机执行消费操作 劣势:实时性弱,和主动pull的轮询机制有关
redis实现mq的问题
存储昂贵:基于内存
数据丢失:redis存储消息时会不可避免的存在数据丢失的风险 1.内存是易失性存储,rdb/aof之类的持久化机制弥补,但这个流程是异步执行无法提供百分百的保证力度;2.redis走的是ap高可用,数据的主从复制流程是异步执行的,主从切换时数据存在弱一致性问题
Redis-list实现
key:消息的topic名称
producer投递消息时,使用lpush,对应o(1),
lpush my_list_topic msg
consumer使用rpop接受,
rpop my_list_topic
"msg"
问题1:consumer的轮询消费流程如何组织
基于list的mq属于pull类型,在合适的时机rpop实现消息的主动拉取
首先这种消费一定是一个类似loop thread的自旋模型,rpop是非阻塞的,没有list数据也会返回一个nil响应
- 倘若rop捕捉到nil时,立即开始下一轮循环,这个轮询行为是无意义的,造成cpu的无畏损耗
- 倘若让consumer休眠一段时间再进行循环,休眠时长又具有一定的人为误判性。太短浪费cpu,太长消息处理不及时
理想方案:在list有数据到达,令consumer即时获取到对应结果,倘若list空,则consumer陷入阻塞等待的状态,直到有数据抵达程序才被唤醒 使用redis中的brpop替代rpop,做到有数据踩返回响应,否则令程序陷入阻塞:
brpop my_list_topic 0
"my_list_topic"
"msg"
局限性:
- 无法支持发布/订阅模式:list数据是独一份的,pop后不复存在
- 无法支持consumer的ack:consumer通过brpop获取到数据后,倘若发生宕机不能告诉mq一个消息处理失败的反馈,这时候消息就真的丢失令
解决无法支持发布/订阅模式的问题,redis提供了pub/sub机制,有效弥补这方面的缺陷(下面吧producer/consumer称呼为publisher/subscriber)
实现上,pub/sub会在两者之间建立一个用于实时通过奴性能的信道--channel,在传递消息时,会根据channel查到所有建立过订阅关系的subscriber,将消息一一送达
subscriber通过subscribe指令建立对某个channel的订阅关系:
subscribe my_channel_topic
Reading messages...
"subscribe"
"my_channel_topic"
"(integer)1"
publisher通过publish指令在对应的channel中执行消息投递操作
publish my_channel_topic
(integer) 1
通过这种模式,消费者通过subscribe指令会对channel采用阻塞模型进行监听,只有在消息到来,才会从阻塞状态中被唤醒。
执行过程:
- 首先,消费方 subscriber 通过 subscribe 指令建立和指定 channel 之间的订阅关系. 这时在 redis 中会维护好 channel 和对应 subscriber 列表的映射关系,并在内存中为每个在线活跃的 subscriber 分配好一个缓冲区 buffer,用以承载后续到来的消息数据
- 接下来随着 publisher 执行 publish 指令,往对应 channel 中投递消息后,此时 redis 会实时查看 channel 对应 subscriber 名单,往每个 subscriber 的缓冲区 buffer 中推送这条数据
- 各执行了 subscribe 指令的 subscriber 会处于阻塞监听缓冲区 buffer 的状态,随着新数据到达,subscriber 会获取到这笔数据
基于这个流程,我们能看出来,pub/sub 对于 channel 以及 subscribers 之间的实时映射关系存在强依赖. 因此在操作的执行顺序上,我们需要保证先执行 subscribe 指令,再执行 publish 执行,否则前几笔 publish 投递的数据就会因为不存在 subscriber 而被直接丢弃.