(思考)使用使用生产者发送一条消息会发生什么问题?
package com.dfyang.rabbitmq;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
public class ProducerDemo1 {
private static final String EXCHANGE_NAME = "test.exchange";
private static final String QUEUE_NAME = "test.queue";
private static final String ROUTING_KEY = "test";
public static void main(String[] args) throws Exception {
Connection connection = RabbitConnectionFactory.getConnection();
Channel channel = connection.createChannel();
channel.exchangeDeclare(EXCHANGE_NAME, "direct");
channel.queueDeclare(QUEUE_NAME, true, false, false, null);
channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, ROUTING_KEY);
channel.basicPublish(EXCHANGE_NAME, ROUTING_KEY, null, "test!".getBytes());
channel.close();
connection.close();
}
}
执行代码,似乎并没有问题。但如果我们的生产者发送消息后,消息并没有到达服务器。很明显,我们的生产者并不会知道自己发送的消息是否到达服务器,如果发送10000条消息,如果有几条消息无法到达服务器,我们肯定希望获取这些消息,再对这些消息进行处理。
那么如何确定消息有没有到达RabbitMQ服务器?
RabbitMQ提供了两种解决方式:
- 事务机制
- 发送方确认机制
首先创建用于获取Connection对象
package com.dfyang.rabbitmq;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import java.io.IOException;
public class RabbitConnectionFactory {
private static final String IP_ADDRESS = "192.168.195.123";
private static final int PORT = 5672;
private static final String USERNAME = "root";
private static final String PASSWORD = "151310";
private static ConnectionFactory factory = new ConnectionFactory();
static {
factory.setHost(IP_ADDRESS);
factory.setPort(PORT);
factory.setUsername(USERNAME);
factory.setPassword(PASSWORD);
}
public static Connection getConnection() {
Connection connection = null;
try {
connection = factory.newConnection();
} catch (Exception e) {
e.printStackTrace();
}
return connection;
}
}
(一)事务机制
开启事务后,会执行下面步骤
- 生产者发送Tx.Select
- 服务端回复Tx.Select-Ok
- 生产者发送消息
- 生产者发送Tx.Commit
- 服务端回复Tx.Commit-Ok
- 如果提交前发生异常,生产者发送Tx.Rollback
- 服务端回复Tx.Rollback-Ok
如果消息开启了持久化,在持久化到硬盘之后才会响应客户端Tx.Commit-Ok
——使用事务会比正常发送多出4个步骤,因此会消耗一定的性能。
下面使用代码演示
channel.txSelect() :开启事务
channel.txCommit():提交事务
channel.txRollback():回滚事务
——如果在发送消息的过程中发生了异常而导致消息没有正确到达服务器,那么我们就可以进行回滚。
package com.dfyang.rabbitmq.tx;
import com.dfyang.rabbitmq.RabbitConnectionFactory;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.MessageProperties;
public class TXProducer {
private static final String EXCHANGE_NAME = "tx.exchange";
private static final String QUEUE_NAME = "tx.queue";
private static final String ROUTING_KEY = "tx";
public static void main(String[] args) throws Exception {
Connection connection = RabbitConnectionFactory.getConnection();
Channel channel = connection.createChannel();
channel.exchangeDeclare(EXCHANGE_NAME, "direct");
channel.queueDeclare(QUEUE_NAME, true, false, false, null);
channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, ROUTING_KEY);
String message = "test!";
try {
channel.txSelect();
channel.basicPublish(EXCHANGE_NAME, ROUTING_KEY, MessageProperties.PERSISTENT_TEXT_PLAIN, message.getBytes());
channel.txCommit();
} catch (Exception e) {
channel.txRollback();
}
channel.close();
connection.close();
}
}
下面是通过wirkshark抓包 输入 (ip.addr==192.168.195.123 && amqp) 进行过滤
对linux进行抓包,我是参照的这篇文章
我们也可以使用事务提交多条消息,但由于txCommit()是同步的,这将非常消耗性能。我们每发送一条消息,需要Tx.Commit并等待服务端发送Tx.Commit-Ok以清楚消息是否发送成功。
package com.dfyang.rabbitmq.tx;
import com.dfyang.rabbitmq.RabbitConnectionFactory;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.MessageProperties;
import java.util.Queue;
public class TXProducer {
private static final String EXCHANGE_NAME = "tx.exchange";
private static final String QUEUE_NAME = "tx.queue";
private static final String ROUTING_KEY = "tx";
public static void main(String[] args) throws Exception {
Connection connection = RabbitConnectionFactory.getConnection();
Channel channel = connection.createChannel();
channel.exchangeDeclare(EXCHANGE_NAME, "direct");
channel.queueDeclare(QUEUE_NAME, true, false, false, null);
channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, ROUTING_KEY);
String message = "test!";
try {
channel.txSelect();
for (int i = 0; i < 10000; i++) {
channel.basicPublish(EXCHANGE_NAME, ROUTING_KEY, MessageProperties.PERSISTENT_TEXT_PLAIN, message.getBytes());
channel.txCommit();
}
} catch (Exception e) {
channel.txRollback();
}
channel.close();
connection.close();
}
}
对比未开启事务和开启事务后发送10000条消息
正常发送:1.935214s
开启事务:28.471526s
可见差距是相当大的。
(二)发送方确认机制
开启发送方确认机制后,会执行下面步骤
- 生产者发送Confirm.Select
- 服务端回复Confirm.Select-Ok
- 生产者发送消息
- 服务端回复Basic.Ack或者Basic.Nack
对比事务
- 生产者发送Tx.Select
- 服务端回复Tx.Select-Ok
- 生产者发送消息
- 生产者发送Tx.Commit
- 服务端回复Tx.Commit-Ok
——可以发现发送方确认机制比事务少执行了一条指令。
下面使用代码演示
三种Confirm模式
- 普通发送方确认模式
- 批量确认模式
- 异步监听发送方确认模式
普通发送方确认模式
每发送一条消息,调用waitForConfirms()。如果出现false,只需将该条消息重传。该模式与事务一样,每次调用waitForConfirms()方法,需要同步等待确认,再发送下一条消息,依旧非常消耗性能。
waitForConfirms——等待直到代理对自上次调用以来发布的所有消息进行了ack或nack处理
waitForConfirmsOrDie——等待直到代理对自上次调用以来发布的所有消息进行了ack或nack处理。如果任何消息被nack, waitForConfirmsOrDie将抛出IOException。
两个方法会阻塞线程
package com.dfyang.rabbitmq.tx;
import com.dfyang.rabbitmq.RabbitConnectionFactory;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.MessageProperties;
import java.util.Queue;
public class TXProducer {
private static final String EXCHANGE_NAME = "tx.exchange";
private static final String QUEUE_NAME = "tx.queue";
private static final String ROUTING_KEY = "tx";
public static void main(String[] args) throws Exception {
Connection connection = RabbitConnectionFactory.getConnection();
Channel channel = connection.createChannel();
channel.exchangeDeclare(EXCHANGE_NAME, "direct");
channel.queueDeclare(QUEUE_NAME, true, false, false, null);
channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, ROUTING_KEY);
String message = "test!";
channel.confirmSelect();
for (int i = 0; i < 10000; i++) {
channel.basicPublish(EXCHANGE_NAME, ROUTING_KEY, MessageProperties.PERSISTENT_TEXT_PLAIN, message.getBytes());
channel.waitForConfirms();
}
channel.close();
connection.close();
}
}
对比三者发送10000条消息
正常发送:1.935214s
开启事务:28.471526s
普通发送方确认模式:25.13076s
发现通过普通发送方确认模式与事务模式相差并不大
批量确认模式
每发送一批消息之后,调用waitForConfirmsOrDie()方法。相比普通发送方确认模式,极大地提升了效率。但当waitForConfirms()出现超时或false,需要将整批消息重传,如果这种情况频繁发生,效率可能反而降低。
package com.dfyang.rabbitmq.tx;
import com.dfyang.rabbitmq.RabbitConnectionFactory;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.MessageProperties;
import java.util.Queue;
public class TXProducer {
private static final String EXCHANGE_NAME = "tx.exchange";
private static final String QUEUE_NAME = "tx.queue";
private static final String ROUTING_KEY = "tx";
public static void main(String[] args) throws Exception {
Connection connection = RabbitConnectionFactory.getConnection();
Channel channel = connection.createChannel();
channel.exchangeDeclare(EXCHANGE_NAME, "direct");
channel.queueDeclare(QUEUE_NAME, true, false, false, null);
channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, ROUTING_KEY);
String message = "test!";
channel.confirmSelect();
for (int i = 0; i < 10000; i++) {
channel.basicPublish(EXCHANGE_NAME, ROUTING_KEY, MessageProperties.PERSISTENT_TEXT_PLAIN, message.getBytes());
}
channel.waitForConfirmsOrDie();
channel.close();
connection.close();
}
}
注意这里使用的是waitForConfirmsOrDie,也就是如果收到一个Nack,那么将抛出IOException
我们发现仅仅只收到了少量的Basic.Ack响应,这是为什么?
这是因为我们的Basic.Ack响应中有一个Multiple参数,表示在这之前的消息均已发送成功
对比四者发送10000条消息
正常发送:1.935214s
开启事务:28.471526s
普通发送方确认模式:25.13076s
批量确认模式:2.351513s
可以看到批量确认模式已经非常接近正常发送速度了,一旦服务端响应了Nack,这意味整批消息将被重新发送,因此这点我们必须考虑。
异步监听发送方确认模式
使用异步的方式并不会阻塞线程,当服务端确认消息后会回调到这个函数。
在回调函数中有两个参数
deliveryTag:消息序号
multiple:这个序列号之前的所有消息都已经得到了处理
package com.dfyang.rabbitmq.tx;
import com.dfyang.rabbitmq.RabbitConnectionFactory;
import com.rabbitmq.client.*;
import java.io.IOException;
import java.util.concurrent.CountDownLatch;
public class TXProducer {
private static CountDownLatch countDownLatch = new CountDownLatch(1);
private static final String EXCHANGE_NAME = "tx.exchange";
private static final String QUEUE_NAME = "tx.queue";
private static final String ROUTING_KEY = "tx";
public static void main(String[] args) throws Exception {
Connection connection = RabbitConnectionFactory.getConnection();
Channel channel = connection.createChannel();
channel.exchangeDeclare(EXCHANGE_NAME, "direct");
channel.queueDeclare(QUEUE_NAME, true, false, false, null);
channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, ROUTING_KEY);
String message = "test!";
channel.confirmSelect();
channel.addConfirmListener(new ConfirmListener() {
@Override
public void handleAck(long deliveryTag, boolean multiple) throws IOException {
if (deliveryTag == 10000)
countDownLatch.countDown();
}
@Override
public void handleNack(long deliveryTag, boolean multiple) throws IOException {
System.err.println("消息发送失败:[deliveryTag = " + deliveryTag + "] [multiple = " + multiple + "]");
}
});
for (int i = 0; i < 10000; i++) {
channel.basicPublish(EXCHANGE_NAME, ROUTING_KEY, MessageProperties.PERSISTENT_TEXT_PLAIN, message.getBytes());
}
countDownLatch.await();
channel.close();
connection.close();
}
}
这里为了方便测试使用了CountDownLatch
使用异步方式同样会有部分ack响应
对比发送10000条消息
正常发送:1.935214s
开启事务:28.471526s
普通发送方确认模式:25.13076s
批量确认模式:2.351513s
异步确认模式:2.432338s
由此可见,使用批量确认模式和使用异步确认模式性能是比较接近的,虽然这里只是粗略的测试。至于使用何种模式,需要根据具体情况而定,相比较而言异步确认模式用的比较多。