文章目录
- 1、RabbitMQ如何保证消息不丢失
-
- [1.1 生产者确认机制](#1.1 生产者确认机制)
- [1.2 消息持久化](#1.2 消息持久化)
- [1.3 消费者确认机制](#1.3 消费者确认机制)
- 2、RabbitMQ消息重复消费
- 3、RabbitMQ延迟队列
-
- [3.1 死信交换机](#3.1 死信交换机)
- [3.2 TTL](#3.2 TTL)
- [3.3 延迟队列插件](#3.3 延迟队列插件)
- 4、处理RabbitMQ消息堆积
-
- [4.1 惰性队列](#4.1 惰性队列)
- 5、RabbitMQ的高可用
-
- [5.1 普通集群](#5.1 普通集群)
- [5.2 镜像集群](#5.2 镜像集群)
- [5.3 仲裁队列](#5.3 仲裁队列)
- 6、面试
MQ的使用场景:
- 异步发送(验证码、短信、邮件)
- MySQL和Redis,MySQL和ES的数据同步
- 分布式事务(前篇提到的借呗和支付宝两个系统)
- 削峰填谷
1、RabbitMQ如何保证消息不丢失
丢消息的几个可能点:
- 消息未达到交换机
- 消息未达到队列
- RabbitMQ宕机,导致队列中的消息丢失
- 消费者拿走消息后,没来得及消费就宕机了
针对以上可能丢数据的点,分别解决如下:
1.1 生产者确认机制
RabbitMQ提供了publisher confirm机制,来保证消息往队列发的过程不丢失。消息发送完成后,会返回一个结果给发送者:
- 如果消息成功到达队列,返回给生产者一个回执:
ack publish-confirm
- 如果消息发送失败,且是未到达交换机,返回:nack publish-confirm
- 如果消息发送失败,且是到了交换机但未到达队列,返回:ack publish-return
消息发送失败后:
- 回调方法,即时重发
- 记录一个日志
- 把这条笑死保存到数据库,然后定时再重发,直到发成功就删除表中的数据
1.2 消息持久化
以上,保证了消息发送的可靠性。消息到达队列后,MQ的消息默认放内存,宕机则队列中的消息丢失。 ⇒ 交换机、队列、消息数据持久化
- 交换机持久化
- 队列持久化
- 消息持久化,SpringAMQP 中的的消息默认是持久的,可以通过 MessageProperties 中的 DeliveryMode来配置
1.3 消费者确认机制
RabbitMQ支持消费者确认机制:消费者处理消息成功后,给MQ发送ack回执,MQ收到ack回执才删除消息。具体,SpringAMQP有三种模式可选择:
manual
:业务代码执行结束后,调用api,手动发送ackauto
:Spring检测Listener代码是否出现异常,无则自动发送ack,有则抛出异常并返回nack(常用)none
:关闭ack,MQ认为,消息你拿走了,就是消费成功了,你前脚拿走,MQ后脚删除消息
此外,选择auto时,还可以利用Spring的retry机制,在消费发生异常时,重试一定次数,仍然失败则扔到一个异常交换机里,后续人工处理
2、RabbitMQ消息重复消费
消费者确认机制选择了自动确认auto模式,设置了retry重试。此时,消费者消费完消息后,在发ack之前,网络抖动或者消费者服务挂了,ack没发出去。
消费者服务重新running后,就会重复消费这条消息。
解决方式:
- 1)每条消息带一个唯一的业务ID,比如订单ID,消息消费前,判断这个ID是否已存在,存在则直接return
- 2)按幂等性处理(考虑分布式锁、数据库锁等方案)
3、RabbitMQ延迟队列
进入队列的消息会被延迟消费。场景:
- 超时订单:30分钟内支付
- 限时优惠:商品优惠还剩2天10小时
- 定时发布:明早八点发布
延迟队列 = 死信交换机 + TTL(过期时间)
3.1 死信交换机
队列中的消息,符合一条,即为死信:
- 消费者使用basic.reject或 basic.nack声明消费失败,且消息的requeue参数设置为false(即消费者明确拒绝把它重新放回队列)
- 过期消息,超时无人消费
- 要投递的队列满了,最早的消息可能成为死信
成为死信的消息,可能被丢弃。此时,如果这个队列配置了dead-letter-exchange属性,指定了一个交换机,则死信被投递到这个交换机(即死信交换机,Dead Letter Exchange,DLX)
声明一个队列simple.queue,其死信交换机为dl.direct
3.2 TTL
Time-To-Live,存活时间,队列的消息,超过TTL还未被消费,成为死信。TTL超时分为:
- 消息所在队列设置了存活时间
- 消息本身设置了存活时间
如上,同时设置了队列存活时间和消息本身存活时间,自然以最短的为准。队列ttl.queue绑定了死信交换机dl.direct,dl.direct又关联了队列,最终死信会被绑定了死信队列的消费者处理 ⇒ 死信交换机 + TTL = 延迟队列
定时发布时,根据定时的时间戳设置其TTL,写发布逻辑,消费死信队列。时间一到,发布信息进入死信队列,实现定时发布。
3.3 延迟队列插件
在RabbitMQ安装DelayExchange插件:
java
https://www.rabbitmq.com/community-plugins.html
使用该插件时,正常声明一个交换机,并将其delayed属性设置为true,以下声明 + 消费 延迟队列的消息:
发消息时:setHeader添加x-delay头,指定超时时间
4、处理RabbitMQ消息堆积
生产快,消费慢,消息堆积,产生死信。解决方式:
- 增加消费者实例
- 消费者实例内开启线程池,加快处理
- 扩大队列容量,提高堆积上限 ⇒ 惰性队列
4.1 惰性队列
特点:
- 接收到的消息不再存内存,放磁盘
- 消费者消费消息时,从磁盘读到内存
- 支持百万条消息存储
声明队列时,调用方法,设置x-queue-mode为lazy,即为惰性队列:
@RabbitListener里的写法:
5、RabbitMQ的高可用
生产环境部署RabbitMQ集群,有两种种:
- 普通集群
- 镜像集群
5.1 普通集群
标注集群,特点:
- 集群各个节点之间共享交换机、队列的元信息,但不包含队列中的消息(节点1有队列test.queue1,节点2、节点3里是队列test.queue1的引用,不包括队列中的消息)
- 在集群节点3访问queue1队列,但节点3只有queue1的引用,实际在节点1,则会把数据传递到节点1处理
- 某个节点宕机,则其上队列的消息丢失
丢东西,所以一般不采用普通集群
5.2 镜像集群
创建队列test.queue1的节点node1,为queue1队列的主节点 ,节点node2备份了queue1队列,为queue1队列的镜像节点。当然,节点node1,也可能是别的队列的镜像节点,相对概念。
- 所有的操作,都是主节点完成,然后同步给镜像节点
- 主节点宕机,镜像节点成为该队列的新的主节点
问题:主节点完成后,还没同步给镜像节点就宕机,会丢一点点数据 ⇒ 仲裁队列解决
5.3 仲裁队列
引入仲裁队列,做主从同步时,基于Raft协议,强一致。主节点宕机后,镜像节点利用仲裁队列来保证能正确复制。声明使用仲裁队列的写法:
6、面试