一、MQ使用背景
(1)同步功能耦合高,异步可进行功能解耦
(2)同步响应时间比较长,异步可以快速响应
(3)同步会导致并发压力向后传递,异步可以削峰限流
(4)同步系统结构弹性不足,异步便于扩展
二、什么是MQ
1.定义
由于消息队列执行的是跨应用的信息传递,所以制定底层通信标准非常重要。目前主流的消息队列通信协议包括:
- AMQP(Advanced Message Queuing Protocol):通用协议,IBM研发
- JMS(Java Message Service):专门为JAVA语言服务,SUN公司研发,一组有JAVA接口组成的标准
2、MQ产品对比
三、RabbitMQ
1 RabbitMQ相关概念
- producer:消息生产者
- consumer:消息接收者
- broker:服务器实例,负责接收和分发消息
- virtual host: 虚拟分组,用户在自己的virtual host中使用MQ组件,在实际开发中,通过virtual host区分不同项目、不同功能
- exchange:交换机,消息到达broker的第一站,不会存储消息
- Queue:消息的容器,消息放在这里等待被consumer取走,消息被取走后,队列会删除该消息
- deliveryTag:交付标签机制(Long型),消费端将消息处理成ACK,NACK,Reject等返回给Broker后,Broker需要对对应的消息执行后续操作,例如:删除消息、重新排队或者标记为死信等等。那么Broker就必须知道他现在要操作的消息具体是哪一条,而deliveryTag作为消息的唯一标识就很好的满足了这个需求。
connection复用:建立TCP连接需要三次握手,反复确认。如果每一次访问RabbitMQ都建立一个connection,开销极大,效率低下。所以Channel就是在一个已经建立的connection中创建的逻辑连接。如果应用程序支持多线程,那么每个线程创建一个单独的Channel进行通讯。每个Channel都用自己的ID,Channel之间完全隔离。核心:connection复用。
2 RabbitMQ体系结构
3 RabbitMQ工作模式
a 简单模式(Simple Queue Model)
定义:生产者将消息发送到队列,消费者从队列中获取消息并处理
使用场景:适用于一对一的消息传递场景
b 工作队列模式(Work Queue Model)
定义:多个消费者监听同一队列(底层使用了默认交换机),从中获取消息,消息在消费者之间负载均衡(RabbitMQ以轮询方式将消息分发给消费者),各个消费者之间是竞争关系(因为每条消息只有一个消费者去处理)
使用场景:适用于任务分发和并行处理的场景
c 发布/订阅模式(Publish/Subscribe Model)
定义:消息被发送到交换机(Fanout类型),再由交换机分发到绑定的队列,多个消费者可以订阅同一个队列
使用场景:广播消息场景
区别:生产者不是把消息直接发送给队列,而是发送到交换机
d 路由模式(Routing Model)
定义:使用带有路由键的交换机(Direct类型),通过路由绑定的方式,把交换机和队列关联起来,消息根据路由键被分发到相应的队列,生产者发送消息时不仅要指定交换机,还要指定路由键
场景:适用于需要有选择性地接收消息的场景
-和发布/订阅者模式的区别:交换机不同;队列绑定时需要指定routing key
e 主题模式(Topic Model)
定义:使用带有主题的交换机,消息根据主题模式(如log.*)分发到对应队列
使用场景:适用于复杂的路由需要,允许使用通配符进行匹配
通配符规则:
- #:匹配零个或者多个词
- *:匹配一个词
f RPC模式(Remote Procedure Call Model)
定义:允许客户端发送请求并等待服务器的响应,模拟远程调用过程,非典型
使用场景:适用于需要同步请求-响应机制的场景
g 头交换模式 (Headers Exchange Model)
定义:使用消息头属性(headers)来路由消息,而不是路由键或主题
场景:适用于需要基于多个属性进行路由的场景
<扩展>交换机
-
交换机类型:
(1)Fanout类型:扇出交换机,广播,将消息发送给所有绑定到交换机的队列
(2)direct类型:直连交换机,定向,把消息交给符合指定路由键routing key的队列
(3)Topic:主题交换机,通配符,把消息交给符合指定routing pattern(路由模式)的队列
(4)header:头部交换机,根据消息头属性进行匹配 -
交换机只负责转发消息,不存储消息
4 RattbitMQ整合Springboot
(1)接收消息
1、先把监听器放到IOC容器中,加上@Component
2、处理消息的方法定义,加上注解@RabbitListener
@RabbitListener(bindings=@QueueBinding(
value=@Queue(value=QUEUE_NAME,durable="true"),
exchange=@Exchange(value=EXCHANGE_DIRECT),
key={ROUTING_KEY}))
public void processMessage(String dateString,Meaasge message,Channel channel){
prcess();
}
- value=@Queue(value=QUEUE_NAME,durable=“true”):QUEUE_NAME队列名称,durable="true"持久化
- exchange=@Exchange(value=EXCHANGE_DIRECT) 指定交换机
- key={ROUTING_KEY}指定路由键,字符串数组类型
(2)发送消息
1、RabbitTemplate自动装配
2、rabbitTemplate.convertAndSend(EXCHANGE_NAME,ROUTING_KEY,“要发送的具体消息“);
5 可靠性投递
故障场景:消息没有发送到消息队列
解决思路A:在生产者端进行确认,具体操作会分别针对交换机和队列来确认,如果没有成功发送消息到消息队列服务器上,那就可以尝试重新发送
解决思路B:为目标交换机指定备份交换机,当目标交换机投递失败时,把消息投递至备份交换机
- 备份交换机类型必须为fanout(广播模式找队列)
故障场景:消息队列服务器宕机导致内存中消息丢失
解决思路:持久化消息和队列,使用消息确认机制
故障场景:消费端宕机或抛异常导致消息没有成功被消费
解决思路:
如果消费端消费消息成功,给服务器返回ACK信息,然后消息队列删除该消息
如果消费端消费消息失败,给服务器返回NACK信息,同时把消息恢复为待消费状态,这样就可以再次取回消息,重试一次
6 其他知识点
(1) deliverTag交付标签机制
问题:如果交换机是fanout模式,同一个消息广播到不同队列,deliverTag会重复吗?
答:在同一个Broker内,deliverTag唯一。
场景题1:
(2)prefetch消息端限流
prefetch概念
prefetch 是指在消息队列中,消费者在确认(acknowledge)之前可以预先获取的消息数量。这个机制可以防止某个消费者一次性获取太多消息而导致其他消费者无法获取到消息,从而实现更均衡的负载分配。
工作原理
- 默认行为:如果不设置 prefetch,RabbitMQ 会将队列中的消息尽可能多地发送给消费者。这可能导致某个消费者积压大量消息,而其他消费者处于空闲状态。
- 设置 prefetch:通过设置 prefetch 值,可以限制消费者在确认之前最多能获取的消息数量。例如,如果 prefetch 值设置为 1,消费者在确认前最多只能获取一条消息。
使用场景
- 负载均衡:通过设置prefetch值,可以确保消息在多个消费者之间更均匀的分配,防止某个消费者因积压过多而成为瓶颈
- 资源管理:限制消费者预先获取的消息数量,可以防止消费者一次性获取太多消息而占用过多内存或其他资源
- 优化性能:在某些场景下,设置较小的 prefetch 值可以减少消息处理的延迟,而设置较大的 prefetch 值可以提高吞吐量。
(3) 死信
概念
- 当一个消息无法被消费,就变成了死信
产生原因
- 拒绝:消费者拒接消息,并且不把消息重新放入原目标队列
- 溢出:队列消息超出限制。比如队列只能存10条消息,并且已经存储10条消息,当再发一条过来的时候,按照先进先出的原则,队列最早的消息会变成死信
- 超时:消息到达超时时间没有被消费
处理方式
- 丢弃:对不重要的消息直接丢弃不处理
- 入库:把死信写入数据库之后处理
- 监听:消息变成死信后进入死信队列,我们专门设置消费端监听死信队列,做后续处理
(4) 事务消息
- 控制的是提交给broker之前,缓存中的消息
- RabbitTransactionManager
(5) 惰性队列
概念
惰性队列通过将消息尽可能多地存储在磁盘上,而不是内存中,从而减少内存消耗。这对于处理非常大规模的消息队列或需要长时间保留消息的场景特别有用。
优点
消息在磁盘上,减少了内存占用;
适合处理大量消息,提高了队列的可扩展性;
在消息积压严重的情况下,性能表现更好。
缺点
访问延迟——由于消息在磁盘上,访问消息会有一定延迟,特别是在高频访问的情况下;
磁盘I/O开销——频繁的磁盘读写操作可能会增加磁盘I/O开销,影响系统的整体性能
使用场景
批量处理——适合需要处理批量消息的场景,比如日志收集、数据批量处理等;
长时间积压——适合消息长时间积压的场景,避免内存占用过多;
资源访问受限系统——适合内存资源有限,但需要处理大量消息的系统。
(6) 优先级队列
- RabbitMQ允许使用一个正整数(1~255,官网建议1到5之间)给消息设定优先级
- 队列在声明时需要指定参数:x-max-priority,否则这个值默认是0,消息即使设置了优先级不设置这个也是无效的
- 使用消息的后置处理器来完成代码实现
四、MQ热点场景题
如何保证消息的顺序性
RocketMQ为例:
-
生产侧顺序性:如果要保证消息生产的顺序性,则必须满足以下几个条件
(1)单一生产者:消息生产的顺序性只支持单一生产者
(2)串行发送:RocketMQ生产者客户端之间支持多线程安全访问,但是如果生产者使用多线程并行发送,则不同线程产生的消息将无法判定其先后顺序 -
消费侧顺序性:如果要保证消息消费的顺序性,则必须满足以下几个条件
(1)按照投递顺序进行消费
(2)有限重试
消息丢失
消息丢失的几种场景
(1)生产者发送到MQ服务器的过程中出现消息丢失。可能是网络波动没收到消息,又或者服务器宕机
(2)MQ服务器消息持久化出现消息丢失,消息发送到MQ之后,没能及时存储完成持久化,MQ服务器出现宕机
(3)消费者拉取过程异常
如何保证消息不丢失
(1)生产者——confirm消息确认机制
confirm模式是RabbitMQ提供的消息可靠性保证机制,当生产者通过confirm模式发送消息时,它会等待RabbitMQ服务的确认,确保消息已经被正确地投递到了指定的Exchange中。
消息被正确地投递到queue中,broker会返回ACK。
消息没有正确地投递到queue时,broker会返回nack。
如果exchange没有绑定对应的queue,也会出现消息丢失(开发者问题)
(2)MQ服务——消息持久化机制
持久化机制是指将消息存到磁盘上,以保证RabbitMQ重启或者宕机时,消息不会丢失。
使用方法:生产者通过将消息的delivery_mode属性设置为2,将消息标记为持久化。队列也需要持久化设置,确保队列在RabbitMQ服务器重启后仍然存在。经典队列需要将durable属性设置为true,仲裁队列和流式队列默认必须持久化保存
(3)消费者——ACK事务确认机制
确保消息被正确消费。当消息被消费者成功处理后,消费者发送ACK给RabbitMQ,告知消息可以被移除。这个过程是自动处理的,也可以关闭进行手动发送ACK。
重复消费
什么场景会发生
(1)生产者:生产者没有做接口幂等性,比如Controller接口被重复的调用了两次
(2)MQ:消费者消费成功时,MQ突然挂了,导致MQ以为消费者还没消费该条数据,MQ恢复后再次发送了该条消息
(3)消费者:消费后没来得及发送确认就挂掉了,MQ认为消费者还没消费,再次推消息
如何保证消息不被重复消费
(1)数据库唯一键约束:局限性很大
(2)乐观锁:假设是更新订单状态,在发送消息的时候带上修改字段的版本号,缺陷:多条消息可能版本号一致
(3)插入一个标记来进行判断
消息堆积
为什么会发生消息堆积
RabbitMQ如何解决消息堆积问题
(1)消费者处理消息的速度太慢
- 增加消费者数量:通过水平扩展,增加消费者的数量来提高处理能力
- 优化消费者性能:提高消费者处理信息的效率,例如优化代码、增加资源
- 消息预取限制(prefetch count):调整消费者获得消息的数量来避免一次处理过多消息导致处理缓慢
(2)队列容量太小
- 调整队列设置以允许更多消息的存储
(3)网络故障
- 监控和告警
- 持久化和高可用性:确保消息和队列的持久化以避免消息丢失,并使用镜像队列提高可用性
(4)消费者故障
- 死信队列:将无法处理的消息转移到死信队列,防止阻塞主队列
- 容错机制:实现消费者的自动重启后错误处理逻辑
(5)队列配置不当
- 优化配置:确认模式、队列长度限制和其他相关配置
(6)消息大小
- 消息分片:将大型消息分割成小的消息片段,加快处理速度
(7)业务逻辑复杂或者耗时
- 简化消费者处理逻辑
(8)消息产生速度快于消费速度
- 使用消息限流:控制消息的生产速度
- 负载均衡:确保消息在消费者之间公平分配,避免个别消费者过载
(9)其他优化配置
- 消息优先级
- 调整MQ配置
RabbitMQ交换机类型
1、direct直连交换机
通过路由键将消息和队列完全匹配
2、fanout扇出交换机
广播模式,将消息分发到所有队列
3、topic主题交换机
通过路由键进行模糊匹配,通过".“分为多个部分,”*“代表一个元素,”#"0个或者多个部分
4、headers头部交换机
匹配AMQP消息的header而不是路由键,几乎不用