概述
在RabbitMQ broker因为某些原因崩溃、重启时,可以确保消息不会丢失。但是我们发送完消息之后,并不知道消息有没有真的发到了RabbitMQ服务器上并存储完毕,如果因为网络闪断等原因导致消息没有发到服务器上,或者RabbitMQ服务器发生内部错误导致持久化失败,这样就会导致消息丢失。针对生产者发送消息的确认问题,RabbitMQ提供了如下两种方式(注意:事务机制跟confirm机制两者是互斥的,如果已经开启了其中一种,再去开启另外一种会报错的)
首先明白一种机制,Ack 消息确认机制
public static void main(String[] args) throws IOException, TimeoutException, IOException, TimeoutException {
// 1.创建连接
Connection connection = RabbitMQConnection.getConnection();
// 2.设置通道
Channel channel = connection.createChannel();
DefaultConsumer defaultConsumer = new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
String msg = new String(body, "UTF-8");
System.out.println("消费者获取消息:" + msg);
// 消费者完成 消费该消息
channel.basicAck(envelope.getDeliveryTag(), false);
}
};
// 3.监听队列
//autoAck 为true 自动签收 消费着只要获取到消息mq服务器自动给移除掉 不安全 因为不确定消费者是否消费成功
//autoAck false 手动签收 需要通过 channel.basicAck(envelope.getDeliveryTag(), false);
channel.basicConsume(QUEUE_NAME, false, defaultConsumer);
}
生成者角色
1,事务机制
提起事务,想必大家都很熟悉,在我们使用关系型数据库的时候经常使用事务,使用方法一般都是:先开启事务,然后操作数据,操作数据完成提交事务,如果操作失败进行事务的回滚。
RabbitMQ的事务机制操作过程跟上面的有点类似,主要有三个方法:
channel.txSelect() 用于开启事务
channel.txCommit() 用于提交事务
channel.txRollback() 用于回滚事务
public static void main(String[] args) throws IOException, TimeoutException {
//1.创建一个新连接
Connection connection = RabbitMQConnection.getConnection();
//2.设置channel
Channel channel = connection.createChannel();
try {
//3.发送消息
String msg = "6666222";
channel.txSelect();
channel.basicPublish("", QUEUE_NAME, null, msg.getBytes());
int i = 1/ 0 ;
channel.txCommit();
System.out.println("消息投递成功");
channel.close();
connection.close();
} catch (Exception e) {
if (channel!=null){
channel.txRollback();
}
System.out.println("消息投递失败");
}
}
只有消息被成功发送到RabbitMQ的交换机后事务才能够提交,否则捕获异常回滚事务,回滚事务之后也可以继续重发消息。事务机制是阻塞的,发送消息之后要一直等待RabbitMQ的回应,否则就无法发送下一条,其效率是最低的,不建议使用。
2,confirm机制 即publish confirm模式
上面介绍了事务机制,但是效率太低了。RabbitMQ还提供了一种生产者确认(publisher confirm)的模式,消息生产者可以通过 channel.confirmSelect() 方法把channel开启confirm模式,通过confirm模式的channel发布的消息都会指定一个唯一的消息ID(也就是deliveryTag,从1开始递增)。消息被发到RabbitMQ后,RabbitMQ会给生产者发送消息,消息内容有:发送消息时传递过去的deliveryTag;一个标志Ack/Nack(Ack表示成功发到了RabbitMQ交换机上,Nack表示发送失败);还有一个multiple参数表示是否是批量确认,如果为false则表示单条确认,如果为true则表示到这个序号之前的所有消息都己经得到了处理
事务模式吞吐量较低的原因是生产者每发送一条消息只能同步等待事务提交,然后才可以发送下一条。而confirm机制可以异步的处理,在生产者发送一条消息之后,可以在等RabbitMQ发送确认消息同时继续发送消息。RabbitMQ收到消息之后会发送一条Ack消息;如果消息服务器出现内部错误等原因导致消息丢失,会发送一条Nack消息。
注意:极少会出现nack的情况,一般都会返回ack的。要注意区分一下这里的ack跟上一节消费者消费消息时的ack,不要搞混了,消费者ack是表示消费者消费消息成功了,生产者收到RabbitMQ的ack表示生产者的消息成功投递到了RabbitMQ上了。
生产者confirm模式使用方式总共有三种: 单条confirm模式、批量confirm模式、异步confirm模式,这三种模式的开启方法都是 channel.confirmSelect()。
1,单条confirm
单条confirm模式就是发送一条等待确认一条,使用方式如下:在每发送一条消息就调用channel.waitForConfirms()方法,该方法等待直到自上次调用以来发布的所有消息都已被ack或nack,如果返回false表示消息投递失败,如果返回true表示消息投递成功。注意,如果当前信道没有开启confirm模式,调用waitforconfirms将引发IllegalstateExcep
public class Producer {
private static final String QUEUE_NAME = "yao-queue";
//Confirm 提交
public static void main(String[] args) {
try {
//1.创建一个新连接
Connection connection = RabbitMQConnection.getConnection();
//2.设置channel
Channel channel = connection.createChannel();
//3.发送消息
String msg = "6666222";
channel.confirmSelect();
channel.basicPublish("", QUEUE_NAME, null, msg.getBytes());
boolean result = channel.waitForConfirms();
if (result) {
System.out.println("消息投递成功");
} else {
System.out.println("消息投递失败");
}
channel.close();
connection.close();
} catch (Exception e) {
}
}
}
2,批量confirm
批量confirm模式就是先开启confirm模式,发送多条之后再调用waitForConfirms()方法确认,这样发送多条之后才会等待一次确认消息,效率比单条confirm模式高了许多。但是如果返回false或者超时,这一批次的消息就要全部重发,如果经常丢消息,效率并不比单条confirm高
channel.confirmSelect();//将信道置为confirm模式
for(int i=0;i<5;i++){
String message = "批量confirm消息"+i;
channel.basicPublish("", QUEUE_NAME, null, message .getBytes());
}
if(!channel.waitForConfirms()){
System.out.println("消息发送失败");
//进行重发等操作
}
System.out.println("消息发送成功");
3,异步confirm
异步confirm模式是通过channel.addConfirmListener(ConfirmListener listener)方式实现的,ConfirmListener中提供了两个方法handleAck(long deliveryTag, boolean multiple) 和 handleNack(long deliveryTag, boolean multiple),两者分别对应RabbitMQ发送给生产者的ack和nack。方法中的deliveryTag就是上面说的发送消息的序号;multiple参数表示是否是批量确认。
异步confirm模式使用起来最为复杂,因为要自己维护一个已发送消息序号的集合,当收到RabbitMQ的confirm回调时需要从集合中删除对应的消息(multiple为false则删除一条,为true则删除多条),上面我们说过开启confirm模式后,channel上发送消息都会附带一个从1开始递增的deliveryTag序号,所以我们可以使用SortedSet的有序特性来维护这个发送序号集合:每次获取发送消息的序号存入集合,当收到ack时,如果multiple为false,则从集合中删除当前deliveryTag元素,如果multiple为true,则将集合中小于等于当前序号deliveryTag元素的集合清除,表示这批序号的消息都已经被ack了;nack的处理逻辑与此类似,只不过要结合具体的业务情况进行消息重发等操作。
public class SendAsynConfirm {
private final static String EXCHANGE_NAME = "fanout_exchange";
//TreeSet是有序集合,元素使用其自然顺序进行排序,拥有存储需要confirm确认的消息序号
static SortedSet<Long> confirmSet = Collections.synchronizedSortedSet(new TreeSet<Long>());
public static void main(String[] argv) throws Exception{
// 获取到连接以及mq通道
Connection connection = ConnectionUtil.getConnection();
// 从连接中创建通道
Channel channel = connection.createChannel();
// 声明交换机
channel.exchangeDeclare(EXCHANGE_NAME, "fanout",true);
//声明队列
channel.queueDeclare("test_queue", true, false, false, null);
//绑定
channel.queueBind("test_queue", EXCHANGE_NAME, "");
channel.confirmSelect();//将信道置为confirm模式
channel.addConfirmListener(new ConfirmListener() {
public void handleNack(long deliveryTag, boolean multiple)
throws IOException {
if (multiple) {
confirmSet.headSet(deliveryTag + 1).clear();
} else {
confirmSet.remove(deliveryTag);
}
}
public void handleAck(long deliveryTag, boolean multiple)
throws IOException {
//confirmSet.headSet(n)方法返回当前集合中小于n的集合
if (multiple) {
//批量确认:将集合中小于等于当前序号deliveryTag元素的集合清除,表示这批序号的消息都已经被ack了
System.out.println("ack批量确认,deliveryTag:"+deliveryTag+",multiple:"+multiple+",当次确认消息序号集合:"+confirmSet.headSet(deliveryTag + 1));
confirmSet.headSet(deliveryTag + 1).clear();
} else {
//单条确认:将当前的deliveryTag从集合中移除
System.out.println("ack单条确认,deliveryTag:"+deliveryTag+",multiple:"+multiple+",当次确认消息序号:"+deliveryTag);
confirmSet.remove(deliveryTag);
}
//需要重发消息
}
});
for(int i=0;i<30;i++){
String message = "异步confirm消息"+i;
//得到下次发送消息的序号
long nextPublishSeqNo = channel.getNextPublishSeqNo();
channel.basicPublish(EXCHANGE_NAME, "", MessageProperties.PERSISTENT_TEXT_PLAIN, message.getBytes());
//将序号存入集合中
confirmSet.add(nextPublishSeqNo);
}
//关闭通道和连接
// channel.close();
// connection.close();
}
}
运行两次结果如下
ack单条确认,deliveryTag:1,multiple:false,当次确认消息序号:1
ack批量确认,deliveryTag:30,multiple:true,当次确认消息序号集合:[2, 3, 4...(中间省略了)...28, 29, 30]
ack批量确认,deliveryTag:3,multiple:true,当次确认消息序号集合:[1, 2, 3]
ack批量确认,deliveryTag:30,multiple:true,当次确认消息序号集合:[4, 5, 6...(中间省略了)...29, 30]
多运行几次,可能每次都会不一样,你也可以尝试将发送消息总数改为50或者更多,然后多运行几次观察结果。通过运行结果发现:单个或者批量确认,貌似是随机的。。。。。。发送的消息条数越多,批量确认的次数越多,毕竟批量确认效率更高
消费者角色
在rabbitmq情况下 必须要将消息消费成功之后,才会将该消息从mq服务器端中移除
在kafka中的情况下 不管是消费成功还是消费失败,该消息都不会立即从mq服务器端移除
Mq服务器端
在默认的情况下 都会对队列中的消息实现持久化到硬盘