在RabbitMQ中如果不做任何配置的情况下,生产者是不知道消息是否真正到达RabbitMQ,也就是说消息发布操作不返回任何消息给生产者。如何保证我们 消息发布的可靠性?以下有几种常用的消息可靠性的机制。
生产者消息发布时的权衡
失败通知
在发送消息时设置mandatory标志,告诉RabbitMQ,如果消息不可路由,应该将消息返回给发送者,并通知失败。可以这样认为,开启mandatory是开启故障检测模式。
注意:它只会让RabbitMQ向你通知失败,而不会通知成功。如果消息正确路由到队列,则发布者不会受到任何通知。带来的问题是无法确保发布消息一定是成功的,因为通知失败的消息可能会丢失。
实现失败通知其实也很简单,只要在信道上添加失败通知的监听即可(channel.addReturnListener),例如下面的示例:
package com.kevin.task.exchange.direct;
import com.rabbitmq.client.BuiltinExchangeType;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
/**
* 描述:Direct交换器生产者-添加失败通知<br/>
* 创建人: Kevin Lea <br/>
* 创建时间: 2019-9-22 14:24<br/>
* 版本:1.0
*/
public class DirectProduct {
public final static String EXCHANGE_NAME = "direct_exchange";
public static void main(String[] args) throws IOException,TimeoutException {
//1.创建连接,连接到RabbitMQ
ConnectionFactory connectionFactory = new ConnectionFactory();
//2.设置连接地址,和端口,默认是5672
connectionFactory.setHost("172.0.0.1");
connectionFactory.setPort(5672);
connectionFactory.setUsername("test");
connectionFactory.setPassword("test");
connectionFactory.setVirtualHost("myhost");
//3.创建连接
Connection connection = connectionFactory.newConnection();
//4.创建信道
Channel channel = connection.createChannel();
//在信道上添加失败通知
channel.addReturnListener((int replyCode, String replyText, String exchange, String routingKey, AMQP.BasicProperties properties, byte[] body) ->{
System.out.println("返回码:" + replyCode);
System.out.println("返回信息:" + replyText);
System.out.println("交换器:" + exchange);
System.out.println("路由键:" + routingKey);
System.out.println("消息内容:" + new String(body,"UTF-8"));
});
//5.在信道中去设置交换器
channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.DIRECT);
//6.申明队列,可以在消费者地方申明,也可以在生产者地方申明,这里采用在消费者地方申明
//7.申明路由键
String[] routeKeys = {"apple","huawei","xiaomi"};
for(int i=0;i<routeKeys.length;i++){
String routeKey = routeKeys[i%3];
String msg = "I like " + routeKey + (i+1);
channel.basicPublish(EXCHANGE_NAME,routeKey,null,msg.getBytes());
System.out.println("Send:" + routeKey + ":" + msg);
}
channel.close();
connection.close();
}
}
事务
事务的实现主要是对信道(Channel)的设置,主要的方法有三个:
- channel.txSelect()声明启动事务模式;
- channel.txComment()提交事务;
- channel.txRollback()回滚事务;
在发送消息之前,需要声明channel为事务模式,提交或者回滚事务即可。
开启事务后,客户端和RabbitMQ之间的通讯交互流程:
• 客户端发送给服务器Tx.Select(开启事务模式)
• 服务器端返回Tx.Select-Ok(开启事务模式ok)
• 推送消息
• 客户端发送给事务提交Tx.Commit
• 服务器端返回Tx.Commit-Ok
以上就完成了事务的交互流程,如果其中任意一个环节出现问题,就会抛出IoException移除,这样用户就可以拦截异常进行事务回滚,或决定要不要重复消息。
那么,既然已经有事务了,为何还要使用发送方确认模式呢,原因是因为事务的性能是非常差的。根据相关资料,事务会降低2~10倍的性能,而且使用消息中间件的目的就是业务解耦和异步处理,使用事务就打破了这个条件,因为事务是同步的。所以在这里不推荐使用事务方式。
发布者确认
基于事务的性能问题,RabbitMQ团队为我们拿出了更好的方案,即采用发送方确认模式,该模式比事务更轻量,性能影响几乎可以忽略不计。
原理:生产者将信道设置成confirm模式,一旦信道进入confirm模式,所有在该信道上面发布的消息都将会被指派一个唯一的ID(从1开始),由这个id在生产者和RabbitMQ之间进行消息的确认。
不可路由的消息,当交换器发现,消息不能路由到任何队列,会进行确认操作,表示收到了消息。如果发送方设置了mandatory模式,则会先调用addReturnListener监听器。
可路由的消息,要等到消息被投递到所有匹配的队列之后,broker会发送一个确认给生产者(包含消息的唯一ID),这就使得生产者知道消息已经正确到达目的队列了,如果消息和队列是可持久化的,那么确认消息会在将消息写入磁盘之后发出,broker回传给生产者的确认消息中delivery-tag域包含了确认消息的序列号。
confirm模式最大的好处在于他可以是异步的,一旦发布一条消息,生产者应用程序就可以在等信道返回确认的同时继续发送下一条消息,当消息最终得到确认之后,生产者应用便可以通过回调方法来处理该确认消息,如果RabbitMQ因为自身内部错误导致消息丢失,就会发送一条nack消息,生产者应用程序同样可以在回调方法中处理该nack消息决定下一步的处理。
Confirm的三种实现方式:
方式一:channel.waitForConfirms()普通发送方确认模式;消息到达交换器,就会返回true。
方式二:channel.waitForConfirmsOrDie()批量确认模式;使用同步方式等所有的消息发送之后才会执行后面代码,只要有一个消息未到达交换器就会抛出IOException异常。
方式三:channel.addConfirmListener()异步监听发送方确认模式;
备用交换器
如果主交换器无法路由消息,那么消息将被路由到这个新的备用的交换器上。如果设置mandatory为true的失败通知了,这个时候如果出现无法路由的情况下,这个时候就不会触发失败通知,消息会通过备用交换器出去。
使用备用交换器,向往常一样,声明Queue和备用交换器,把Queue绑定到备用交换器上。然后在声明主交换器时,通过交换器的参数,alternate-exchange,,将备用交换器设置给主交换器。
建议备用交换器设置为faout类型,Queue绑定时的路由键设置为“#”
在主消费者只接收apple路由键的消息,那么其他的路由键将被路由到备用交换器里。
总结:生产者消息发布权衡如果想要投递消息越快那么可靠性越低,如果保证可靠性越高,那么速度就会相应的有所减慢。这个需要看具体使用场景来权衡。一般情况下使用失败通知+发布者确认+备用交换器就能完成比较高的可靠性消息投递,并且速度也不会太慢。