一、简介
在前面博客中,我们研究了自动应答AutoAck,
channel.basicConsume(String queue,
boolean autoAck,
Consumer callback);
当为true时,他表示自动应答;
当为false时,需要消息消费者在处理完成消息后,手动回执一个信息 channel.basicAck(envelope.getDeliveryTag(), false),表示收到消费者成功收到消息了。
上面的autoAck保证了消息队列和消费者之间的通信安全和消息的安全性。
但大家可能会有一个疑问,autoAck确实保证了消息队列和消费者的消息安全;但是消息生产者产生消息推送至消息队列中,如何保证消息队列一定是收到了呢?
好吧,这是个很好的问题,我们接下来一起学习了解 rabbitmq的消息确认机制。
二、消息确认机制以及事务机制
rabbitmq为我们提供了两种方式
1、通过AMQP协议保证事务机制的实现;
2、通过设置channel 为 confirm 模式来实现;
2.1 事务机制
rabbitmq中的事务处理方式有三个:
1、txSelect() 用于将当前 channel 设置成 transaction 模式(开启事务);
2、txCommit 用于提交事务;
3、txRollback 用于回滚事务;
关键代码
channel.txSelect();
channel.txCommit();
channel.txRollback();
实现demo
import java.io.IOException;
import java.util.concurrent.TimeoutException;
import com.rabbitmq.client.BuiltinExchangeType;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import cn.linkpower.util.MqConnectUtil;
public class Send {
private static final String queue_name = "test_work_queue_tx";
public static void main(String[] args) throws IOException, TimeoutException, InterruptedException {
//1、建立连接
Connection mqConnection = MqConnectUtil.getMqConnection();
//2、建立信道(通道)
Channel channel = mqConnection.createChannel();
//3、声明队列
channel.queueDeclare(queue_name, false, false, false, null);
//公平分发---
//为了开启公平分发操作,在消息消费者发送确认收到的指示后,消息队列才会给这个消费者继续发送下一条消息。
//此处的 1 表示 限制发送给每个消费者每次最大的消息数。
channel.basicQos(1);
try {
//开启事务
channel.txSelect();
String string = "hello xiangjiao ";
System.out.println("send msg = "+string);
//发送消息
channel.basicPublish("", queue_name, null, string.getBytes());
//模拟异常操作
//int a = 10/0;
//无异常 正常执行,则提交事务操作
channel.txCommit();
} catch (Exception e) {
System.out.println("出现异常 回滚操作");
channel.txRollback();
}
//5、使用完毕后,需要及时的关闭流应用
channel.close();
mqConnection.close();
}
}
消费者
import java.io.IOException;
import java.util.concurrent.TimeoutException;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.DefaultConsumer;
import com.rabbitmq.client.Envelope;
import com.rabbitmq.client.AMQP.BasicProperties;
import cn.linkpower.util.MqConnectUtil;
/**
* 消息消费者要实现 “公平分发” 的操作,需要关闭自动应答操作;<br>
* 同时,在处理完消息后,需要向消息队列做“消费完成”的应答!<br>
* @author 76519
*
*/
public class GetMsg1 {
private final static String queue_name="test_work_queue_tx";
public static void main(String[] args) throws IOException, TimeoutException {
//1、建立连接
Connection mqConnection = MqConnectUtil.getMqConnection();
//2、获取信道
final Channel channel = mqConnection.createChannel();
//3、声明队列
channel.queueDeclare(queue_name, false, false, false, null);
//公平分发---每次只分发一个消息
channel.basicQos(1);
//4、信访室接受消息
DefaultConsumer consumer = new DefaultConsumer(channel){
@Override
public void handleDelivery(String consumerTag, Envelope envelope, BasicProperties properties, byte[] body)
throws IOException {
String message = new String(body, "UTF-8");
System.out.println(" get msg new1 = " + message );
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
System.out.println("get msg new1 done");
//公平分发--- 消费完成后,需要做相关的回执信息
channel.basicAck(envelope.getDeliveryTag(), false);
}
}
};
//5、创建监听
//channel.basicConsume(queue_name,true, consumer);
//公平分发--- 同时需要关闭自动“应答”
channel.basicConsume(queue_name, false,consumer);
}
}
分别进行正常测试和异常测试,发现当出现异常时,会进行回滚操作,此时的消息队列并不能接收到任何的消息。
但是使用这种方式有一个很大的弊端:
由于这种方式进行处理事务操作,很耗时,会严重降低rabbitmq处理消息的吞吐量。
2.2、Confirm模式
2.2.1、生产者端,confirm模式的实现原理。
1、生产者将信道(channel)设置为confirm模式。
2、所有在该信道上发布的消息,都会指派一个起始为1且唯一的id,一旦消息被推送至匹配的队列之后,broker就会发送一个(携带id的)确认给生产者,使得生产者知道消息成功到达了目标队列中。
3、如果消息和队列是可持久化(durable = true)的,那么 确认消息 会将消息写至磁盘后发出。
4、broker回传给生产者的确认消息中deliver-tag域包含了确认消息的序列号,此外broker也可以设置basic.ack的multiple域,表示这个序列号之前的所有消息都已经得到了处理。
2.2.2、confirm模式的优势:
confirm 模式最大的好处在于他是异步的。
一旦发布一条消息,生产者应用程序就可以在等信道返回确认的同时继续发送下一条消息,当消息最终得到确认之后,生产者应用便可以通过回调方法来处理该确认消息,如果RabbitMQ 因为自身内部错误导致消息丢失,就会发送一条 nack 消息,生产者应用程序同样可以在回调方法中处
理该 nack 消息。
2.2.3、开启confirm模式:
//生产者调用channel的confirmSelect()将信道设置为confirm模式。
channel.confirmSelect();
这里有个坑:
已经在 transaction 事务模式的 channel 是不能再设置成 confirm 模式的。
即这两种模式是不能共存的。
2.2.4、confirm的编程模式
- 普通 confirm 模式:每发送一条消息后,调用 waitForConfirms()方法,等待服务器端 confirm。实际上是一种串行 confirm 了。
- 批量 confirm 模式:每发送一批消息后,调用 waitForConfirms()方法,等待服务器端 confirm。
- 异步 confirm 模式:提供一个回调方法,服务端 confirm 了一条或者多条消息后 Client 端会回调这个方法。
单条confirm(同步)
生产者demo
import java.io.IOException;
import java.util.concurrent.TimeoutException;
import com.rabbitmq.client.AMQP.BasicProperties;
import com.rabbitmq.client.AMQP.BasicProperties.Builder;
import com.rabbitmq.client.BuiltinExchangeType;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import cn.linkpower.util.MqConnectUtil;
/**
* 普通模式
*
* @author 76519
*
*/
public class Send {
// 之前设置的事务机制的对了 此处做测试能否和confirm机制共存
// private static final String queue_name = "test_work_queue_tx";
private static final String queue_name = "test_queue_confirm1";
public static void main(String[] args) throws IOException, TimeoutException, InterruptedException {
// 1、建立连接
Connection mqConnection = MqConnectUtil.getMqConnection();
// 2、建立信道(通道)
Channel channel = mqConnection.createChannel();
// 3、声明队列(开启持久化队列)
channel.queueDeclare(queue_name, true, false, false, null);
// 开启confirm
channel.confirmSelect();
channel.basicQos(1);
String string = "hello xiangjiao confirm";
System.out.println("send msg = " + string);
// 消息持久化
Builder builder = new Builder();
builder.deliveryMode(2);
BasicProperties properties = builder.build();
// 发送消息
channel.basicPublish("", queue_name, properties, string.getBytes());
//confirm 普通方式 判断消息发送成功还是失败
if(!channel.waitForConfirms()){
System.out.println("send msg failed");
}else{
System.out.println("send msg success");
}
channel.close();
mqConnection.close();
}
}
消费者demo
import java.io.IOException;
import java.util.concurrent.TimeoutException;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.DefaultConsumer;
import com.rabbitmq.client.Envelope;
import com.rabbitmq.client.AMQP.BasicProperties;
import cn.linkpower.util.MqConnectUtil;
/**
* 消息消费者要实现 “公平分发” 的操作,需要关闭自动应答操作;<br>
* 同时,在处理完消息后,需要向消息队列做“消费完成”的应答!<br>
* @author 76519
*
*/
public class GetMsg1 {
private final static String queue_name="test_queue_confirm1";
public static void main(String[] args) throws IOException, TimeoutException {
//1、建立连接
Connection mqConnection = MqConnectUtil.getMqConnection();
//2、获取信道
final Channel channel = mqConnection.createChannel();
//3、声明队列(开启持久化队列)
channel.queueDeclare(queue_name, true, false, false, null);
//公平分发---每次只分发一个消息
channel.basicQos(1);
//4、信访室接受消息
DefaultConsumer consumer = new DefaultConsumer(channel){
@Override
public void handleDelivery(String consumerTag, Envelope envelope, BasicProperties properties, byte[] body)
throws IOException {
String message = new String(body, "UTF-8");
System.out.println(" get msg new1 = " + message );
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
System.out.println("get msg new1 done");
//公平分发--- 消费完成后,需要做相关的回执信息
channel.basicAck(envelope.getDeliveryTag(), false);
}
}
};
//公平分发--- (采取手动应答操作)
channel.basicConsume(queue_name, false,consumer);
}
}
批量多条发送,监听confirm操作(同步)
生产者(只是将发送消息改为发送多条,批量发送,全部发送完成后再确认)
for (int i = 0; i < 10; i++) {
channel.basicPublish("",
queue_name,
properties,
string.getBytes());
}
-------
//监听操作依旧不变
if(!channel.waitForConfirms()){
System.out.println("send msg failed");
}else{
System.out.println("send msg success");
}
针对以上同步的操作,有一个很严重的问题:
多条消息发送同时监听,假如多条消息中有成功的和失败的,那么这多条消息都会返回重发!
异步发送
消息生产者实现demo
import java.io.IOException;
import java.util.Collections;
import java.util.SortedSet;
import java.util.TreeSet;
import java.util.concurrent.TimeoutException;
import com.rabbitmq.client.AMQP.BasicProperties;
import com.rabbitmq.client.AMQP.BasicProperties.Builder;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.ConfirmListener;
import com.rabbitmq.client.Connection;
import cn.linkpower.util.MqConnectUtil;
/**
* 普通模式
*
* @author 76519
*
*/
public class Send {
private static final String queue_name = "test_queue_confirm_yibu";
public static void main(String[] args) throws IOException, TimeoutException, InterruptedException {
// 1、建立连接
Connection mqConnection = MqConnectUtil.getMqConnection();
// 2、建立信道(通道)
Channel channel = mqConnection.createChannel();
// 3、声明队列(开启持久化队列)
channel.queueDeclare(queue_name, true, false, false, null);
// 开启confirm
channel.confirmSelect();
channel.basicQos(1);
// 创建一个有序的集合 保存每一次的tag
final SortedSet<Long> confirmSortedSet = Collections.synchronizedSortedSet(new TreeSet<Long>());
// 设置监听事件 异步监听每条消息的成功与失败
channel.addConfirmListener(new ConfirmListener() {
//成功时回调(异步的 此时表示没有问题的回调)
//每回调一次handleAck方法,unconfirm集合删掉相应的一条(multiple=false)或多条(multiple=true)记录。
public void handleAck(long deliveryTag, boolean multiple) throws IOException {
if(multiple){
//当发送多条消息时,他返回可能多条消息的接受情况,也可能返回单条消息的情况
System.out.println("handleAck --- multiple == true");
confirmSortedSet.headSet(deliveryTag+1).clear();
}else{
//单条消息
System.out.println("handleAck --- multiple == false");
confirmSortedSet.remove(deliveryTag);
}
}
//失败时回调
public void handleNack(long deliveryTag, boolean multiple) throws IOException {
if(multiple){
System.out.println("handleNack --- multiple == true");
confirmSortedSet.headSet(deliveryTag+1).clear();
}else{
//单条消息
System.out.println("handleNack --- multiple == false");
confirmSortedSet.remove(deliveryTag);
}
}
});
String msg = "hello xiangjiao confirm yibu";
System.out.println("send msg = " + msg);
// 消息持久化
Builder builder = new Builder();
builder.deliveryMode(2);
BasicProperties properties = builder.build();
// //发送多条消息
// for (int i = 0; i < 100; i++) {
// //获取下一条消息的tag编号
// long nextPublishSeqNo = channel.getNextPublishSeqNo();
// channel.basicPublish("", queue_name, properties, msg.getBytes());
// //向集合中保存对应的编号信息
// confirmSortedSet.add(nextPublishSeqNo);
// }
//
// channel.close();
// mqConnection.close();
while (true) {
//获取下一条消息的tag编号
long nextPublishSeqNo = channel.getNextPublishSeqNo();
channel.basicPublish("", queue_name, properties, msg.getBytes());
//向集合中保存对应的编号信息
confirmSortedSet.add(nextPublishSeqNo);
}
}
}
消费者demo实现
import java.io.IOException;
import java.util.concurrent.TimeoutException;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.DefaultConsumer;
import com.rabbitmq.client.Envelope;
import com.rabbitmq.client.AMQP.BasicProperties;
import cn.linkpower.util.MqConnectUtil;
/**
* 消息消费者要实现 “公平分发” 的操作,需要关闭自动应答操作;<br>
* 同时,在处理完消息后,需要向消息队列做“消费完成”的应答!<br>
* @author 76519
*
*/
public class GetMsg1 {
private final static String queue_name="test_queue_confirm_yibu";
public static void main(String[] args) throws IOException, TimeoutException {
//1、建立连接
Connection mqConnection = MqConnectUtil.getMqConnection();
//2、获取信道
final Channel channel = mqConnection.createChannel();
//3、声明队列
channel.queueDeclare(queue_name, true, false, false, null);
//公平分发---每次只分发一个消息
channel.basicQos(1);
//4、信访室接受消息
DefaultConsumer consumer = new DefaultConsumer(channel){
@Override
public void handleDelivery(String consumerTag, Envelope envelope, BasicProperties properties, byte[] body)
throws IOException {
String message = new String(body, "UTF-8");
System.out.println(" get msg new1 = " + message );
channel.basicAck(envelope.getDeliveryTag(), false);
}
};
//公平分发--- 同时需要关闭自动“应答”
channel.basicConsume(queue_name, false,consumer);
}
}
这个里面有几项 重点:
1、在消息生产者代码中,为什么 confirmSortedSet.headSet(deliveryTag+1) 必须加1?
在源码中,有这一句描述 Returns a view of the portion of this set whose elements are strictly less than toElement.,他的意思是:返回当前 toElement标识前的所有元素对象信息,但不包含当前 toElement的元素信息。
所以,要想返回包含当前toElement标识的所有对象的信息,就必须要进行 +1 操作。
2、在设置监听回调的操作中,handleAck 和 handleNack分别代表了什么?
我在看资料的时候,基本上每种资料都有不同的讲解含义。但在我看到handleAck时,突然想到手动回执消息时,不是采取的channel.basicAck实现的吗?于是在源码中发现了一个很重要的信息结合在信道上也是做监听操作,
channel.addConfirmListener,由此可见,这里的两个回调分别表示
服务端收到或者拒收的信息回调。