RabbitMQ 的消息可靠性
通常情况下,在某些领域消息的可靠性必须高,特别像金融领域,涉及到金额相关的消息必须确保百分百确认,那么如果通过MQ来进行消息的确认。通常消息确认流程如下图所示:
为什么要进行消息确认?
在RabbitMQ中,消费者和生产者并不直接进行通信,生产者只负责把消息发送到交换机或者队列,消费者只负责从队列获取消息。
-
消费者从队列拉取到推送到消息后,这条消息就不存在队列中,但是消费者建立与broder建立的长连接突然中断,这条消息没有被消费到,那么这条消息就丢失了,所以,通常习惯上都需要等待消费者成功消费返回确认,在删除这条消息。
-
生产者发送消息,需要发送给交换机,等待交换机的确认才能保证消息的完全到达,否则跟第一条一样消息丢失。
消息可靠性的基本方法
- 1.客户端代码中的异常捕获,包括生产者和消费者
- 2.AMQP/RabbitMQ的事务机制
- 3.发送端确认机制
- 4.消息持久化机制
- 5.Broker端的高可用集群
- 6.消费者确认机制
- 7.消费端限流
- 8.消息幂等性
异常捕获处理
- 基本流程
先执行行业务操作,业务操作成功后执行行消息发送,消息发送过程通过trycatch方法进行捕获异常,在异常处理的代码块中执行行回滚业务操作或者执行行重发操作等。
trycatch方法是一种最大努力确保的方式,并无法保证100%绝对可靠,因为这里没有异常并不代表消息就一定投递成功。
- 示例代码
boolean result = doBiz();
if(result){
try {
//调用投递消息
sendMsg();
}catch (Exception e){
//异常处理,回滚消息等方法
rollbackBiz();
}
}
AMQP/RabbitMQ的事务机制
RabbitMQ的事务机制需要每个消息或每组消息发布提交的通道设置为事务性的,非常耗费性能,降低了消息吞吐量,RabbitMQ的事务机制没有捕捉到并不代表消息一定发送成功,如果发送消息后一直到事务提交后都没有异常。就说明消息发送成功。但是,这种方式在性能方面的开销比较大,一般也不推荐使用。
- 示例代码
try {
//将channel设置成为事务模式
channel.txSelect();
String message = "hello rabbit 666";
//发送消息到交换器
channel.basicPublish(RabbitConstant.QUEUE_HELLOWORLD,,"",null,message.getBytes(StandardCharsets.UTF_8));
//只有消息成功被broker接受后才能算是提交成功
channel.txCommit();
}catch (Exception e){
e.printStackTrace();
}
发送方消息确认机制
- 基础概念
RabbitMQ也提供了一种机制叫发送方确认(publisher confirm)机制。
生产者将channel设置成confirm(确认)模式,一旦channel进入confirm模式,所有在该信道上⾯面发布的消息都会被指派一个唯一的ID(从1开始),一旦消息被投递到所有匹配的队列之后(如果消息和队列是持久化的,那么确认消息会在消息持久化后发出),RabbitMQ就会发送一个确认(Basic.Ack)给生产者(包含消息的唯一ID),这样生产者就知道消息已经正确送达了。
- 流程
-
图解:
-
producer通过channel1发送三个消息到RabbitMQ中,如果MQ全部接收到则响应一条nack命令。ack包含deliveryTag和mutlipte,deliveryTag表示最后返回消息的编号,mutlipte为true表示比deliveryTag小的消息都确认。
-
producer通过channel2发送三个消息到RabbitMQ中,如果MQ全部接收到则响应一条nack命令。ack包含deliveryTag和mutlipte,deliveryTag表示最后返回消息的编号,mutlipte为false表示比deliveryTag小的消息都确认。
-
RabbitMQ回传给生产者的确认消息中的deliveryTag字段包含了确认消息的序号,另外,通过设置channel.basicAck方法中的multiple参数,表示到这个序号之前的所有消息是否都已经得到了处理了。
生产者投递消息后并不需要一直阻塞着,可以继续投递下一条消息并通过回调方式处理理ACK响应。如果RabbitMQ因为自身内部错误导致消息丢失等异常情况发生,就会响应一条nack(Basic.Nack)命令,生产者应用程序同样可以在回调方法中处理理该nack命令。
- 代码
public class PublisherConfirmsProducers01 {
public static void main(String[] args) throws IOException, TimeoutException {
//创建长连接
Connection connection = RabbitUtils.getConnection();
//创建信道
Channel channel = connection.createChannel();
//向rabbitmq服务器发送amqp命令,将当前channel标记为发送方确认通道
channel.confirmSelect();
channel.queueDeclare(RabbitConstant.QUEUE_NAME_PC, true, false, false, null);
channel.exchangeDeclare(RabbitConstant.EXCHANGE_NAME_PC, "direct", true, false, null);
channel.queueBind(RabbitConstant.QUEUE_NAME_PC, RabbitConstant.EXCHANGE_NAME_PC, RabbitConstant.ROUTING_KEY_NAME_PC);
//发送消息
String message = "hello";
channel.basicPublish("ex.pc", "key.pc", null, message.getBytes());
//同步方式等待MQ确认消息
try {
channel.waitForConfirmsOrDie(5_000);
System.out.println(" 发送消息已确认:message= " + message);
} catch (IOException e) {
e.printStackTrace();
System.err.println(" 消息被拒绝! mesmess};");
} catch (InterruptedException e) {
e.printStackTrace();
System.err.println("发送消息的通道不是PublisherConfirms通道");
} catch (TimeoutException e) {
e.printStackTrace();
System.err.println("等待消息超时,message=" + message);
}
channel.close();
connection.close();
}
}
waitForConfirm方法有个重载的,可以自定义timeout超时时间,超时后会抛TimeoutException。
类似的有几个waitForConfirmsOrDie方法,Broker端在返回nack(Basic.Nack)之后该方法会抛出java.io.IOException。需要根据异常类型来做区别处理理,TimeoutException超时是属于第三状态(无法确定成功还是失败),而返回Basic.Nack抛出IOException这种是明确的失败。
public class PublisherConfirmsProducers03 {
public static void main(String[] args) throws IOException, TimeoutException, InterruptedException {
//创建长连接
Connection connection = RabbitUtils.getConnection();
//创建信道
Channel channel = connection.createChannel();
//向rabbitmq服务器发送amqp命令,将当前channel标记为发送方确认通道
channel.confirmSelect();
channel.queueDeclare(RabbitConstant.QUEUE_NAME_PC, true, false, false, null);
channel.exchangeDeclare(RabbitConstant.EXCHANGE_NAME_PC, "direct", true, false, null);
channel.queueBind(RabbitConstant.QUEUE_NAME_PC, RabbitConstant.EXCHANGE_NAME_PC, RabbitConstant.ROUTING_KEY_NAME_PC);
ConfirmCallback clearOutstandingConfirms1 = new ConfirmCallback() {
@Override
public void handle(long deliveryTag, boolean multiple) throws IOException {
if (multiple) {
System.out.println("编号小于等于 " + deliveryTag + " 的消息都已经被确认了");
} else {
System.out.println("编号为:" + deliveryTag + " 的消息被确认");
}
}
};
ConcurrentNavigableMap<Long, String> outstandingConfirms = new ConcurrentSkipListMap<>();
ConfirmCallback clearOutstandingConfirms = (deliveryTag, multiple) -> {
if (multiple) {
System.out.println("编号小于等于 " + deliveryTag + " 的消息都已经被确认了");
final ConcurrentNavigableMap<Long, String> headMap
= outstandingConfirms.headMap(deliveryTag, true);
// 清空outstandingConfirms中已经被确认的消息信息
headMap.clear();
} else {
// 移除已经被确认的消息
outstandingConfirms.remove(deliveryTag);
System.out.println("编号为:" + deliveryTag + " 的消息被确认");
}
};
// 设置channel的监听器,处理确认的消息和不确认的消息
channel.addConfirmListener(clearOutstandingConfirms, (deliveryTag, multiple) -> {
if (multiple) {
// 将没有确认的消息记录到一个集合中
System.out.println("消息编号小于等于:" + deliveryTag + " 的消息 不确认");
ConcurrentNavigableMap<Long, String> headMap = outstandingConfirms.headMap(deliveryTag,
true);
} else {
System.out.println("编号为:" + deliveryTag + " 的消息不确认");
outstandingConfirms.remove(deliveryTag);
}
});
String message = "hello-";
for (int i = 0; i < 1000; i++) {
// 获取下一条即将发送的消息的消息ID
final long nextPublishSeqNo = channel.getNextPublishSeqNo();
channel.basicPublish("ex.pc", "key.pc", null, (message + i).getBytes());
System.out.println("编号为:" + nextPublishSeqNo + " 的消息已经发送成功,尚未确认");
outstandingConfirms.put(nextPublishSeqNo, (message + i));
}
// 等待消息被确认
Thread.sleep(10000);
channel.close();
connection.close();
}
}
消息持久化机制
持久化是提高RabbitMQ可靠性的基础,如果当RabbitMQ遇到异常时(如:重启、断电、停机等情况)数据将会丢失。
数据将会丢失。
-
持久性的方法
-
- Exchange(交换机)的持久化。通过定义交换机时设置durable为ture来保证Exchange相关的数据不丢失。
-
- Queue(队列)的持久化。通过定义队列时设置durable为ture来保证Queue相关的数据不丢失。
-
3.消息的持久化。通过生产者将消息的投递模式(BasicProperties中的deliveryMode属性)设置为2即可实现消息的持久化,保证发送的消息自身不丢失。
-
-
代码
public static void main(String[] args) throws IOException {
Connection connection = RabbitUtils.getConnection();
Channel channel = connection.createChannel();
//持久化队列
channel.queueDeclare(RabbitConstant.QUEUE_NAME_PERSISTENT, true, false, false, null);
//持久化交换机
channel.exchangeDeclare(RabbitConstant.EXCHANGE_NAME_PERSISTENT, "direct", true, false, null);
channel.queueBind(RabbitConstant.QUEUE_NAME_PC, RabbitConstant.EXCHANGE_NAME_PERSISTENT, RabbitConstant.ROUTING_KEY_NAME_PERSISTENT);
/**
deliveryMode 参数2 是持久化消息
*/
AMQP.BasicProperties properties = new AMQP.BasicProperties.Builder()
.deliveryMode(2).build();
String message = "hello";
channel.basicPublish(RabbitConstant.EXCHANGE_NAME_PERSISTENT, RabbitConstant.ROUTING_KEY_NAME_PERSISTENT, properties, message.getBytes());
}
RabbitMQ中的持久化消息都需要写入磁盘(当系统内存不不足时,非持久化的消息也会被刷盘处理理),这些处理理动作都是在“持久层”中完成的。
-
持久层的基本定义
-
1.队列索引(rabbit_queue_index),rabbit_queue_index负责维护Queue中消息的信息,包括消息的存储位置、是否已交给消费者、是否已被消费及Ack确认等,每个Queue都有与之对应的rabbit_queue_index。
-
2.消息存储(rabbit_msg_store),rabbit_msg_store以键值对的形式存储消息,它被所有队列列共享,在每个节点中有且只有一个。
-
Broker端的高可用集群
Broker端的高可用集群下篇详写
消费者确认机制
RabbitMQ在除了在生产者端有确认机制,那消费端会有Ack机制,即消费端消费消息后需要发送Ack确认报文给Broker端,告知自己是否已消费完成,否则可能会一直重发消息直到消息过期(AUTO模式)。
-
ack的相关模式
-
1.采用NONE模式,消费的过程中自行捕获异常,引发异常后直接记录日志并落到异常恢复表,再通过后台定时任务扫描异常恢复表尝试做重试动作。如果业务不自行处理则有丢失数据的风险
-
2.采用AUTO(自动Ack)模式,不主动捕获异常,当消费过程中出现异常时会将消息放回Queue中,然后消息会被重新分配到其他消费者节点(如果没有则还是选择当前节点)重新被消费,默认会一直重发消息并直到消费完成返回Ack或者一直到过期
-
3.采用MANUAL(手动Ack)模式,消费者自行控制流程并手动调用channel相关的方法返回Ack
-
-
代码
public static void main(String[] args) throws IOException {
Connection connection = RabbitUtils.getConnection();
Channel channel = connection.createChannel();
channel.queueDeclare("queue.ca", false, false, false, null);
//ack设置为false,手动确认消息
channel.basicConsume(
"queue.ca",
false,
"myConsumer",
new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag,
Envelope envelope,
AMQP.BasicProperties properties,
byte[] body) throws IOException {
System.out.println(new String(body));
//确认消息
channel.basicAck(envelope.getDeliveryTag(), false);
/**
* 拒绝消息,主要用于拒收多条消息
*
* 1:消息的标签
* 2:拒绝消息确认
* 3:将拒绝的消息重新发送到队列中(true表示重新入列,false表示不重发)
*/
channel.basicNack(envelope.getDeliveryTag(), false, true);
//拒收一条消息
channel.basicReject(envelope.getDeliveryTag(), true);
}
});
}
消费端限流
- 基本概念
如果当生产者发送消息到队列中的消息远远大于消费者从队列取出消息消费度的速度,那么随着长时间的积累,就可能会出现消息挤压的问题。
消息中间件本身是具备一定的缓冲能力的,但消息缓冲能力是有容量限制的,如果长期运行并没有任何处理,最终会导致Broker崩溃。
- RabbitMQ配置文件处理限流操作
RabbitMQ可以对内存和磁盘使用量设置阈值,当达到阈值后,生产者将被阻塞(block),直到对应项指标恢复正常。
全局上可以防止超大流量、消息积压等导致的Broker被压垮。当内存受限或磁盘可用空间受限的时候,服务器都会暂时阻止连接,服务器将暂停从发布消息的已连接客户端的套接字读取数据。连接心跳监视也将被禁用。
所有网络连接将在rabbitmqctl和管理插件中显示为“已阻止”,这意味着它们尚未尝试发布,因此可以继续或被阻止,这意味着它们已发布,现在已暂停。兼容的客户端被阻止时将收到通知。
- RabbitMQ的QOS保护机制
RabbitMQ中的QoS保证机制,QoS保证机制可以限制Channel上接收到的未被Ack的消息数量,如果超过这个数量限制RabbitMQ将不会再往消费端推送消息。
这是一种流控手段,可以防止大量消息瞬时从Broker送达消费端造成消费端巨大压力(甚至压垮消费端)。
QoS机制仅对于消费端推模式有效,对拉模式无效。而且不支持NONE Ack模式。
执行channel.basicConsume方法之前通过channel.basicQoS方法可以设置该数量
。消息的发送是异步的,消息的确认也是异步的。在消费者消费慢的时候,可以设置Qos的prefetchCount,它表示broker在向消费者发送消息的时候,一旦发送了prefetchCount个消息而没有一个消息确认的时候,就停止发送。消费者确认一个,broker就发送一个,确认两就发送两个。换句话说,消费者确认多少,broker就发送多少,消费者等待处理的个数永远限制在prefetchCount个。
如果对于每个消息都发送确认,增加了网络流量,此时可以批量确认消息。如果设置了multiple为true,消费者在确认的时候,比如说id是8的消息确认了,则在8之前的所有消息都确认了。
生产者往往是希望自己产生的消息能快速投递出去,而当消息投递太快且超过了下游的消费速度时就容易出现消息积压/堆积,所以,从上游来讲我们应该在生产端应用程序中也可以加入限流、应急开关等控制手段,避免超过Broker端的极限承载能力或者压垮下游消费者。再看看下游,我们期望下游消费端能尽快消费完消息,而且还要防止瞬时大量消息压垮消费端(推模式),我们期望消费端处理速度是最快、最稳定而且还相对均匀(比较理想化)。
我们期望消费端处理速度是最快、最稳定而且还相对均匀(比较理想化)。
-
提升下游应用的吞吐量和缩短消费过程的耗时,优化主要以下几种方式:
- 1.优化应用程序的性能,缩短响应时间(需要时间)
- 2.增加消费者节点实例(成本增加,而且底层数据库操作这些也可能是瓶颈)
- 3.调整并发消费的线程数(线程数并非越大越好,需要大量压测调优至合理值)
-
代码
public class Consumer {
public static void main(String[] args) throws IOException {
Connection connection = RabbitUtils.getConnection();
Channel channel = connection.createChannel();
channel.queueDeclare("queue.qos", false, false, false, null);
//使用basic做限流,仅对消息推送模式生效
//表示qos是10个消息,最多有10个消息等待
channel.basicQos(10);
//表示最多10个消息等待确认,如果global设置为true,则表示只要使用当前channel的Consumer都生效,false经表示当前Consumer
channel.basicQos(10, false);
//prefetchSize表示未确认消息的大小,rabbit没有实现不用管
channel.basicQos(1000, 10, true);
channel.basicConsume("queue.qos", false, new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag,
Envelope envelope,
AMQP.BasicProperties properties,
byte[] body) throws IOException {
//消息确认(批量处理),减少网络流量负载
channel.basicAck(envelope.getDeliveryTag(), true);
}
});
}
}