尚硅谷RabbitMQ教程丨快速掌握MQ消息中间件_哔哩哔哩_bilibili
Rabbitmq学习笔记(尚硅谷2021)_尚硅谷rabbitmq笔记_江北残刀的博客-CSDN博客
RabbitMQ学习【尚硅谷】_尚硅谷rabbitmq_后晨的博客-CSDN博客
六大模式:
哔哩哔哩尚硅谷学习目录
1、mq的意义:流量削峰,异步处理,应用解耦
2、mq分类:RabbitMQ最常用
3、mq名称解释及安装
4、helloword模式
5、Work Queues模式 多个消费者共同工作
5.1 工作队列(又称任务队列)的主要思想是避免立即执行资源密集型任务,而不得不等待它完成。
相反我们安排任务在之后执行。我们把任务封装为消息并将其发送到队列。在后台运行的工作进
程将弹出任务并最终执行作业。当有多个工作线程时,这些工作线程将一起处理这些任务。
5.2 消息手动自动应答
(使用手动应答,默认自动,需要springboot配置或者springboot配置文件配置手动)
RabbitMQ生产者一旦向消费者发送完消息,便立即将该消
息标记为删除。在这种情况下,突然有个消费者挂掉了,我们将丢失正在处理的消息。以及后续
发送给该消费这的消息,因为它无法接收到。
Ready:待消费的消息总数。
Unacked:待应答(待确认)的消息总数。
Total:总数 Ready+Unacked。
消息应答概念:
保证消息在发送过程中不丢失,rabbitmq 引入消息应答机制,意思就是
消费者在接收到消息并且处理该消息之后,告诉 rabbitmq 它已经处理了,
rabbitmq 可以把该消息删除了。
springboot配置开启手动应答:
参考博客:https://blog.csdn.net/love_Saber_Archer/article/details/109111088
spring.rabbitmq.listener.simple.acknowledge-mode=manual
手动消息应答写法:下面的博客还有详解补充。(不在springboot中配置,直接编写下面的手动应答代码,也有手动应答效果)
Channel.basicAck(deliveryTag, true);
channel.basicReject(deliveryTag, true);
channel.basicNack(deliveryTag, false, true);
channel.basicRecover(true);
5.3 消息自动重新入队(重新入队 和 手动和自动应答是对应起来的)
(消息未发送 ACK 确认(肯定确认和否定确认),即消息应答,mq会将其重新排队,
如果此时其他消费者可以处理,它将很快将其重新分发给另一个消费者)
5.4 队列持久化/消息持久化(队列和消息交换机在springboot集成rabbitmq中都是默认持久化的看源码)
RabbitMQ在默认情况下,我们创建的消息队列以及存放在队列里面的消息,都是非持久化的,
也就是说当RabbitMQ宕掉了或者是重启了,创建的消息队列以及消息都不会保存,为了解决这种情况,
保证消息传输的可靠性,我们可以使用RabbitMQ提供的消息队列的持久化机制。
我们需要将队列和消息都标记为持久化。
5.5 不公平分发/预取值
不公平分发:(RabbitMQ 分发消息采用的轮训分发,但是在某种场景下这种策略并不是
很好,比方说有两个消费者在处理任务,其中有个消费者 1 处理任务的速度非常快,
而另外一个消费者 2 处理速度却很慢,这个时候我们还是采用轮训分发的化就会到这处理
速度快的这个消费者很大一部分时间处于空闲状态,而处理慢的那个消费者一直在干活,
这种分配方式在这种情况下其实就不太好,可以使用不公平分发)
预取值:希望开发人员能限制此缓冲区的大小,以避免缓冲区里面无限制的未确认消息问题
5.6 amqp-client自带的发布确认机制:
单个发布确认(消息发布出去,确认被发布了才能发布下一个)、
批量发布确认(发布消息一批消息后一起确认)、
异步发布确认(最优解) --发布确认默认是没有开启的
生产者将信道设置成 confirm 模式(springboot可以配置,见下方博客),一旦信道进入 confirm 模式,
所有在该信道上面发布的消息都将会被指派一个唯一的 ID(从 1 开始),
一旦消息被投递到所有匹配的队列之后,broker就会发送一个确认给生产者(包含消息的唯一 ID),
这就使得生产者知道消息已经正确到达目的队列了,如果消息和队列是可持久化的,
那么确认消息会在将消息写入磁盘之后发出,broker 回传给生产者的确认消息中 delivery-tag 域
包含了确认消息的序列号,此外 broker 也可以设置basic.ack 的 multiple 域,
表示到这个序列号之前的所有消息都已经得到了处理。confirm 模式最大的好处在于他是异步的,
一旦发布一条消息,生产者应用程序就可以在等信道返回确认的同时继续发送下一条消息,
当消息最终得到确认之后,生产者应用便可以通过回调方法来处理该确认消息,
如果 RabbitMQ 因为自身内部错误导致消息丢失,就会发送一条 nack 消息,
生产者应用程序同样可以在回调方法中处理该 nack 消息。
通过:
//开启发布确认
channel.confirmSelect();
//服务端返回 false 或超时时间内未返回,生产者可以消息重发
boolean flag = channel.waitForConfirms();
6、交换机类型(直接(direct), 主题(topic) ,标题(headers) , 扇出(fanout) )、无名交换机、临时队列、绑定
7、fanout 发布订阅模式 一对多
8、direct 路由模式 通过routing key路由 指定发送 (常用!)
9、topic 主题模式 topic模糊匹配(常用!)
当一个队列绑定键是#,那么这个队列将接收所有数据,就有点像 fanout 了
如果队列绑定键当中没有#和*出现,那么该队列绑定类型就是 direct 了
10、死信
应用场景:
用户在商城下单成功并点击去支付后在指定时间未支付时自动失效。
来源:
消息 TTL 过期
队列达到最大长度(队列满了,无法再添加数据到 mq 中)
消息被拒绝(basic.reject 或 basic.nack)并且 requeue=false.
11、延迟队列 (Rabbitmq 插件实现延迟队列最优解)
通常在应用开发中我们会碰到定时任务的需求,比如未付款订单,超过一定时间后,系统自动取消订单并释放占有物品。
许多同学的第一反应就是通过spring的schedule定时任务轮询数据库来实现,这种方案有一下几点劣势:
(1)消耗系统内存,由于定时任务一直在系统中占着进程,比较消耗内存
(2)增加了数据库的压力,这个提现在两方面,一是长时间占着数据库的连接,而是查询基数大
(3)存在较大的时间误差
如果我们利用rabbitmq来实现,就可以解决以上几种问题。
延迟队列场景:
1.订单在十分钟之内未支付则自动取消
2.新创建的店铺,如果在十天内都没有上传过商品,则自动发送消息提醒。
3.用户注册成功后,如果三天内没有登陆则进行短信提醒。
4.用户发起退款,如果三天内没有得到处理则通知相关运营人员。
5.预定会议后,需要在预定的时间点前十分钟通知各个与会人员参加会议
12、springboot和mq的结合(延迟队列)(!!!重点!!!)
13、RabbitMQ发布确认高级:springboot整合的rabbitMQ实现发布确认。
(Mq到交换机)消息到达交换机的监听,如果交换机不存在会调用回调方法,需要在配置文件中开启确认发布
(交换机到队列)监听消息路由到队列是否成功,需要在配置文件中配置消息回退机制。
https://blog.csdn.net/qq_42029989/article/details/122639491(消息确认机制--重要!!!)
https://blog.51cto.com/u_15127631/3283171 (rabbitmq消息发送失败处理--重要!)
https://baijiahao.baidu.com/s?id=1725051531179193433&wfr=spider&for=pc (发消息的时持久化到库中,设置发布确认的回调中修改状态,状态有误的重新发送)
springboot配置:
spring.rabbitmq.publisher-confirm-type=CORRELATED
spring.rabbitmq.publisher-returns=true
发布确认实现ConfirmCallback(mq到交换机)和ReturnCallback(交换机到队列)接口
(Mq到交换机) #启用交换机确认回调接口 spring.rabbitmq.publisher-confirm-type=correlated
生产者发布消息可能遇到交换机宕机,导致交换机无法收到消息,可以通过设置回调函数来处理,
发布消息成功到交换器或失败后都会触发回调方法,可以在回调方法中重新发送或者持久化用定时任务重发。
(交换机到队列) #开启发布回退 spring.rabbitmq.publisher-returns=true
如果交换机转发给队列,发现队列宕机,消息会丢弃,可以通过配置并通过回调函数回退消息给生产者,
确保消息发送失败后可以重新返回到队列中,如果消息发送失败会进入回调函数。
(回调函数中可以去判断具体错误),我们也可以在回调函数中重新发送或者持久化用定时任务重发。
Mq到交换机和交换机到队列如何持久化或者重新发送呢:
第一种:就是把发的每条消息存起来,指定id为correlationId,回调函数在ReturnedMessage的message和CorrelationData中都可以拿到id,发布失败时,通过id去查询库再重新发送或者持久化定时重发。
第二种:通过同一个格式发送,例如map,在confirm回调函数中参考上面博客2,
封装在correlationDataExt中,然后回调中去拿消息数据重新发送或者持久化重发;
在returnedMessage回调函数拿到message后转码为map重新发送或者持久化定时重发。
14、备份交换机
我们还可以直接备份交换机,让备份的交换机来消费这些消息进行报警,如果同时设置回退消息和备份交换机,
备份交换机会执行!备份交换机优先级高!
15、幂等性:(在消息中加入唯一id,如果重复消费:每次消费消息时用该 id 先判断该消息是否已消费过)
16:优先级队列:队列需要设置为优先级队列,消息需要设置消息的优先级,消费者需要等待消息已经发送到队列中才去消费,因为这样才有机会对消息进行排序
17:惰性队列:
队列具备两种模式:default 和 lazy。默认的为 default 模式,在 3.6.0 之前的版本无需做任何变更。
lazy模式即为惰性队列的模式,可以通过调用 channel.queueDeclare 方法的时候在参数中设置,
也可以通过Policy 的方式设置,如果一个队列同时使用这两种方式设置的话,
那么 Policy 的方式具备更高的优先级。
如果要通过声明的方式改变已有队列的模式的话,那么只能先删除队列,然后再重新声明一个新的。
在队列声明的时候可以通过“x-queue-mode”参数来设置队列的模式,取值为“default”和“lazy”。
下面示例中演示了一个惰性队列的声明细节:
Map<String, Object> args = new HashMap<String, Object>();
args.put("x-queue-mode", "lazy");
channel.queueDeclare("myqueue", false, false, false, args);
在发送 1 百万条消息,每条消息大概占 1KB 的情况下,普通队列占用内存是 1.2GB,
而惰性队列仅仅占用 1.5MB
18、RabbitMQ 集群 + 负载均衡
RabbitMQ基础概念:
Brocker:消息队列服务器实体。
Exchange:消息交换机,指定消息按什么规则,路由到哪个队列。
Queue:消息队列,每个消息都会被投入到一个或者多个队列里。
Binding:绑定,它的作用是把exchange和queue按照路由规则binding起来。
Routing Key:路由关键字,exchange根据这个关键字进行消息投递。
Virtual Host: 虚拟主机,一个broker里可以开设多个vhost,用作不用用户的权限分离。
每个virtual host本质上都是一个RabbitMQ Server(但是一个server中可以有多个virtual host),
拥有它自己若干的个Exchange、Queue和bings rule等等。其实这是一个虚拟概念,类似于权限控制组。
Virtual Host是权限控制的最小粒度。
Producer:消息生产者,就是投递消息的程序。
Consumer:消息消费者,就是接受消息的程序。
Connection: 就是一个TCP的连接。Producer和Consumer都是通过TCP连接到RabbitMQ Server的。
接下来的实践案例中我们就可以看到,producer和consumer与exchange的通信的前提是先建立TCP连接。
仅仅创建了TCP连接,producer和consumer与exchange还是不能通信的。
我们还需要为每一个Connection创建Channel。
Channel: 它是建立在上述TCP连接之上的虚拟连接。数据传输都是在Channel中进行的。AMQP协议规定只有通过
Channel才能执行AMQP的命令。一个Connection可以包含多个Channel。有人要问了,为什么要使用Channel呢,
直接用TCP连接不就好了么?对于一个消息服务器来说,它的任务是处理海量的消息,当有很多线程需要从RabbitMQ
中消费消息或者生产消息,那么必须建立很多个connection,也就是许多个TCP连接。然而对于操作系统而言,建立
和关闭TCP连接是非常昂贵的开销,而且TCP的连接数也有限制,频繁的建立关闭TCP连接对于系统的性能有很大的影
响,如果遇到高峰,性能瓶颈也随之显现。RabbitMQ采用类似NIO的做法,选择TCP连接服用,不仅可以减少性能开
销,同时也便于管理。在TCP连接中建立Channel是没有上述代价的,可以复用TCP连接。对于Producer或者
Consumer来说,可以并发的使用多个Channel进行Publish或者Receive。有实验表明,在Channel中,1秒可以
Publish10K的数据包。对于普通的Consumer或者Producer来说,这已经足够了。除非有非常大的流量时,一个
connection可能会产生性能瓶颈,此时就需要开辟多个connection。
------------------------------------------------------------------
逻辑过程:消息队列的使用过程大概如下:
消息接收:
客户端连接到消息队列服务器,打开一个channel。
客户端声明一个exchange,并设置相关属性。
客户端声明一个queue,并设置相关属性。
客户端使用routing key,在exchange和queue之间建立好绑定关系。
消息发送:
客户端投递消息到exchange。
exchange接收到消息后,就根据消息的key和已经设置的binding,进行消息路由,将消息投递到
一个或多个队列里。
springboot rabbitmq配置:
复制代码
spring.rabbitmq.host: 默认localhost
spring.rabbitmq.port: 默认5672
spring.rabbitmq.username: 用户名
spring.rabbitmq.password: 密码
spring.rabbitmq.virtual-host: 连接到代理时用的虚拟主机
spring.rabbitmq.addresses: 连接到server的地址列表(以逗号分隔),先addresses后host
spring.rabbitmq.requested-heartbeat: 请求心跳超时时间,0为不指定,如果不指定时间单位默认为妙
spring.rabbitmq.publisher-confirms: 是否启用【发布确认】,默认false
spring.rabbitmq.publisher-returns: 是否启用【发布返回】,默认false
spring.rabbitmq.connection-timeout: 连接超时时间,单位毫秒,0表示永不超时
复制代码
SSL:
复制代码
spring.rabbitmq.ssl.enabled: 是否支持ssl,默认false
spring.rabbitmq.ssl.key-store: 持有SSL certificate的key store的路径
spring.rabbitmq.ssl.key-store-password: 访问key store的密码
spring.rabbitmq.ssl.trust-store: 持有SSL certificates的Trust store
spring.rabbitmq.ssl.trust-store-password: 访问trust store的密码
spring.rabbitmq.ssl.trust-store-type=JKS:Trust store 类型.
spring.rabbitmq.ssl.algorithm: ssl使用的算法,默认由rabiitClient配置
spring.rabbitmq.ssl.validate-server-certificate=true:是否启用服务端证书验证
spring.rabbitmq.ssl.verify-hostname=true 是否启用主机验证
复制代码
缓存cache:
spring.rabbitmq.cache.channel.size: 缓存中保持的channel数量
spring.rabbitmq.cache.channel.checkout-timeout: 当缓存数量被设置时,从缓存中获取一个channel的超时时间,单位毫秒;如果为0,则总是创建一个新channel
spring.rabbitmq.cache.connection.size: 缓存的channel数,只有是CONNECTION模式时生效
spring.rabbitmq.cache.connection.mode=channel: 连接工厂缓存模式:channel 和 connection
Listener:
复制代码
spring.rabbitmq.listener.type=simple: 容器类型.simple或direct
spring.rabbitmq.listener.simple.auto-startup=true: 是否启动时自动启动容器
spring.rabbitmq.listener.simple.acknowledge-mode: 表示消息确认方式,其有三种配置方式,分别是none、manual和auto;默认auto
spring.rabbitmq.listener.simple.concurrency: 最小的消费者数量
spring.rabbitmq.listener.simple.max-concurrency: 最大的消费者数量
spring.rabbitmq.listener.simple.prefetch: 一个消费者最多可处理的nack消息数量,如果有事务的话,必须大于等于transaction数量.
spring.rabbitmq.listener.simple.transaction-size: 当ack模式为auto时,一个事务(ack间)处理的消息数量,最好是小于等于prefetch的数量.若大于prefetch, 则prefetch将增加到这个值
spring.rabbitmq.listener.simple.default-requeue-rejected: 决定被拒绝的消息是否重新入队;默认是true(与参数acknowledge-mode有关系)
spring.rabbitmq.listener.simple.missing-queues-fatal=true 若容器声明的队列在代理上不可用,是否失败; 或者运行时一个多多个队列被删除,是否停止容器
spring.rabbitmq.listener.simple.idle-event-interval: 发布空闲容器的时间间隔,单位毫秒
spring.rabbitmq.listener.simple.retry.enabled=false: 监听重试是否可用
spring.rabbitmq.listener.simple.retry.max-attempts=3: 最大重试次数
spring.rabbitmq.listener.simple.retry.max-interval=10000ms: 最大重试时间间隔
spring.rabbitmq.listener.simple.retry.initial-interval=1000ms:第一次和第二次尝试传递消息的时间间隔
spring.rabbitmq.listener.simple.retry.multiplier=1: 应用于上一重试间隔的乘数
spring.rabbitmq.listener.simple.retry.stateless=true: 重试时有状态or无状态
spring.rabbitmq.listener.direct.acknowledge-mode= ack模式
spring.rabbitmq.listener.direct.auto-startup=true 是否在启动时自动启动容器
spring.rabbitmq.listener.direct.consumers-per-queue= 每个队列消费者数量.
spring.rabbitmq.listener.direct.default-requeue-rejected= 默认是否将拒绝传送的消息重新入队.
spring.rabbitmq.listener.direct.idle-event-interval= 空闲容器事件发布时间间隔.
spring.rabbitmq.listener.direct.missing-queues-fatal=false若容器声明的队列在代理上不可用,是否失败.
spring.rabbitmq.listener.direct.prefetch= 每个消费者可最大处理的nack消息数量.
spring.rabbitmq.listener.direct.retry.enabled=false 是否启用发布重试机制.
spring.rabbitmq.listener.direct.retry.initial-interval=1000ms # Duration between the first and second attempt to deliver a message.
spring.rabbitmq.listener.direct.retry.max-attempts=3 # Maximum number of attempts to deliver a message.
spring.rabbitmq.listener.direct.retry.max-interval=10000ms # Maximum duration between attempts.
spring.rabbitmq.listener.direct.retry.multiplier=1 # Multiplier to apply to the previous retry interval.
spring.rabbitmq.listener.direct.retry.stateless=true # Whether retries are stateless or stateful.
Template
spring.rabbitmq.template.mandatory: 启用强制信息;默认false
spring.rabbitmq.template.receive-timeout: receive() 操作的超时时间
spring.rabbitmq.template.reply-timeout: sendAndReceive() 操作的超时时间
spring.rabbitmq.template.retry.enabled=false: 发送重试是否可用
spring.rabbitmq.template.retry.max-attempts=3: 最大重试次数
spring.rabbitmq.template.retry.initial-interva=1000msl: 第一次和第二次尝试发布或传递消息之间的间隔
spring.rabbitmq.template.retry.multiplier=1: 应用于上一重试间隔的乘数
spring.rabbitmq.template.retry.max-interval=10000: 最大重试时间间隔
优质博客:
(扇出交换机)SpringBoot整合RabbitMQ实现生产者与消费者_沮丧的南瓜的博客-CSDN博客_rabbitmq springboot 生产者
springboot整合rabbitmq集群配置详解_ThePual的博客-CSDN博客_springboot整合rabbitmq集群
RabbitMQ:@RabbitListener 与 @RabbitHandler 及 消息序列化_砂锅大的拳头的博客-CSDN博客_rabbitlistener注解
(路由交换机)SpringBoot整合RabbitMq实现生产者、消费者,RabbitMQ的简单介绍_祁仙森的博客-CSDN博客_springboot消费rabbitmq
spring boot rabbitmq整合rabbitmq之消息持久化存储_weixin_43831204的博客-CSDN博客_springboot整合rabbitmq的持久化
SpringBoot整合RabbitMQ,开启手动应答,失败重传机制_哒哒哒哒~的博客-CSDN博客
RabbitMQ中各种消息类型如何处理?_mb5fe32930661bd的技术博客_51CTO博客
RabbitMQ实现JSON、Map格式数据的发送与接收_pan_junbiao的博客-CSDN博客_rabbitmq发送json
1、MQ幂等性重复消费问题:
消费端处理消息的业务逻辑保持幂等性。
幂等性,通俗点说,就一个数据,或者一个请求,给你重复来多次,你得确保对应的数据是不会改变的,不能出错。
比如1,你拿到这个消息做数据库的insert操作。那就容易了,给这个消息做一个唯一主键,那么就算出现重复消费的情况,就会导致主键冲突,避免数据库出现脏数据。
比如2,你拿到这个消息做redis的set的操作,那就容易了,不用解决,因为你无论set几次结果都是一样的,set操作本来就算幂等操作。
如果上面两种情况还不行,上大招。准备一个第三方介质,来做消费记录。以redis为例,给消息分配一个全局id,只要消费过该消息,将<id,message>以K-V形式写入redis。那消费者开始消费前,先去redis中查询有没消费记录即可。
2、MQ消费顺序性问题:RabbitMQ如何保证消费的顺序性 -阿里云开发者社区
RabbitMQ本身并不能直接保证消息的消费顺序,因为它将消息发送到多个消费者并行处理。然而,你可以采取以下方法来实现消息的有序消费:
- 单一消费者模式(Single Consumer Mode):
- 将队列设置成只允许一个消费者连接,这样就可以确保消息按照发送的顺序进行消费。
- 这种方式简单有效,但无法实现水平扩展和高吞吐量,因为只有一个消费者在处理消息。
- 消息分区(Message Partitioning)(P1):
- 可以根据某个消息中的特定属性(例如Key)将消息分区到不同的队列中。
- 每个队列对应一个消费者,在每个队列中的消息是有序的,保证了消息的顺序处理。
- 分区依赖于业务逻辑来确定消息的分发规则,需要在生产者端和消费者端进行相关设置。
- 基于优先级的消费者(Priority Consumers)(P2):
- 在消费者端,可以为每个消费者指定不同的优先级。
- RabbitMQ 会优先将消息发送给优先级高的消费者,从而实现按优先级有序消费。
- 消费者协调(Consumer Coordination):
- 使用一个辅助的控制组件或调度程序,协调多个消费者的工作。
- 控制组件可以根据需要分配消息给各个消费者,保证消息按照正确的顺序进行消费。
需要注意的是,上述方法并不是 RabbitMQ 内置的机制,而是一些可行的解决方案。根据具体的业务需求和系统架构,选择合适的方案来实现有序消费,能够满足你的需求。
(P1) RabbitMQ提供了多种方式来实现消息分区,下面介绍两种常用的方法:
- 基于路由键(Routing Key)的消息分区:
- 在生产者端发送消息时,可以为每个消息指定一个特定的路由键。
- RabbitMQ根据路由键将消息发送到不同的交换机(Exchange)中。
- 消费者通过绑定指定的队列和路由键来接收消息。这样,每个消费者只会接收到其关注的特定路由键的消息,从而实现了消息的分区。
- 基于发布确认(Publisher Confirms)的消息分区:
- 在生产者发送消息之前,可以通过设置发布确认模式以及等待确认消息的机制来实现消息的分区。
- 生产者在每次发送消息之后,可以等待RabbitMQ返回的确认消息,判断消息是否成功发送到RabbitMQ服务器。
- 如果消息被确认,则可以继续发送下一条消息;如果消息被拒绝,则可以进行重试或其他处理。
- 通过在不同的生产者通道上设定发布确认模式,可以将消息分区到不同的通道上,从而实现消息的分区。
(P2) RabbitMQ提供了通过设置队列和消费者的优先级来实现优先级消费的功能。下面是设置优先级消费者的步骤:
- 创建优先级队列(Priority Queue):
- 在创建队列时,可以通过设置x-max-priority参数来指定队列的最大优先级。
- 例如,使用 RabbitMQ 的管理界面或 AMQP 协议创建队列时,可以添加参数 x-max-priority: <priority>,其中 <priority> 是一个整数值。
- 发送带有优先级属性的消息:
- 在生产者发送消息时,可以为每个消息设置优先级属性。
- 可以在消息的属性中包含一个priority字段,用于表示消息的优先级。
- 较高优先级的消息将会在消费者端被优先处理。
- 创建优先级消费者:
- 在消费者端,需要创建一个支持优先级消费的消费者。
- 首先,确保消费者连接到队列,并且可以接收到消息。
- 然后,在消费者的基础上设置消费者的prefetch_count属性,以限制一次只处理一个消息。
- 这样可以确保消费者按照优先级逐个处理消息,而不是同时处理多个消息。
3、MQ消息积压问题:
RabbitMQ--消息堆积--解决方案_rabbitmq消息积压如何解决_IT利刃出鞘的博客-CSDN博客
4、延迟队列:
1、什么是死信队列
死信队列:DLX,dead-letter-exchange
利用DLX,当消息在一个队列中变成死信 (dead message) 之后,它能被重新publish到另一个Exchange,这个Exchange就是DLX
2、消息变成死信有以下几种情况
消息被拒绝(basic.reject / basic.nack),并且requeue = false
消息TTL过期
队列达到最大长度
3、死信处理过程
DLX也是一个正常的Exchange,和一般的Exchange没有区别,它能在任何的队列上被指定,实际上就是设置某个队列的属性。
当这个队列中有死信时,RabbitMQ就会自动的将这个消息重新发布到设置的Exchange上去,进而被路由到另一个队列。
可以监听这个队列中的消息做相应的处理。
4、死信队列实现延时消息原理
我们将消息发送到消息队列中,并设置一个过期时间,该队列没有消费者
消息的过期时间到了之后,由于没有消费者,就会进入死信队列
我们用一个消费者接收死信队列的消息,就能达到延迟消息的目的
注意:如果设置了队列的 TTL 属性,那么一旦消息过期,就会被队列丢弃(如果配置了死信队列被丢到死信队列中)。
消息设置 TTL,消息即使过期,也不一定会被马上丢弃,因为消息是否过期是在即将投递到消费者之前判定的,如果当前队列有严重消息积压情况,则已过期的消息也许还能存活较长时间;
注意:消息也可以指定延时时间,但是会存在问题:因为 RabbitMQ 只会检查第一个消息是否过期,如果过期则丢到死信队列, 如果第一个消息的延时时长很长,而第二个消息的延时时长很短,第二个消息并不会优先得到执行。
Rabbitmq学习笔记(尚硅谷2021)_尚硅谷rabbitmq笔记_江北残刀的博客-CSDN博客
解决方案:
1、Rabbitmq 安装插件实现延迟队列(尚硅谷)在官网上下载 https://www.rabbitmq.com/community-plugins.html,下载 rabbitmq_delayed_message_exchange 插件
2、可以使用下面2种方式:
【SpringBoot】43、SpringBoot中整合RabbitMQ实现延时队列(延时插件篇)_Asurplus的博客-CSDN博客_springboot 延时队列
redis实现延时队列的两种方式_卿着飞翔的博客-CSDN博客_redis延时队列
普通延时队列实现:
【SpringBoot】60、SpringBoot中整合RabbitMQ实现延时队列(死信队列篇)
(重要)SpringBoot整合RabbitMQ实现延时队列_PGL2009的博客-CSDN博客_springboot+rabbitmq的延迟队列实现方式
5、集群搭建:
1、镜像集群
手把手教你搭建 RabbitMQ 集群__江南一点雨的博客-CSDN博客_rabbitmq集群部署方案
RabbitMQ 进阶 -- 阿里云服务器部署RabbitMQ集群_Bug 终结者的博客-CSDN博客_阿里云rabbitmq
2、高可用集群(RabbitMQ镜像集群
+ HAProxy负载均衡
+ KeepAlived
解决单点失效 )
6、消息应答
SpringBoot 集成 RabbitMq 消费者手动确认消息,失败重试后发送至死信队列_springboot向rabbitmq发送消息确认失败后重发-CSDN博客
消息自动应答和消息手动应答的区别
在RabbitMQ中,消息应答有两种模式:手动应答和自动应答( 默认)。
自动应答,是指broker不在乎消费者对消息处理是否成功,都会告诉队列删除消息。如果处理消息失败,又没有捕获异常,则会实现自动补偿(队列重新向消费者投递消息);
如果捕获异常了,broker以为消息消费成功,就会将消息从队列中删除,导致数据丢失。
手动应答,是指消费者处理完业务逻辑之后,手动返回ack(通知)告诉broker消息处理完了,你可以删除消息了;或者手动返回nack消息,告诉broker消息处理失败,你先别删除消息。
自动应答是默认的,没有额外的工作量,有更高的吞吐量(只要消费者能够跟上),但是存在消息丢失和消息积压的问题;
手动应答需要自己配置,需要在消费者捕获异常,并手动确认应答状态,是ack还是nack,但是不存在消息丢失和消息积压的问题。
从上面的分析,我们可以很轻松的做出选择——肯定是选择手动应答的方式。
设置手动应答后,如果一直消费失败,对于总是消费失败的消息,一直重新入队是不对的,这会导致消息积压的问题,怎么处理这种问题呢?
这就是另一个问题了(设置重试次数和死信队列)
消息手动应答的4个写法:
---------------------------------------------------
Channel.basicAck(deliveryTag, true);
肯定应答,第二个参数为multiple,是否批量,一般设置false
----------------------------------------------------
channel.basicReject(deliveryTag, true);
basic.reject方法拒绝deliveryTag对应的消息,第二个参数是否requeue,true则重新入队列,
否则丢弃或者进入死信队列。该方法reject后,该消费者还是会消费到该条被reject的消息。
-----------------------------------------------------
channel.basicNack(deliveryTag, false, true);
basic.nack方法为不确认deliveryTag对应的消息,第二个参数multiple 是否应用于多消息,
第三个参数是否requeue,与basic.reject区别就是同时支持多个消息,可以nack该消费者先前接收
未ack的所有消息。nack后的消息也会被自己消费到。
------------------------------------------------
channel.basicRecover(true);
basic.recover是否恢复消息到队列,参数是是否requeue,true则重新入队列,并且尽可能的
将之前recover的消息投递给其他消费者消费,而不是自己再次消费。false则消息会重新被投递给自己。
项目演示:
public class Constant {
/*船岸交换机*/
public static final String SHIPTOSHORE_EXCHANGE_NAME = "exchange_shiptoshore_direct";
/*船岸队列*/
public static final String SHIPTOSHORE_QUEUE_NAME = "shiptoshore_queue";
/*船岸routingkey*/
public static final String SHIPTOSHORE_ROUTING_KEY = "shiptoshore_routingKey";
/*岸船队列*/
public static final String SHORETOSHIP_QUEUE_NAME = "shoretoship_queue";
/*岸船routingkey*/
public static final String SHORETOSHIP_ROUTING_KEY = "shoretoship_routingKey";
package com.oceansite.system.mq;
import com.oceansite.system.constant.Constant;
import org.springframework.amqp.core.Binding;
import org.springframework.amqp.core.BindingBuilder;
import org.springframework.amqp.core.DirectExchange;
import org.springframework.amqp.core.Queue;
import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter;
import org.springframework.amqp.support.converter.MessageConverter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* @ClassName: ExchangeQueueBindingConfig
* @Description:
* @Author:
* @Date: 2022/4/21 9:35
**/
@Configuration
public class ExchangeQueueBindingConfig {
@Bean
public DirectExchange shiptoshoreExchange(){
return new DirectExchange(Constant.SHIPTOSHORE_EXCHANGE_NAME);
}
@Bean
public Queue shiptoshoreQueue(){
return new Queue(Constant.SHIPTOSHORE_QUEUE_NAME);
}
@Bean
public Binding binding(){
return BindingBuilder.bind(shiptoshoreQueue()).to(shiptoshoreExchange()).with(Constant.SHIPTOSHORE_ROUTING_KEY);
}
@Bean
public MessageConverter messageConverter(){
return new Jackson2JsonMessageConverter();
}
}
package com.oceansite.system.mq;
import com.oceansite.system.api.domain.SysUser;
import com.oceansite.system.constant.Constant;
import com.oceansite.system.domain.SysUserRole;
import com.oceansite.system.domain.vo.*;
import com.oceansite.system.service.impl.SysShoreToShiplSyncConsumer;
import com.rabbitmq.client.Channel;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitHandler;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.io.IOException;
import java.util.List;
import java.util.Map;
/**
* @ClassName: ShipToShoreMQListener
* @Description:
* @Author:
* @Date: 2022/4/21 10:23
**/
@Component
@RabbitListener(queues = Constant.SHORETOSHIP_QUEUE_NAME)
public class ShipToShoreMQListener {
@Autowired
private SysShoreToShiplSyncConsumer sysShoreToShiplSyncConsumer;
@RabbitHandler
public void shipToShoreSystemData(SysRoleVo sysRoleVo, Channel channel, Message message) throws IOException {
try {
switch (sysRoleVo.getOperationType()){
case "insertRole":{
sysShoreToShiplSyncConsumer.insertRole(sysRoleVo.getSysRole());
}
case "updateRole":{
sysShoreToShiplSyncConsumer.updateRole(sysRoleVo.getSysRole());
}
case "updateRoleStatus":{
sysShoreToShiplSyncConsumer.updateRoleStatus(sysRoleVo.getSysRole());
}
case "authDataScope":{
sysShoreToShiplSyncConsumer.authDataScope(sysRoleVo.getSysRole());
}
}
channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);
}catch (Exception e){
channel.basicReject(message.getMessageProperties().getDeliveryTag(),true);
}
}
@RabbitHandler
public void shipToShoreSystemData(Map<String,Object> map, Channel channel, Message message) throws IOException {
try {
switch ((String) map.get("operationType")){
case "deleteRoleById":{
sysShoreToShiplSyncConsumer.deleteRoleById((Long) map.get("roleId"));
}
case "deleteRoleByIds":{
sysShoreToShiplSyncConsumer.deleteRoleByIds((Long[]) map.get("roleIds"));
}
case "deleteAuthUsers":{
sysShoreToShiplSyncConsumer.deleteAuthUsers((Long)map.get("roleId"), (Long[]) map.get("userIds"));
}
case "insertAuthUsers":{
sysShoreToShiplSyncConsumer.insertAuthUsers((Long)map.get("roleId"), (Long[]) map.get("userIds"));
}
case "insertUserAuth":{
sysShoreToShiplSyncConsumer.insertUserAuth((Long)map.get("userId"), (Long[]) map.get("roleIds"));
}
case "updateUserAvatar":{
sysShoreToShiplSyncConsumer.updateUserAvatar((String) map.get("userName"), (String) map.get("avatar"));
}
case "resetUserPwd":{
sysShoreToShiplSyncConsumer.resetUserPwd((String) map.get("userName"), (String) map.get("password"));
}
case "deleteUserById":{
sysShoreToShiplSyncConsumer.deleteUserById((Long) map.get("userId"));
}
case "deleteUserByIds":{
sysShoreToShiplSyncConsumer.deleteUserByIds((Long[]) map.get("userIds"));
}
case "importUser":{
sysShoreToShiplSyncConsumer.importUser((List<SysUser>) map.get("userList"),(Boolean) map.get("isUpdateSupport"),(String)map.get("operName"));
}
}
channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);
}catch (Exception e){
channel.basicReject(message.getMessageProperties().getDeliveryTag(),true);
}
}
}
@Override
@Transactional(rollbackFor = Exception.class)
public int insertRole(SysRole role){
//指定唯一的roleid
role.setRoleId(new SnowflakeidGenerator().snowflakeId());
boolean b = remoteShoreSystem.remoteQueryPermission(role);
if (b){
SysRoleVo sysRoleVo = new SysRoleVo();
String methodName = Thread.currentThread().getStackTrace()[1].getMethodName();
sysRoleVo.setSysRole(role);
sysRoleVo.setOperationType(methodName);
try {
rabbitTemplate.convertAndSend(Constant.SHIPTOSHORE_EXCHANGE_NAME,Constant.SHIPTOSHORE_QUEUE_NAME, sysRoleVo);
}catch (Exception e){
return -1;
}
roleMapper.insertRole(role);
return insertRoleMenu(role);
}else {
return -2;
}
}
rabbitmq:
host: 192.168.79.60
username: oceansite
password: oceansite
virtual-host: /
port: 5672
listener:
simple:
acknowledge-mode: manual #切换手动应答
动态监听队列实现:通过spel表达式
配置队列:
@Bean
public Queue queue(){
if (systemConfig.getSystemType().equals("shipSystem")){
String queueName = "direct_queue"+ systemConfig.getShipImo();
return new Queue(queueName);
}
if (systemConfig.getSystemType().equals("cloudSystem")){
String queueName = "topic_queue";
return new Queue(queueName);
}
return null;
}
监听:
@Component
@RabbitListener(queues = "#{queue.name}") //重点,queue.name中的queue是bean的方法名
public class ShoreToShipMQListener {
@Autowired
private SysShoreToShipSyncServiceImpl iSysShoreToShipSyncService;
@RabbitHandler
public void shipToShoreSystemData(SysRoleVo sysRoleVo, Channel channel, Message message) throws IOException {
try {
switch (sysRoleVo.getOperationType()){
case "insertRole":{
iSysShoreToShipSyncService.insertRole(sysRoleVo.getSysRole());
break;
}
case "updateRole":{
iSysShoreToShipSyncService.updateRole(sysRoleVo.getSysRole());
break;
}
case "updateRoleStatus":{
iSysShoreToShipSyncService.updateRoleStatus(sysRoleVo.getSysRole());
break;
}
case "authDataScope":{
iSysShoreToShipSyncService.authDataScope(sysRoleVo.getSysRole());
break;
}
//后面代码省略
发布确认见百度云are-common-parent