视频链接:https://www.bilibili.com/video/BV1cb4y1o7zz/?spm_id_from=333.337.search-card.all.click&vd_source=9545770e4a2968c05878ffac8589ec6c
视频选集:P39— P70
1.交换机【Exchanges】
在上一节中,创建了一个工作队列。假设的是工作队列背后,每个任务都恰好交付给一个消费者(工作进程)。在这一部分中,将做一些完全不同的事情:将消息传达给多个消费者。这种模式称为“发布/订阅".
为了说明这种模式,我们将构建一个简单的日志系统。它将由两个程序组成:第一个程序将发出日志消息,第二个程序是消费者。其中我们会启动两个消费者,其中一个消费者接收到消息后把日志存储在磁盘,另外一个消费者接收到消息后把消息打印在屏幕上,事实上第一个程序发出的日志消息将广播给所有消费者者
1.1 概念
RabbitMQ消息传递模型的核心思想是:生产者生产的消息从不会直接发送到队列。实际上,通常生产者甚至都不知道这些消息传递传递到了哪些队列中。
相反,生产者只能将消息发送到交换机(exchange),交换机工作的内容非常简单,一方面它接收来自生产者的消息,另一方面将它们推入队列。交换机必须确切知道如何处理收到的消息。是应该把这些消息放到特定队列还是说把他们到许多队列中还是说应该丢弃它们。这就的由交换机的类型来决定。
交换机的类型:直接(direct)【路由类型】,主题(topic) ,标题(headers)【头类型】,扇出(fanout)【发布/订阅模式】
还有无名交换机:在本教程的前面部分我们对exchange一无所知,但仍然能够将消息发送到队列。之前能实现的原因是因为我们使用的是默认交换,我们通过空字符串(" ")进行标识。
第一个参数是交换机的名称。空字符串表示默认或无名称交换机:消息能路由发送到队列中其实是由routingKey(bindingkey)绑定key指定的,如果它存在的话
临时队列
之前的章节我们使用的是具有特定名称的队列还记得hello和ack_queue吗?)。队列的名称我们来说至关重要-我们需要指定我们的消费者去消费哪个队列的消息。
每当我们连接到Rabbit时,我们都需要一个全新的空队列,为此我们可以创建一个具有随机名称的队列,或者能让服务器为我们选择一个随机队列名称那就更好了。其次一旦我们断开了消费者的连接,队列将被自动删除。
1.2 绑定
binding其实是exchange和queue之间的桥梁,它告诉我们exchange和那个队列进行了绑定关系。比如说下面这张图告诉我们的就是X与Q1和Q2进行了绑定
绑定操作:
效果:
理论分析:【根据routingkey判断消息发给哪个队列,想发给谁就发给谁】
1.3 Fanout交换机【发布订阅模式】
Fanout这种类型非常简单。正如从名称中猜到的那样,它是将接收到的所有消息广播到它知道的所有队列中。系统中默认有些exchange类型
定义一个发送,两个接受方:
消息的接受【消费者】:
注意:两个消费者的代码都是一样的
消息的发送【生产者】:
注意:交换机在上面已经声明了,这里不需要再声明了
测试:
先启动两个消费者
启动发送者并发送消息
1.4 Direct交换机【路由模式】
上一节中的我们的日志系统将所有消息广播给所有消费者,对此想做一些改变,例如希望将日志消息写入磁盘的程序仅接收严重错误(errros),而不存储哪些警告(warning)或信息(info)日志消息避免浪费磁盘空间。Fanout这种交换类型并不能给我们带来很大的灵活性-它只能进行无意识的广播,在这里我们将使用direct这种类型来进行替换,这种类型的工作方式是,消息只去到它绑定的routingKey队列中去。
先定义两个消费者:【声明两个队列】
定义发送者:
1.5 Topics交换机【主题模式】
在上一个小节中,我们改进了日志记录系统。我们没有使用只能进行随意广播的fanout,交换机,而是使用了direct交换机,从而有能实现有选择性地接收日志。
尽管使用direct交换机改进了我们的系统,但是它仍然存在局限性-比方说我们想接收的日志类型有info.base和info.advantage,某个队列只想 info.base的消息,那这个时候direct就办不到了。这个时候就只能使用topic类型
1.5.1 要求
发送到类型是topic交换机的消息的routing_key不能随意写,必须满足一定的要求,它必须是一个单词列表,以点号分隔开。这些单词可以是任意单词,比如说:“stock.usd.nyse” , “nyse.vmw”,“quick.orange.rabbit”.这种类型的。当然这个单词列表最多不能超过255个字节。
在这个规则列表中,其中有两个替换符是大家需要注意的
*(星号)可以代替一个单词
#(井号)可以替代零个或多个单词
1.5.2 Topics匹配案例
绑定关系如下
Q1–>绑定的是:中间带orange带3个单词的字符串(*.orange.*)
Q2–>绑定的是:最后一个单词是rabbit的3个单词(*.*.rabbit);第一个单词是lazy的多个单词(lazy.#)
下面举例:
当队列绑定关系是下列这种情况时需要引起注意:
- 当一个队列绑定键是#,那么这个队列将接收所有数据,就有点像fanout 了
- 如果队列绑定键当中没有#和*出现,那么该队列绑定类型就是direct 了
1.5.3 消费者
1.5.4 生产者
1.5.4 测试
将两个消费者和一个发送者启动起来:
2.死信队列
死信:顾名思义就是无法被消费的消息,字面意思可以这样理解,一般来说,producer将消息投递到broker或者直接到queue里了,consumer 从 queue取出消息进行消费,但某些时候由于特定的原因导致queue中的某些消息无法被消费,这样的消息如果没有后续的处理,就变成了死信,有死信自然就有了死信队列。
应用场景:为了保证订单业务的消息数据不丢失,需要使用到RabbitMQ.的死信队列机制,当消息消费发生异常时,将消息投入死信队列中.还有比如说:用户在商城下单成功并点击去支付后在指定时间未支付时自动失效
2.1 死信来源
- 消息TTL过期
- 队列达到最大长度(队列满了,无法再添加数据到mq.中)
- 消息被拒绝(basic.reject或 basic.nack)并且requeue=false.
2.2 死信实战代码框架图
2.3 死信实战-消费者1
2.4 死信实战-生产者
全部运行后:
2.5 死信实战-消费者2
2.6 死信实战-队列达到最大长度
生产者中代码修改:【将时间限制去掉】
在C1消费者中修改代码:
注意:此时需要把原先队列删除因为参数改变了
重新运行C1后:
将发送者运行后:
2.7 死信实战-消息被拒
在C1中进行修改:
运行效果:
3.延迟队列
3.1 概念
延时队列,队列内部是有序的,最重要的特性就体现在它的延时属性上,延时队列中的元素是希望在指定时间到了以后或之前取出和处理,简单来说,延时队列就是用来存放需要在指定时间被处理元素的队列。
3.2 使用场景
1.订单在十分钟之内未支付则自动取消
2.新创建的店铺,如果在十天内都没有上传过商品,则自动发送消息提醒。
3.用户注册成功后,如果三天内没有登陆则进行短信提醒。
4.用户发起退款,如果三天内没有得到处理则通知相关运营人员。
5.预定会议后,需要在预定的时间点前十分钟通知各个与会人员参加会议
这些场景都有一个特点,需要在某个事件发生之后或者之前的指定时间点完成某一项任务,如:发生订单生成事件,在十分钟之后检查该订单支付状态,然后将未支付的订单进行关闭;看起来似乎使用定时任务,一直轮询数据,每秒查一次,取出需要被处理的数据,然后处理不就完事了吗?如果数据量比较少,确实可以这样做,比如:对于“如果账单一周内未支付则进行自动结算”这样的需求,如果对于时间不是严格限制,而是宽松意义上的一周,那么每天晚上跑个定时任务检查一下所有未支付的账单,确实也是一个可行的方案。但对于数据量比较大,并且时效性较强的场景,如:“订单十分钟内未支付则关闭“,短期内未支付的订单数据可能会有很多,活动期间甚至会达到百万甚至千万级别,对这么庞大的数据量仍旧使用轮询的方式显然是不可取的,很可能在一秒内无法完成所有订单的检查,同时会给数据库带来很大压力,无法满足业务要求而且性能低下。
3.3 整合springboot
创建项目+添加依赖+修改配置文件+添加swagger配置类
3.4 RabbitMQ中的TTL
创建两个队列QA和QB,两者队列TTL分别设置为10S和40S,然后在创建一个交换机×和死信交换机Y,它们的类型都是direct,创建一个死信队列QD,它们的绑定关系如下:
队列TTL代码架构图:
3.5 队列TTL
3.5.1 配置类代码
在配置文件中创建配置文件类:
3.5.2 生产者
3.5.3 消费者
3.5.4 测试
4.延时队列优化
上面代码问题:如果这样使用的话,岂不是每增加一个新的时间需求,就要新增一个队列,这里只有10S和40S两个时间选项,如果需要一个小时后处理,那么就需要增加TTL为一个小时的队列,如果是预定会议室然后提前通知这样的场景,岂不是要增加无数个队列才能满足需求?
4.1 代码架构图
在这里新增了一个队列QC,绑定关系如下,该队列不设置TTL时间
4.2 配置类
在原始配置类中加入:
4.3 生产者
在上面的生产者代码中添加下面的内容:
4.4 基于死信存在问题
先测试上面代码:
看起来似乎没什么问题,但是在最开始的时候,就介绍过如果使用在消息属性上设置TTL的方式,消息可能并不会按时“死亡“,因为RabbitMQ只会检查第一个消息是否过期,如果过期则丢到死信队列,如果第一个消息的延时时长很长,而第二个消息的延时时长很短,第二个消息并不会优先得到执行。
4.5 基于插件的
在官网上下载https://www.rabbitmq.com/community-plugins.html
- 下载rabbitmq_delayed_message_exchange插件,
- 然后解压放置到RabbitMQ的插件目录。
- 进入RabbitMQ的安装目录下的plgins.目录,
/usr/ib/rabbitmq/lib/rabbitmq _server-3.8.8/plugins
执行下面命令让该插件生效
rabbitmq-plugins enable rabbitmq_delayed_message_exchange - 然后重启RabbitMQ
基于死信:
基于插件:
在这里新增了一个队列delayed.queue,一个自定义交换机delayed.exchange,绑定关系如下:
4.5.1 配置类
4.5.2 生产者
4.5.3 消费者
4.5.4 测试
4.6 总结
延时队列在需要延时处理的场景下非常有用,使用RabbitMQ.来实现延时队列可以很好的利用RabbitMQ的特性,如:消息可靠发送、消息可靠投递、死信队列来保障消息至少被消费一次以及未被正确处理的消息不会被丢弃。另外,通过RabbitMQ.集群的特性,可以很好的解决单点故障问题,不会因为单个节点挂掉导致延时队列不可用或者消息丢失。
当然,延时队列还有很多其它选择,比如利用Java的DelayQueue,利用Redis的zset,利用Quartz或者利用kafka的时间轮,这些方式各有特点,看需要适用的场景