目录
消息可靠性
消息可靠性 -- 生产者确认
消息可靠性 -- 消息持久化
消息可靠性 -- 消费者确认
消息可靠性 -- 消费者失败重试机制
消息可靠性 -- 实现总结
死信交换机
死信交换机 -- 实现消息延迟推送
死信交换机 -- 延迟队列插件 -- DelayExchange
消息堆积 -- 惰性队列
MQ集群分类
MQ集群 -- 普通集群
MQ集群 -- 镜像集群
MQ集群 -- 仲裁集群
在消息的推送,路由和接收处理中,都有可能遇到消息的丢失,常见的丢失原因可以分为三类:
发送时丢失: 生产者发送的消息未送达exchange 消息到达exchange后未到达queue
MQ宕机,queue中的消息丢失
consumer接收到消息后未消费就宕机
针对这三类情况,MQ提供了四种解决方案:
生产者确认机制
消息持久化
消费者确认机制
失败重试机制
RabbitMQ提供了publisher confirm机制来避免消息发送到MQ过程中丢失。这种机制必须给每个消息指定一个唯一ID。消息发送到MQ以后,会返回一个结果给发送者,表示消息是否处理成功。
返回结果有两种方式:
publisher-confirm,发送者确认
消息成功投递到交换机,返回ack
消息未投递到交换机,返回nack
publisher-return,发送者回执
消息投递到交换机了,但是没有路由到队列。返回ACK,及路由失败原因。
发送消息的时候需要给消息提供一个唯一ID,结果返回的时候可以绑定ack/nack, 用来区分不同的消息,避免ack碰撞
配置生产者确认模式:
修改publisher服务中的application.yml文件:
spring: rabbitmq: publisher-confirm-type: correlated publisher-returns: true template: mandatory: true
publish-confirm-type:开启publisher-confirm,这里支持两种类型:
simple:同步等待confirm结果,直到超时
correlated:异步回调,定义ConfirmCallback,MQ返回结果时会回调这个ConfirmCallback
publish-returns:开启publish-return功能,同样是基于callback机制,不过需要的定义ReturnCallback
template.mandatory:定义消息路由失败时的策略。true,则调用ReturnCallback;false:则直接丢弃消息
定义publish-returns的回调ReturnCallback:
在spring中RabbitTemplate只能配置一个ReturnCallback,所以我们需要在启动时配置
@Slf4j @Configuration //继承ApplicationContextAware,spring的bean容器通知接口 public class CommonConfig implements ApplicationContextAware { //重写设置容器方法 @Override public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { // 从bean容器中获取RabbitTemplate这个bean RabbitTemplate rabbitTemplate = applicationContext.getBean(RabbitTemplate.class); // 通过lambda表达式设置ReturnCallback回调函数 rabbitTemplate.setReturnCallback((message, replyCode, replyText, exchange, routingKey) -> { // 投递失败,记录日志 log.info("消息发送失败,应答码{},原因{},交换机{},路由键{},消息{}", replyCode, replyText, exchange, routingKey, message.toString()); // 如果有业务需要,可以重发消息 }); } }
定义publisher-confirm的correlated类型回调ConfirmCallback:
//模拟发送消息 public void testSendMessage2SimpleQueue() throws InterruptedException { //构造消息体 String message = "hello, spring amqp!"; //因为Correlated类型的回调 //所以需要创建CorrelationData对象 //需要封装两个属性 1:消息唯一id 2:回调函数 这里封装消息唯一ID CorrelationData correlationData = new CorrelationData(UUID.randomUUID().toString()); //封装callback回调 correlationData.getFuture().addCallback( result -> { //调用函数判断MQ返回结果 if(result.isAck()){ //ack,消息成功 log.debug("消息发送成功, ID:{}", correlationData.getId()); }else{ //nack,消息失败 log.error("消息发送失败, ID:{}, 原因{}",correlationData.getId(), result.getReason()); } }, ex -> log.error("消息发送异常, ID:{}, 原因{}",correlationData.getId(),ex.getMessage()) ); //发送消息,参数: 交换机 routingKey correlationData对象 rabbitTemplate.convertAndSend("task.direct", "task", message, correlationData); // 休眠一会儿,等待ack回执 Thread.sleep(2000); }
生产者确认机制可以保证将消息推送到队列中,但是如果MQ宕机,还是会出现消息丢失问题,我们可以使用消息持久化机制解决这个问题
而消息持久化分为三个阶段:
交换机持久化
队列持久化
消息持久化
而spring中,默认是全部实现持久化的
不过我们也可以手动设置,或者取消,因为持久化是写入到本地中的,会有额外的磁盘IO,所以我们可以根据实际情况,将一些低价值的数据取消持久化,实现和取消持久化,只需要设置direct 类型即可,true为开启持久化, false为取消持久化
交换机设置
@Bean public DirectExchange simpleExchange(){ // 三个参数:交换机名称、是否持久化、当没有queue与其绑定时是否自动删除 return new DirectExchange("simple.direct", true, false); }
队列设置
@Bean public Queue simpleQueue(){ // 使用QueueBuilder构建队列,durable就是持久化的 return QueueBuilder.durable("simple.queue").build(); }
消息设置
值得注意的是RabbitMQ是"阅后即焚"模式的,即消费者确认接收到消息后,MQ就会将消息删除,那如果此时消费者宕机,消息就会丢失,所以针对消费者确认机制,MQ提供了三种策略:
manual:手动ack,需要在业务代码结束后,调用api发送ack。
auto:自动ack,由spring运用AOP机制监测代码是否出现异常,没有异常则返回ack;抛出异常则返回nack
none:关闭ack,MQ假定消费者获取消息后会成功处理,因此消息投递后立即被删除
一般情况下,我们使用auto就能满足需求
auto模式开启:
修改消费者的application.yml配置文件
spring: rabbitmq: listener: simple: acknowledge-mode: auto # 开启spring管理
none模式开启:
spring: rabbitmq: listener: simple: acknowledge-mode: none # 关闭ack
当消费者出现异常后,消息会requeue(重入队)到消息队列中,进行失败重试,但如果消费者依旧是不可用状态,name消息就会一直requeue,导致MQ消息负载,而spring为我们提供了retry机制(本地重试机制)
retry机制:
当消息异常后,会在本地按照我们的自定义策略进行重试,这样可以避免MQ的消息负载
开启retry,修改消费者的application.yml文件
spring: rabbitmq: listener: simple: retry: enabled: true # 开启本地消费者失败重试 initial-interval: 1000 # 第一次失败等待时长为1秒 multiplier: 1 # 失败的等待时长倍数,下次等待时长 = multiplier * 上一次等待时长 max-attempts: 3 # 最大重试次数 stateless: true # true无状态;false有状态。如果业务中包含事务,这里改为false
当超过了重试此时spring默认会抛出异常,同时将消息丢弃,
而spring也提供了三种重试失败策略供我们定制, MessageRecovery接口的三种实现:
RejectAndDontRequeueRecoverer:重试耗尽后,直接reject,丢弃消息。默认就是这种方式
ImmediateRequeueMessageRecoverer:重试耗尽后,返回nack,消息重新入队
RepublishMessageRecoverer:重试耗尽后,将失败消息投递到指定的交换机
比较实用的是RepublishMessageRecoverer自定义处理,我们可以定义一个专门处理这种问题消息的交换机,统一进行人工处理
@Configuration public class ErrorMessageConfig { //定义专门处理的"error"交换机 @Bean public DirectExchange errorMessageExchange(){ return new DirectExchange("error.direct"); } //定义"error"队列 @Bean public Queue errorQueue(){ return new Queue("error.queue", true); } //绑定交换机与队列 @Bean public Binding errorBinding(Queue errorQueue, DirectExchange errorMessageExchange){ return BindingBuilder.bind(errorQueue).to(errorMessageExchange).with("error"); } //设置republishMessageRecoverer接口 @Bean public MessageRecoverer republishMessageRecoverer(RabbitTemplate rabbitTemplate){ return new RepublishMessageRecoverer(rabbitTemplate, "error.direct", "error"); } }
开启生产者确认机制,确保生产者的消息能到达队列
开启持久化功能,确保消息未消费前在队列中不会丢失
开启消费者确认机制为auto,由spring确认消息处理成功后完成ack
开启消费者失败重试机制,并设置MessageRecoverer,多次重试失败后将消息投递到异常交换机,交由人工处理
当一个队列中的消息满足下列情况之一时,可以成为死信(dead letter):
消费者使用basic.reject或 basic.nack声明消费失败,并且消息的requeue参数设置为false
消息是一个过期消息,超时无人消费
要投递的队列消息满了,无法投递
如果这个包含死信的队列配置了
dead-letter-exchange
属性,指定了一个交换机,那么队列中的死信就会投递到这个交换机中,而这个交换机称为死信交换机(Dead Letter Exchange,简称DLX)。如果队列绑定了死信交换机,那么死信消息就会被推送到死信交换机中
如果死信交换机绑定了队列,那么死信消息就会进入到死信队列中
另外,队列将死信投递给死信交换机时,必须知道两个信息:
死信交换机名称
死信交换机与死信队列绑定的RoutingKey
这样才能确保投递的消息能到达死信交换机,并且正确的路由到死信队列。
上面讲到的RepublishMessageRecoverer失败重试机制,我们可以为队列配置死信交换机与死信队列就可以不用配置"error"交换机,
TTL超时:
上面讲到称为死信有个条件是" 消息是一个过期消息,超时无人消费 ", 我们可以运用这一点,设置TTL超时的时间,实现消息的延迟推送
具体原理:
有两种方式设置超时时间:
设置消息的超时时间:
@Test public void testTTLMsg() { // 创建消息 Message message = MessageBuilder .withBody("hello, ttl message".getBytes(StandardCharsets.UTF_8)) .setExpiration("5000") //设置5000毫秒的超时 .build(); // 消息ID,需要封装到CorrelationData中 CorrelationData correlationData = new CorrelationData(UUID.randomUUID().toString()); // 发送消息 rabbitTemplate.convertAndSend("ttl.direct", "ttl", message, correlationData); log.debug("发送消息成功"); }
设置队列的超时时间:
@Bean public Queue ttlQueue(){ return QueueBuilder.durable("ttl.queue") // 指定队列名称,并持久化 .ttl(10000) // 设置队列的超时时间,10秒 .deadLetterExchange("dl.ttl.direct") // 指定死信交换机 .build(); }
当队列和消息都设置了超时时间的话,spring会以最短的那个时间为准
总结
消息超时的两种设置方式:
设置消息TTL属性,队列接收到消息后超过TTL时间的信息变为死信
设置队列TTL属性,消息进入队列后超过TTL时间的信息变为死信
如何实现消息的延迟推送:
1) 给目标队列指定死信交换机
2) 将消费者监听的队列绑定到死信交换机
3) 设置消息/队列的TTL超时时间
利用TTL结合死信交换机,我们实现了消息发出后,消费者延迟收到消息的效果。这种消息模式就称为延迟队列(Delay Queue)模式。
延迟队列的使用场景包括:
- 延迟发送短信
- 用户下单,如果用户在15 分钟内未支付,则自动取消
- 预约工作会议,20分钟后自动通知所有参会人员
因为延迟队列的需求非常多,所以RabbitMQ的官方也推出了一个插件,原生支持延迟队列效果。这个插件就是DelayExchange插件。Community Plugins — RabbitMQRabbitMQ插件库 Community Plugins — RabbitMQ
使用方式可以参考官网地址:Scheduling Messages with RabbitMQ | RabbitMQ - Blog
DelayExchange原理
DelayExchange需要将一个交换机声明为delayed类型。当我们发送消息到delayExchange时,流程如下:
- 接收消息
- 判断消息是否具备x-delay属性
- 如果有x-delay属性,说明是延迟消息,持久化到硬盘,读取x-delay值,作为延迟时间
- 返回routing not found结果给消息发送者
- x-delay时间到期后,重新投递消息到指定队列
使用DelayExchange
声明一个交换机,交换机的类型可以是任意类型,只需要设定delayed属性为true即可,然后声明队列与其绑定即可。
基于注解:
发送消息时,需要设置x-delay属性,给定超时时间
消息堆积问题:
当遇到消息的生产速度高于消息的处理速度的时候,消息就会在队列中堆积,引发消息堆积问题,解决消息堆积问题有两种方案
使用work queue的队列模型,通过增加消费者来提高消息处理的速度
提高队列的存储容量,而消息队列默认是将数据存储到内存中的,这样显然不满足需求,所以我们要使用RabbitMQ的3.6.0版本开始,增加的Lazy Queues的概念,也就是惰性队列
Lazy Queues(惰性队列):
- 接收到消息后直接存入磁盘而非内存
- 消费者要消费消息时才会从磁盘中读取并加载到内存
- 支持数百万条的消息存储使用惰性队列就能极大的提高消息队列的容量,不过这时候,消费消息的时候要从磁盘读取,会损耗一定的性能
开启Lazy Queues:
设置惰性队列,有两种方式,可以在程序运行时,动态的将普通队列改为惰性队列,也可以在定义队列的时候直接设置为惰性队列
修改运行中的队列:
rabbitmqctl set_policy Lazy "^lazy-queue$" '{"queue-mode":"lazy"}' --apply-to queues
命令解读:
- rabbitmqctl :RabbitMQ的命令行工具
- set_policy :添加一个策略
- Lazy :策略名称,可以自定义
- "^lazy-queue$" :用正则表达式匹配队列的名字
- '{"queue-mode":"lazy"}':设置队列模式为lazy模式
- --apply-to queues :策略的作用对象,是所有满足正则表达式的队列创建惰性队列:
基于注解:
基于@Bean:
总结
消息堆积问题的解决方案?
- 队列上绑定多个消费者,提高消费速度
- 使用惰性队列,可以再mq中保存更多消息惰性队列的优点有哪些?
- 基于磁盘存储,消息上限高
- 没有间歇性的page-out,性能比较稳定惰性队列的缺点有哪些?
- 基于磁盘存储,消息时效性会降低
- 性能受限于磁盘的IO
RabbitMQ的是基于Erlang语言编写,而Erlang又是一个面向并发的语言,天然支持集群模式。RabbitMQ的集群有两种模式:
普通集群:是一种分布式集群,将队列分散到集群的各个节点,从而提高整个集群的并发能力。
镜像集群:是一种主从集群,普通集群的基础上,添加了主从备份功能,提高集群的数据可用性。
镜像集群虽然支持主从,但主从同步并不是强一致的,某些情况下可能有数据丢失的风险。因此在RabbitMQ的3.8版本以后,推出了新的功能:仲裁队列来代替镜像集群,底层采用Raft协议确保主从的数据一致性。
普通集群,或者叫标准集群(classic cluster),具备下列特征:
- 会在集群的各个节点间共享部分数据,包括:交换机、队列元信息。不包含队列中的消息。
- 当访问集群某节点时,如果队列不在该节点,会从数据所在节点传递到当前节点并返回
- 缺点是队列所在节点宕机,队列中的消息就会丢失
镜像集群:本质是主从模式,具备下面的特征:
- 交换机、队列、队列中的消息会在各个mq的镜像节点之间同步备份。
- 创建队列的节点被称为该队列的主节点,备份到的其它节点叫做该队列的镜像节点。
- 一个队列的主节点可能是另一个队列的镜像节点
- 所有操作都是主节点完成,然后同步给镜像节点
- 主宕机后,镜像节点会替代成新的主节点
仲裁队列:仲裁队列是3.8版本以后才有的新功能,用来替代镜像队列,具备下列特征:
- 与镜像队列一样,都是主从模式,支持主从数据同步
- 使用非常简单,没有复杂的配置
- 主从同步基于Raft协议Raft协议详解,强一致