为什么要确认
队列中的消息能够成功被消费者消费的前提是消息能被生产者正确的推送到队列中,下面我们来研究RabbitMQ为保证消息被成功推送到队列中的解决方案,事务机制和发布确认机制。
如下图,解决下图部分。
事务机制
在AMQP协议中为保障消息能够正确的被推送到RabbitMQ服务器的队列中,它提供了一种事务机制,以确保没有推送成功的消息可以进行回滚。
1. 开启事务
我们需要在Channel(信道)中开启事务,使Channel(信道)处于transactional模式,发送者使用该模式向队列中推送消息。
2. 提交事务
提交当前的事务。
3. 事务回滚
发送消息出现异常后,进行事务回滚。
生产者代码
package rabbitmq.ced.confirm;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import java.io.IOException;
/**
* 事务机制 创建生产者
*
* @author 崔二旦
* @since now
*/
public class TransactionProducer {
public static void main(String[] args) {
// 1.创建连接工厂
ConnectionFactory connectionFactory = new ConnectionFactory();
// 2. 设置连接属性
connectionFactory.setHost("localhost");
connectionFactory.setPort(5672);
connectionFactory.setVirtualHost("/");
connectionFactory.setUsername("admin");
connectionFactory.setPassword("admin");
Connection connection = null;
Channel channel = null;
try {
// 3. 从连接工厂中获取连接
connection = connectionFactory.newConnection("生产者");
// 3. 在连接中创建信道,RabbitMQ中的所有操作都是在信道中完成的
channel = connection.createChannel();
// 声明队列,如果队列不存在会创建队列
channel.queueDeclare("hello", false, false, true, null);
// 准备要被发送的消息内容
String message = "NB 崔二旦";
// 开启事务机制
channel.txSelect();
channel.basicPublish("", "hello", null, message.getBytes());
// 提交事务
channel.txCommit();
System.out.println("消息发送成功");
} catch (Exception e) {
e.printStackTrace();
try {
if (null != channel) {
// 回滚事务
channel.txRollback();
}
} catch (IOException ioe) {
ioe.printStackTrace();
System.out.println("消息发送异常,回滚异常");
}
System.out.println("消息发送异常");
} finally {
// 释放关闭连接信道与连接
if (channel != null && channel.isOpen()) {
try {
channel.close();
} catch (Exception ex) {
ex.printStackTrace();
}
}
if (connection != null) {
try {
connection.close();
} catch (Exception ex) {
ex.printStackTrace();
}
}
}
}
}
查看上面代码,可以看出只是在之前的生产者代码中加了三行关于开启事务,提交事务,事务回滚的方法。
如下:
// 开启事务机制
channel.txSelect();
// 提交事务
channel.txCommit();
// 回滚事务
channel.txRollback();
事务机制总结
因事务机制在每次发送消息的时候,都要在Channel中进行开启事务操作,所以会在与MQ交互时带来巨大的多余的开销,导致消息的吞吐量下降很多,据说会下降到250%。所以在RabbitMQ中还提供了另一种解决方案,那就是发布确认模式(Confirm)。
发布确认机制
发布确认机制也就是发送方确认模式,生产者在Channel(信道)上调用confirmSelect()方法,请求将Broker的Channel(信道)设置成为confirm模式,一旦信道进入confirm模式,所有在该信道上流通的消息都会被指派一个唯一的ID(从1开始),并将消息被推送到匹配的队列中。生产者可以在推送消息之后,通过信道调用waitForConfirms()方法,向RabbitMQ发送消息发布确认请求,RabbitMQ服务器会返回消息发布确认的状态给生产者(包括确认的消息及消息的ID信息),这样生产者就知道消息已经被正确推送到目的队列了。如果收到发布确认失败的消息,生产者可以进行后续的重新发送。
发布确认机制有多种方式可以实现确认操作,分别是单个确认、批量确认、异步确认。
开启方法
发布确认机制默认是不开启的,需要手动开启,开启方法是在创建Channel(信道)之后,将Channel设置成confirm模式,如下:
// 创建信道
Channel channel = connection.createChannel();
// 开启confirm模式
channel.confirmSelect();
单个确认
单个确认是一种简单的确认方式,它是一种同步确认发布的方式,也就是发布一个消息之后,便向Broker发送确认发布请求,只有当这个消息被确认发布之后,才会继续发布后面的消息,一种串行的确认发布过程,这种方式的发布速度特别的慢,因为前面的消息没有被确认发布,就会阻塞后面所有的消息发布。这种方式的吞吐量每秒小于100条。
- 生产者发送一条消息给Broker。
- 生产者向Broker发送一次发布确认请求 。
- Broker向生产者返回确认发布失败(NACK)的状态,生产者重新发送这一条消息。失败可能是Broker服务端出现问题,导致消息无法被发送。
- Broker向生产者返回确认发布成功(ACK)的状态,生产者继续发送下一条消息。
单个确认生产者代码
模拟了1000条消息,使用单个确认方式将消息推送到队列中。
package rabbitmq.ced.confirm;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
/**
* 发布确认机制 单个发布 创建生产者
*
* @author 崔二旦
* @since now
*/
public class ConfirmSingleProducer {
public static void main(String[] args) {
// 1.创建连接工厂
ConnectionFactory connectionFactory = new ConnectionFactory();
// 2. 设置连接属性
connectionFactory.setHost("localhost");
connectionFactory.setPort(5672);
connectionFactory.setVirtualHost("/");
connectionFactory.setUsername("admin");
connectionFactory.setPassword("admin");
Connection connection = null;
Channel channel = null;
try {
// 3. 从连接工厂中获取连接
connection = connectionFactory.newConnection("生产者");
// 3. 在连接中创建信道,RabbitMQ中的所有操作都是在信道中完成的
channel = connection.createChannel();
// 队列名称
String queueName = "single_queue";
// 声明队列,如果队列不存在会创建队列
channel.queueDeclare(queueName, false, false, false, null);
long begin = System.currentTimeMillis();
// 开启发布确认模式
channel.confirmSelect();
for (int i = 0; i < 1000; i++) {
// 准备要被发送的消息内容
String message = "崔二旦" + i;
//消息ID
long seqNo = channel.getNextPublishSeqNo();
// 推送消息
channel.basicPublish("", queueName, null, message.getBytes());
/**
* 发送方向Broker发送确认发布请求。
* 服务端返回false或超时未返回,
* 生产者可以获取服务器返回的相应消息的序号,
* 对消息进行重新发送,或者其它处理
*/
boolean flag = channel.waitForConfirms();
//服务端返回true,标识消息已经正确的被发送到队列中,
// 我们可以在这里对成功消息进行进一步处理,如保存日志等
if (flag) {
//打印发送成功的消息的序列
System.out.println("消息发送成功,被发送成功的消息序列号为:" + seqNo);
/**
* 服务端返回false,标识消息没有被发送到指定队列中,
* 我们可以进行重新发送或者对失败的消息进行处理,
* 如记录日志,将消息重新发送到某一个失败消息的队列。
*/
} else {
System.out.println("消息发送失败,被发送失败的消息序列号为:" + seqNo);
}
}
long end = System.currentTimeMillis();
System.out.println("发布1000个消息到同一队列,耗时" + (end - begin) + "ms");
} catch (Exception e) {
e.printStackTrace();
System.out.println("消息发送异常");
} finally {
// 释放关闭连接信道与连接
if (channel != null && channel.isOpen()) {
try {
channel.close();
} catch (Exception ex) {
ex.printStackTrace();
}
}
if (connection != null) {
try {
connection.close();
} catch (Exception ex) {
ex.printStackTrace();
}
}
}
}
}
结果
发布1000个消息到同一队列,耗时1863ms。
批量确认
批量确认其实是单个确认的一个升级版,因为单个确认的方式是需要一条消息一条消息的确认,发送消息的速度比较慢,如果推送一批消息之后一起确认,这样就会大大提高吞吐量,但是在推送消息的过程中,出现了故障就会导致一些消息丢失,因为在批量确认的时候是一次性确认一批消息,Broker是没有办法返回具体确认状态里消息的信息的,所以出现失败时是不知道丢失的消息是哪个,所以我们要把整批消息记录下来,一边失败后的重新推送。这种批量确认的方式也是同步发送的,也会出现消息阻塞的情况。
- 生产者发送一批消息给Broker。
- 生产者向Broker发送一次发布确认请求 。
- Broker向生产者返回确认发布失败(NACK)的状态,生产者重新发送这一批消息,失败可能是Broker服务端出现问题,导致消息无法被发送。
- Broker向生产者返回确认发布成功(ACK)的状态,生产者继续发送下一批消息。
批量确认生产者代码
模拟了1000条消息,使用批量确认方式将消息推送到队列中。
package rabbitmq.ced.confirm;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
/**
* 发布确认机制 批量发布 创建生产者
*
* @author 崔二旦
* @since now
*/
public class ConfirmBatchProducer {
public static void main(String[] args) {
// 1.创建连接工厂
ConnectionFactory connectionFactory = new ConnectionFactory();
// 2. 设置连接属性
connectionFactory.setHost("localhost");
connectionFactory.setPort(5672);
connectionFactory.setVirtualHost("/");
connectionFactory.setUsername("admin");
connectionFactory.setPassword("admin");
Connection connection = null;
Channel channel = null;
try {
// 3. 从连接工厂中获取连接
connection = connectionFactory.newConnection("生产者");
// 4. 在连接中创建信道,RabbitMQ中的所有操作都是在信道中完成的
channel = connection.createChannel();
// 队列名称
String queueName = "batch_queue";
// 声明队列,如果队列不存在会创建队列
channel.queueDeclare(queueName, false, false, false, null);
long begin = System.currentTimeMillis();
// 开启发布确认
channel.confirmSelect();
// 批量确认消息个数
int batchSize = 100;
// 未确认消息数量
int noConfirmCount = 0;
for (int i = 0; i < 1000; i++) {
// 准备要被发送的消息内容
String message = "崔二旦" + i;
channel.basicPublish("", queueName, null, message.getBytes());
noConfirmCount++;
// 当发送的消息数与我们设置的批量确认的数值一致,我们就会已经一次确认操作
if (noConfirmCount == batchSize) {
// 将该批消息向rabbitmq发送一个确认发布的请求,
// rabbitmq会返回确认发布的成功状态
boolean flag = channel.waitForConfirms();
if (flag) {
System.out.println("该批消息已经确认发送成功");
} else {
System.out.println("该批消息有确认失败的,需要重新发送整批失败的消息");
}
noConfirmCount = 0;
}
}
// 当最后一批消息发送的数量不足批量确认的数量时,把剩余的消息在进行确认
if (noConfirmCount > 0) {
channel.waitForConfirms();
}
long end = System.currentTimeMillis();
System.out.println("发布1000个批量消息到同一队列,耗时" + (end - begin) + "ms");
} catch (Exception e) {
e.printStackTrace();
System.out.println("消息发送异常");
} finally {
// 释放关闭连接信道与连接
if (channel != null && channel.isOpen()) {
try {
channel.close();
} catch (Exception ex) {
ex.printStackTrace();
}
}
if (connection != null) {
try {
connection.close();
} catch (Exception ex) {
ex.printStackTrace();
}
}
}
}
}
结果
发布1000个批量消息到同一队列,耗时150ms。
异步确认
Confirm模式最大的好处是可以异步确认,在生产者发送消息之前,在生产者程序中设置一个确认发布监听器,用于给Borker端调用,Broker将确认发布信息推送给生产者确认发布监听器,设置好监听器之后生产者就可以向Broker端推送消息了。确认发布监听器与消息推送是两个独立的线程,所以推送消息和监听状态是可以并行的,也就是所谓的异步操作,生产者可以一直给Broker端推送消息,Broker端会将确认状态通过监听器回调给生产者,Broker端会给确认发布监听器传递两种确认发布的状态,确认成功(Confirm Ack)和确认失败(Confirm Nack),生产者可以获取到成功或失败的消息,做其它处理,如将失败的消息重新发送。所以异步确认是性价比最好的。
生产者在将Channel(信道)设置成Confirm模式之后,需要先将确认发布监听器准备好,以便于Broker随时可以将消息的确认状态回调给生产者。
- 生产者连续给Broker发送消息。
- Broker在接收到消息之后,随时将确认发布状态回传给生产者的监听器(调用回调函数) 。
- 确认发布监听器将发送失败(Nack)的消息,进行处理重新发送。
- 确认发布监听器将发送成功(Ack)的消息,进行后续处理,比如进度日志等。
异步确认生产者代码(一)
从控制台输入消息
package rabbitmq.ced.confirm;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.ConfirmCallback;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import java.util.Scanner;
import java.util.concurrent.ConcurrentNavigableMap;
import java.util.concurrent.ConcurrentSkipListMap;
/**
* 发布确认机制 异步发布 创建生产者
* 逐条发送
*
* @author 崔二旦
* @since now
*/
public class ConfirmAsyncProducer {
public static void main(String[] args) {
// 1.创建连接工厂
ConnectionFactory connectionFactory = new ConnectionFactory();
// 2. 设置连接属性
connectionFactory.setHost("localhost");
connectionFactory.setPort(5672);
connectionFactory.setVirtualHost("/");
connectionFactory.setUsername("admin");
connectionFactory.setPassword("admin");
Connection connection = null;
Channel channel = null;
try {
// 3. 从连接工厂中获取连接
connection = connectionFactory.newConnection("生产者");
// 4. 在连接中创建信道,RabbitMQ中的所有操作都是在信道中完成的
channel = connection.createChannel();
// 队列名称
String queueName = "async_queue";
// 声明队列,如果队列不存在会创建队列
channel.queueDeclare(queueName, false, false, false, null);
// 开启发布确认
channel.confirmSelect();
/**
* 定义一个记录已发送消息的Map
*/
ConcurrentSkipListMap<Long, Object> outstandingConfirms = new ConcurrentSkipListMap<>();
/**
* 处理发送成功的消息,消费者回传过来的两个参数
* 1. 消息ID:当前可以确认发布的消息序号
* 2. 是否批量发送:true:自动确认,false:手动确认
*/
ConfirmCallback ackCallback = (sequenceNo, multiple) -> {
//获取当前确认发布成功的消息
Object message = outstandingConfirms.get(sequenceNo);
// 批量发送,true:代表消费者是以自动确认的机制去接受消息的,
// 但是自动确认机制只要消费者接收到消息,MQ就会认为消息已经发送成功,
// 所以MQ是不会管消费者接收到消息之后的操作是否成功的,如果消费者后续失败了,消费者也是不知道的。
// 所以生产者会将该消息之前的所有消息都认为是发送成功了。
if (multiple) {
// 返回的是小于等于当前序列号的未确认消息,是一个Map
// headMap该方法是截取outstandingConfirms集合,将sequenceNo序号之前的都截取到confirmed 中。
// outstandingConfirms中sequenceNo序号之前的都会被删除。
ConcurrentNavigableMap<Long, Object> confirmed =
outstandingConfirms.headMap(sequenceNo, true);
// 清除该部分未确认消息,将截取出来的消息都认为是发送成功了,将截取后的集合清空。
confirmed.clear();
} else {
// false:代表是手动确认的,也就是只有发过来的消息才是被成功处理的。
// 只清除当前序列号的消息
outstandingConfirms.remove(sequenceNo);
}
System.out.println("发布的消息:" + message + " 被成功确认,序列号未" + sequenceNo);
};
/**
* 处理发送失败的消息
*/
Channel finalChannel = channel;
ConfirmCallback nackCallback = (sequenceNo, multiple) -> {
//获取失败消息
Object message = outstandingConfirms.get(sequenceNo);
finalChannel.basicPublish("", queueName, null,
((String) message).getBytes());
System.out.println("发布的消息:" + message + " 未被确认,序列号未" + sequenceNo);
};
/**
* 确认发布监听器
* 1. 成功消息的处理函数
* 2. 失败消息的处理函数
*/
channel.addConfirmListener(ackCallback, nackCallback);
// 通过控制台输入信息
Scanner scanner = new Scanner(System.in);
while (scanner.hasNext()) {
String message = scanner.next();
// 记录即将被发送的消息信息
outstandingConfirms.put(channel.getNextPublishSeqNo(), message);
channel.basicPublish("", queueName, null, message.getBytes());
}
} catch (Exception e) {
e.printStackTrace();
System.out.println("消息发送异常");
} finally {
// 释放关闭连接信道与连接
if (channel != null && channel.isOpen()) {
try {
channel.close();
} catch (Exception ex) {
ex.printStackTrace();
}
}
if (connection != null) {
try {
connection.close();
} catch (Exception ex) {
ex.printStackTrace();
}
}
}
}
}
异步确认生产者代码(二)
模拟了1000条消息,使用异步确认方式将消息推送到队列中。
package rabbitmq.ced.confirm;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.ConfirmCallback;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import java.util.concurrent.ConcurrentNavigableMap;
import java.util.concurrent.ConcurrentSkipListMap;
/**
* 发布确认机制 异步发布 创建生产者
* 发送多条
*
* @author 崔二旦
* @since now
*/
public class ConfirmAsyncMoreProducer {
public static void main(String[] args) {
// 1.创建连接工厂
ConnectionFactory connectionFactory = new ConnectionFactory();
// 2. 设置连接属性
connectionFactory.setHost("localhost");
connectionFactory.setPort(5672);
connectionFactory.setVirtualHost("/");
connectionFactory.setUsername("admin");
connectionFactory.setPassword("admin");
Connection connection = null;
Channel channel = null;
try {
// 3. 从连接工厂中获取连接
connection = connectionFactory.newConnection("生产者");
// 4. 在连接中创建信道,RabbitMQ中的所有操作都是在信道中完成的
channel = connection.createChannel();
// 队列名称
String queueName = "async_queue";
// 声明队列,如果队列不存在会创建队列
channel.queueDeclare(queueName, false, false, false, null);
// 开启发布确认
channel.confirmSelect();
ConcurrentSkipListMap<Long, String> outstandingConfirms = new ConcurrentSkipListMap<>();
ConfirmCallback ackCallback = (sequenceNo, multiple) -> {
String message = outstandingConfirms.get(sequenceNo);
outstandingConfirms.put(sequenceNo, message + "-true");
// 这里打印的成功消息不全,因为程序执行完就自动关闭了,
// 导致Broker无法将确认状态回传,但是查看队列中已经存在1000条消息了。
System.out.println("发布的消息:" + message + " 被成功确认,序列号未" + sequenceNo);
if (multiple) {
// 返回的是小于等于当前序列号的未确认消息,是一个Map
ConcurrentNavigableMap<Long, String> confirmed =
outstandingConfirms.headMap(sequenceNo, true);
// 清除该部分未确认消息
confirmed.clear();
} else {
// 只清除当前序列号的消息
outstandingConfirms.remove(sequenceNo);
}
};
ConfirmCallback nackCallback = (sequenceNo, multiple) -> {
String message = outstandingConfirms.get(sequenceNo);
System.out.println("发布的消息" + message + "未被确认,序列号未" + sequenceNo);
};
channel.addConfirmListener(ackCallback, nackCallback);
long begin = System.currentTimeMillis();
for (int i = 0; i < 1000; i++) {
// 准备要被发送的消息内容
String message = "崔二旦" + i;
outstandingConfirms.put(channel.getNextPublishSeqNo(), message);
channel.basicPublish("", queueName, null, message.getBytes());
}
long end = System.currentTimeMillis();
System.out.println("发布1000个异步消息到同一队列,耗时" + (end - begin) + "ms");
} catch (Exception e) {
e.printStackTrace();
System.out.println("消息发送异常");
} finally {
// 释放关闭连接信道与连接
if (channel != null && channel.isOpen()) {
try {
channel.close();
} catch (Exception ex) {
ex.printStackTrace();
}
}
if (connection != null) {
try {
connection.close();
} catch (Exception ex) {
ex.printStackTrace();
}
}
}
}
}
发布1000个异步消息到同一队列,耗时99ms
速度对比
单个确认:
发布1000个消息到同一队列,耗时1863ms。
批量确认:
发布1000个批量消息到同一队列,耗时150ms。
异步确认:
发布1000个异步消息到同一队列,耗时99ms