RabbitMq可以使用事务,confim、持久化等机制保障消息的可靠性。
1、消息持久化
如上图,在Features字段里有一个D,就是持久化队列,英文durable(持久的)。
持久化队列会被保存在磁盘中,固定并持久的存储,当Rabbit服务重启后,该队列会保持原来的状态在RabbitMQ中被管理,而非持久化队列不会被保存在磁盘中,Rabbit服务重启后队列就会消失。
如果需要队列的完整性,数据在队列中的保存是必须不允许丢失的,那么可以使用持久化。而当需要获取的信息是实时的,或者是随机的信息,不需要信息的精确性或完整性,但是追求获取性能,可以选择非持久化队列。
2、事务
对事务的支持是AMQP协议的一个重要特性。假设当生产者将一个持久化消息发送给服务器时,因为consume命令本身没有任何Response返回,所以即使服务器崩溃,没有持久化该消息,生产者也无法获知该消息已经丢失。如果此时使用事务,即通过txSelect()开启一个事务,然后发送消息给服务器,然后通过txCommit()提交该事务,即可以保证,如果txCommit()提交了,则该消息一定会持久化,如果txCommit()还未提交即服务器崩溃,则该消息不会服务器接收。当然Rabbit MQ也提供了txRollback()命令用于回滚某一个事务。
3、Confirm机制
使用事务固然可以保证只有提交的事务,才会被服务器执行。但是这样同时也将客户端与消息服务器同步起来,这背离了消息队列解耦的本质。Rabbit MQ提供了一个更加轻量级的机制来保证生产者可以感知服务器消息是否已被路由到正确的队列中——Confirm。如果设置channel为confirm状态,则通过该channel发送的消息都会被分配一个唯一的ID,然后一旦该消息被正确的路由到匹配的队列中后,服务器会返回给生产者一个Confirm,该Confirm包含该消息的ID,这样生产者就会知道该消息已被正确分发。对于持久化消息,只有该消息被持久化后,才会返回Confirm。Confirm机制的最大优点在于异步,生产者在发送消息以后,即可继续执行其他任务。而服务器返回Confirm后,会触发生产者的回调函数,生产者在回调函数中处理Confirm信息。如果消息服务器发生异常,导致该消息丢失,会返回给生产者一个nack,表示消息已经丢失,这样生产者就可以通过重发消息,保证消息不丢失。Confirm机制在性能上要比事务优越很多。但是Confirm机制,无法进行回滚,就是一旦服务器崩溃,生产者无法得到Confirm信息,生产者其实本身也不知道该消息是否已经被持久化,只有继续重发来保证消息不丢失,但是如果原先已经持久化的消息,并不会被回滚,这样队列中就会存在两条相同的消息,系统需要支持去重。
4、消息确认:Message acknowledgment
在实际应用中,可能会发生消费者收到Queue中的消息,但没有处理完成就宕机(或出现其他意外)的情况,这种情况下就可能会导致消息丢失。为了避免这种情况发生,我们可以要求消费者在消费完消息后发送一个回执给RabbitMQ,RabbitMQ收到消息回执(Message acknowledgment)后才将该消息从Queue中移除;如果RabbitMQ没有收到回执并检测到消费者的RabbitMQ连接断开,则RabbitMQ会将该消息发送给其他消费者(如果存在多个消费者)进行处理。这里不存在Timeout概念,一个消费者处理消息时间再长也不会导致该消息被发送给其他消费者,除非它的RabbitMQ连接断开。 这里会产生另外一个问题,如果我们的开发人员在处理完业务逻辑后,忘记发送回执给RabbitMQ,这将会导致严重的问题,Queue中堆积的消息会越来越多,消费者重启后会重复消费这些消息并重复执行业务逻辑。 如果我们采用no-ack的方式进行确认,也就是说,每次Consumer接到数据后,而不管是否处理完成,RabbitMQ会立即把这个Message标记为完成,然后从queue中删除了。
5、代码示例
5.1、事务+持久化保证消息安全发送
5.1.1、 声明队列、交换器和绑定关系
@Configuration
public class TxConfig {
/**
* 声明一个持久化的队列
*
* @return
*/
@Bean
public Queue txQueue() {
return new Queue("queue.tx.order", true);
}
/**
* 声明一个持久化的 direct exchange
*
* @return
*/
@Bean
public Exchange txExchange() {
return new DirectExchange("ex.tx.order", true, false);
}
@Bean
public Binding txBinding(Exchange txExchange, Queue txQueue) {
return BindingBuilder.bind(txQueue).to(txExchange).with("").noargs();
}
5.1.2、事务方式发送消息
/**
* @author gejt
*/
@RestController
public class TxController {
@Resource
private ConnectionFactory connectionFactory;
/**
* 使用事务方式发送消息
*
* @param msg
* @return
*/
@GetMapping("tx/send")
public Object txSend(String msg) throws IOException {
Connection connection = connectionFactory.createConnection();
Channel channel = connection.createChannel(true);
try {
channel.txSelect();
System.out.println("tx msg send:" + msg);
channel.basicPublish("ex.tx.order", "", MessageProperties.PERSISTENT_BASIC, msg.getBytes());
//int i = 1/0;
channel.txCommit();
System.out.println("tx msg send:" + msg + ",send ok");
} catch (Exception e) {
channel.txRollback();
System.out.println("tx msg send:" + msg + ",send fail");
System.out.println(e);
} finally {
try {
channel.close();
connection.close();
} catch (TimeoutException e) {
e.printStackTrace();
}
}
return "ok";
}
/**
* 手动确认消息
*
* @param message
* @param channel
* @throws IOException
*/
@RabbitListener(queues = "queue.tx.order", ackMode = "MANUAL")
public void txMessageListener(Message message, Channel channel) throws IOException {
// 采用手动应答模式, 手动确认应答更为安全稳定
long deliveryTag = message.getMessageProperties().getDeliveryTag();
System.out.println("txMessageListener tag:" + deliveryTag + ",message:" + new String(message.getBody()));
// 第一个参数是消息标识, 第二个是批量确认; false当前消息确认, true此次之前的消息确认
channel.basicAck(deliveryTag, false);
// 消费失败,消息重返队列
//channel.basicNack(deliveryTag,false,true);
}
5.1.3、结果演示
tx msg send:nick
tx msg send:nick,send ok
txMessageListener tag:1,message:nick
tx msg send:nick
txMessageListener tag:2,message:nick
tx msg send:nick,send ok
5.2、Confirm机制+持久化保证消息安全发送
5.2.1、配置文件中增加如下配置
# 支持消息发送失败后重返队列
spring.rabbitmq.publisher-returns=true
# 开启消息确认机制
spring.rabbitmq.publisher-confirm-type=correlated
5.2.2、声明队列、交换器和绑定关系
@Configuration
public class ConfirmConfig {
/**
* 声明一个持久化的队列
*
* @return
*/
@Bean
public Queue cfQueue() {
return new Queue("queue.cf.order", true);
}
/**
* 声明一个持久化的 direct exchange
*
* @return
*/
@Bean
public Exchange cfExchange() {
return new DirectExchange("ex.cf.order", true, false);
}
@Bean
public Binding cfBinding(Exchange cfExchange, Queue cfQueue) {
return BindingBuilder.bind(cfQueue).to(cfExchange).with("").noargs();
}
}
5.2.3、confirm方式发送消息
@RestController
public class ConfirmController implements RabbitTemplate.ReturnCallback, RabbitTemplate.ConfirmCallback {
@Resource
RabbitTemplate rabbitTemplate;
@PostConstruct
public void init() {
rabbitTemplate.setReturnCallback(this);
rabbitTemplate.setConfirmCallback(this);
}
@GetMapping("cf/send")
public Object confirmSend(String msg){
System.out.println("confirm msg send:" + msg);
rabbitTemplate.setMandatory(true);
rabbitTemplate.convertAndSend("ex.cf.order","",msg);
System.out.println("confirm msg send:" + msg + ",send ok");
return "ok";
}
@Override
public void returnedMessage(Message message, int replyCode, String replyText, String exchange, String routingKey) {
System.out.println("这条消息发送失败了"+message+",请处理");
System.out.println("这条消息发送失败了"+replyText+",请处理");
System.out.println("这条消息发送失败了"+exchange+",请处理");
System.out.println("这条消息发送失败了"+routingKey+",请处理");
}
@Override
public void confirm(CorrelationData correlationData, boolean ack, String cause) {
if(ack){
System.out.println("确认了这条消息:"+correlationData);
}else{
System.out.println("确认失败了:"+correlationData+";出现异常:"+cause);
}
}
@RabbitListener(queues = "queue.cf.order", ackMode = "MANUAL")
public void cfMessageListener(Message message, Channel channel) throws IOException {
// 采用手动应答模式, 手动确认应答更为安全稳定
long deliveryTag = message.getMessageProperties().getDeliveryTag();
System.out.println("cfMessageListener tag:" + deliveryTag + ",message:" + new String(message.getBody()));
// 第一个参数是消息标识, 第二个是批量确认; false当前消息确认, true此次之前的消息确认
channel.basicAck(deliveryTag, false);
// 消费失败,消息重返队列
//channel.basicNack(deliveryTag,false,true);
}
}
5.2.4、结果演示
confirm msg send:nick
confirm msg send:nick,send ok
确认了这条消息:null
cfMessageListener tag:1,message:nick
confirm msg send:nick
confirm msg send:nick,send ok
cfMessageListener tag:2,message:nick
确认了这条消息:null
5.3、手动确认消息保证消息不丢失
@RabbitListener(queues = "queue.cf.order", ackMode = "MANUAL")
public void cfMessageListener(Message message, Channel channel) throws IOException {
// 采用手动应答模式, 手动确认应答更为安全稳定
long deliveryTag = message.getMessageProperties().getDeliveryTag();
System.out.println("cfMessageListener tag:" + deliveryTag + ",message:" + new String(message.getBody()));
// 第一个参数是消息标识, 第二个是批量确认; false当前消息确认, true此次之前的消息确认
channel.basicAck(deliveryTag, false);
// 消费失败,消息重返队列
//channel.basicNack(deliveryTag,false,true);
}