概念
消息如何保证100% 的投递
保障消息的成功发出
保障MQ节点的成功接收
发送端收到MQ节点(Broker) 确定应答
完善的消息进行补偿机制
常见解决方案
1、消息落库,对消息状态进行打标
数据存储在两个数据库(也可以用一个数据库)
BIZ DB 订单数据库
MSG DB 消息数据库
status:0 订单 -》进行中状态
status:1 订单 -》 订单完成状态
status:2 订单 -》 异常状态(尝试数(n)次未接收待订单完成的MQ消息)
Confirm LIstener 消息监听 (接受消息 ,更新消息状态)
MQ Broker
Sender
step1 : 将订单数据和消息数据分别入库 (如果此处失败则进行快速失败操作)
step2 :发送消息改MQ Broker
step3 :生产端接受待MQ Broker 发送过来的消息
step4 :更新消息的状态(更新数据库)
step5 :如果MSG DB中的消息数据状态一直未更新(分布式定时任务根据预先指定的规则抓取 MSG DB中没有确认消息数据) ,执行step6操作
step6 :重新发送消息1 step2、step3重新执行
step7 :重试数次(n次)如果不成功 消息状态改为 2 (由技术人员人工排查这些数据)
这种方式在高并发下不适合 ,数据库写入两次
方案二
Upstream sevice 上游服务 生产端
Downstrem service 下游服务 消费端
MQ Broker MQ集群
callback service 回调服务
MSG DB
BIZ DB
step1 : 业务消息入库 发送消息 (订单业务入库 BIZ DB) 发送消息给MQ(step1)
step2 :延迟消息投递检查 (第二条消息)
step3 :消费端接受消息并处理
step4 :Downstrem service 生成新的消息 (发送消息 确认)
step5 :callback service 收到 Downstrem service 发送来的确认消息 (查询消息数据库确认消息是否入库)
step6 :检查 消息数据库 (消息是否入库) 查询到给 订单服务发送消息
RPC Resend command :(没有在消息数据库中查询到消息 ) callback service 发送消息给订单服务 (消息库中没有查询到数据)
2、消息的延迟投递,做二次确认,回调检查
消费端的限流
为什么要对消费端限流
假设一个场景,首先,我们 Rabbitmq 服务器积压了有上万条未处理的消息,我们随便打开一个消费者客户端,会出现这样情况: 巨量的消息瞬间全部推送过来,但是我们单个客户端无法同时处理这么多数据!
当数据量特别大的时候,我们对生产端限流肯定是不科学的,因为有时候并发量就是特别大,有时候并发量又特别少,我们无法约束生产端,这是用户的行为。所以我们应该对消费端限流,用于保持消费端的稳定,当消息数量激增的时候很有可能造成资源耗尽,以及影响服务的性能,导致系统的卡顿甚至直接崩溃。
限流的 api 讲解
RabbitMQ 提供了一种 qos (服务质量保证)功能,即在非自动确认消息的前提下,如果一定数目的消息(通过基于 consume 或者 channel 设置 Qos 的值)未被确认前,不进行消费新的消息
void basicQos(int prefetchSize, int prefetchCount, boolean global) throws IOException;
- prefetchSize:0,单条消息大小限制,0代表不限制
- prefetchCount:一次性消费的消息数量。会告诉 RabbitMQ 不要同时给一个消费者推送多于 N 个消息,即一旦有 N 个消息还没有 ack,则该 consumer 将 block 掉,直到有消息 ack。
- global:true、false 是否将上面设置应用于 channel,简单点说,就是上面限制是 channel 级别的还是 consumer 级别。当我们设置为 false 的时候生效,设置为 true 的时候没有了限流功能,因为 channel 级别尚未实现。
- 注意:prefetchSize 和 global 这两项,rabbitmq 没有实现,暂且不研究。特别注意一点,prefetchCount 在 no_ask=false 的情况下才生效,即在自动应答的情况下这两个值是不生效的。
.如何对消费端进行限流
第一步,我们既然要使用消费端限流,我们需要关闭自动 ack,将 autoAck 设置为 falsechannel.basicConsume(queueName, false, consumer);
第二步我们来设置具体的限流大小以及数量。channel.basicQos(0, 15, false);
第三步在消费者的 handleDelivery 消费方法中手动 ack,并且设置批量处理 ack 回应为 truechannel.basicAck(envelope.getDeliveryTag(), true);
仔细查看一下 Consumer 的回调方法:
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
......
consumerChannel1.basicAck(envelope.getDeliveryTag(), false);
}
当我们需要确认一条消息已经被消费时,我们调用的 basicAck 方法的第一个参数是 Delivery Tag。
Delivery Tag 用来标识信道中投递的消息。RabbitMQ 推送消息给 Consumer 时,会附带一个 Delivery Tag,以便 Consumer 可以在消息确认时告诉 RabbitMQ 到底是哪条消息被确认了。
RabbitMQ 保证在每个信道中,每条消息的 Delivery Tag 从 1 开始递增。
RabbitMQ 消息消费并没有超时机制,也就是说,程序不重启,消息就永远是 Unacked 状态。处理运维事件时不要忘了这些 Unacked 状态的消息。当程序关闭时(实际只要 Consumer 关闭就行),这3条消息会恢复为 Ready 状态。
消息的ACK与重回队列
消费端的手工ACK 与 NACK
-
消费端进行消费的时候,如果由于业务异常导致失败了,返回 NACK 达到最大重试次数,此时我们可以进行日志的记录,然后手动 ACK 回去,最后对这个记录进行补偿。
-
或者由于服务器宕机等严重问题,导致 ACK 和 NACK 都没有,那我们就需要手工进行 ACK 保障消费端消费成功,再通过补偿机制补偿。
消费端的重回队列
- 消费端的重回队列是为了对没有处理成功的消息,把消息重新递给 broker
- 但是在我们的实际生产,一般都会关闭重回队列,也就是设置为false
if((Integer)properties.getHeaders().get("num") == 0) {
//requeue true 是否重回队列,重回队列后会添加到队列的末端 ,重新发送(不建议使用)
channel.basicNack(envelope.getDeliveryTag(), false, true);
} else {
//false 表示不批量确认
channel.basicAck(envelope.getDeliveryTag(), false);
}
TTL消息
- TTL 是 Time To Live 的缩写,也就是生存时间
- RabbitMQ支持消息的过期时间,在消息发送时可指定过期时间
- RabbitMQ支持队列的过期时间,从消息入队列开始计算,只要超过队列的超时时间配置,那么消息会自动的消除
死信队列
利用DLX(死信队列 dead-letter-exchange)当消息在一个队列中变成死信(dead message)之后,他被重新publish到另外一个Exchage,这个 Exchage就是DLX
消息变成死信有一下几种情况
- 消息被拒绝(basic.reject/basic.ncak) 并且request =false
- 消息TTL过期
- 队列到达最大长度
死信队列在开发中很重要 接收无接受的的队列消息 进行补偿处理