RabbitMQ消息发布时的权衡
不做任何配置的情况下,生产者是不知道消息是否真正到达RabbitMQ,也就是说消息发布操作不返回任何消息给生产者。怎么保证我们消息发布的可靠性?有几种常用机制。
1.失败通知
发送消息时设置mandatory标志
在发送消息时设置mandatory标志,告诉RabbitMQ,如果消息不可路由,应该将消息返回给发送者,并通知失败。可以这样认为,开启mandatory是开启故障检测模式。
注意:它只会让RabbitMQ向你通知失败,而不会通知成功。如果消息正确路由到队列,则发布者不会受到任何通知。带来的问题是无法确保发布消息一定是成功的,因为通知失败的消息可能会丢失。
mandatory:true,channel.addReturnListener。
package mandatory;
import com.rabbitmq.client.*;
import java.io.IOException;
/**
* @Author: Rab
* @Date: 2020-04-15 17:57
* @Description:
*/
public class ProducerMandatory {
public final static String EXCHANGE_NAME = "mandatory_test";
public static void main(String[] args) throws Exception {
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("127.0.0.1");
Connection connection = factory.newConnection();
Channel channel = connection.createChannel();
channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.DIRECT);
connection.addShutdownListener(new ShutdownListener() {
@Override
public void shutdownCompleted(ShutdownSignalException cause) {
System.out.println("connection shutdown:" + cause.getMessage());
}
});
channel.addShutdownListener(new ShutdownListener() {
@Override
public void shutdownCompleted(ShutdownSignalException cause) {
System.out.println("channel shutdown:" + cause.getMessage());
}
});
// 失败通知返回
channel.addReturnListener(new ReturnListener() {
@Override
public void handleReturn(int replyCode, String replyText, String exchange, String routingKey, AMQP.BasicProperties properties, byte[] body) throws IOException {
String message = new String(body);
System.out.println("返回的replyText :" + replyText);
System.out.println("返回的exchange :" + exchange);
System.out.println("返回的routingKey :" + routingKey);
System.out.println("返回的message :" + message);
}
});
channel.addConfirmListener(new ConfirmListener() {
@Override
public void handleAck(long l, boolean b) throws IOException {
}
@Override
public void handleNack(long l, boolean b) throws IOException {
}
});
String[] severities = {"error", "info", "warning"};
for (int i = 0; i < 3; i++) {
String severity = severities[i % 3];
// 发送的消息
String message = "Hello World_" + (i + 1)
+ ("_" + System.currentTimeMillis());
channel.basicPublish(EXCHANGE_NAME, severity, true, null, message.getBytes());
System.out.println("----------------------------------");
System.out.println(" Sent Message: [" + severity +"]:'"
+ message + "'");
Thread.sleep(200);
}
// 关闭频道和连接
channel.close();
connection.close();
}
}
监听器的小甜点
在信道关闭和连接关闭时,还有两个监听器可以使用
2.发布者确认
基于事务的性能问题,RabbitMQ团队为我们拿出了更好的方案,即采用发送方确认模式,该模式比事务更轻量,性能影响几乎可以忽略不计。
原理:生产者将信道设置成confirm模式,一旦信道进入confirm模式,所有在该信道上面发布的消息都将会被指派一个唯一的ID(从1开始),由这个id在生产者和RabbitMQ之间进行消息的确认。
- 不可路由的消息,当交换器发现,消息不能路由到任何队列,会进行确认操作,表示收到了消息。如果发送方设置了mandatory模式,则会先调用addReturnListener监听器。
- 可路由的消息,要等到消息被投递到所有匹配的队列之后,broker会发送一个确认给生产者(包含消息的唯一ID),这就使得生产者知道消息已经正确到达目的队列了,如果消息和队列是可持久化的,那么确认消息会在将消息写入磁盘之后发出,broker回传给生产者的确认消息中delivery-tag域包含了确认消息的序列号。
confirm模式最大的好处在于他可以是异步的,一旦发布一条消息,生产者应用程序就可以在等信道返回确认的同时继续发送下一条消息,当消息最终得到确认之后,生产者应用便可以通过回调方法来处理该确认消息,如果RabbitMQ因为自身内部错误导致消息丢失,就会发送一条nack消息,生产者应用程序同样可以在回调方法中处理该nack消息决定下一步的处理。
启用发送者确认模式: channel.confirmSelect();
Confirm的三种实现方式:
channel.waitForConfirms()
普通发送方确认模式;消息到达交换器,就会返回true。
package producerconfirm;
import com.rabbitmq.client.*;
import java.io.IOException;
/**
* @Author: Rab
* @Date: 2020-04-15 19:53
* @Description:
*/
public class ProducerConfirm {
public final static String EXCHANGE_NAME = "producer_confirm";
private final static String ROUTE_KEY = "error";
public static void main(String[] args) throws Exception {
/**
* 创建连接连接到RabbitMQ
*/
ConnectionFactory factory = new ConnectionFactory();
// 设置MabbitMQ所在主机ip或者主机名
factory.setHost("127.0.0.1");
// 创建一个连接
Connection connection = factory.newConnection();
// 创建一个信道
Channel channel = connection.createChannel();
channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.DIRECT);
channel.addReturnListener(new ReturnListener() {
public void handleReturn(int replyCode, String replyText,
String exchange, String routingKey,
AMQP.BasicProperties properties,
byte[] body)
throws IOException {
String message = new String(body);
System.out.println("RabbitMq返回的replyCode: " + replyCode);
System.out.println("RabbitMq返回的replyText: " + replyText);
System.out.println("RabbitMq返回的exchange: " + exchange);
System.out.println("RabbitMq返回的routingKey: " + routingKey);
System.out.println("RabbitMq返回的message: " + message);
}
});
// 启用发送者确认模式
channel.confirmSelect();
//所有日志严重性级别
for (int i = 0; i < 2; i++) {
// 发送的消息
String message = "Hello World_" + (i + 1);
channel.basicPublish(EXCHANGE_NAME, ROUTE_KEY, true, null, message.getBytes());
if (channel.waitForConfirms()) {
System.out.println("send success");
}else {
System.out.println("send failure");
}
}
// 关闭频道和连接
channel.close();
connection.close();
}
}
channel.waitForConfirmsOrDie()
批量确认模式;使用同步方式等所有的消息发送之后才会执行后面代码,只要有一个消息未到达交换器就会抛出IOException异常
package producerconfirm;
import com.rabbitmq.client.*;
import java.io.IOException;
/**
* @Author: Rab
* @Date: 2020-04-15 20:01
* @Description:
*/
public class ProducerBatchConfirm {
public final static String EXCHANGE_NAME = "producer_wait_confirm";
private final static String ROUTE_KEY = "error";
public static void main(String[] args) throws Exception {
/**
* 创建连接连接到RabbitMQ
*/
ConnectionFactory factory = new ConnectionFactory();
// 设置RabbitMQ所在主机ip或者主机名
factory.setHost("127.0.0.1");
// 创建一个连接
Connection connection = factory.newConnection();
// 创建一个信道
Channel channel = connection.createChannel();
channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.DIRECT);
channel.addReturnListener(new ReturnListener() {
public void handleReturn(int replyCode, String replyText,
String exchange, String routingKey,
AMQP.BasicProperties properties,
byte[] body)
throws IOException {
String message = new String(body);
System.out.println("RabbitMq返回的replyCode: " + replyCode);
System.out.println("RabbitMq返回的replyText: " + replyText);
System.out.println("RabbitMq返回的exchange: " + exchange);
System.out.println("RabbitMq返回的routingKey: " + routingKey);
System.out.println("RabbitMq返回的message: " + message);
}
});
// 启用发送者确认模式
channel.confirmSelect();
//所有日志严重性级别
for (int i = 0; i < 2; i++) {
// 发送的消息
String message = "Hello World_" + (i + 1);
channel.basicPublish(EXCHANGE_NAME, ROUTE_KEY, true, null, message.getBytes());
System.out.println(" Sent Message: [" + ROUTE_KEY +"]:'"+ message + "'");
}
channel.waitForConfirmsOrDie();
// 关闭频道和连接
channel.close();
connection.close();
}
}
channel.addConfirmListener()
异步监听发送方确认模式;
package producerconfirm;
import com.rabbitmq.client.*;
import java.io.IOException;
/**
* @Author: Rab
* @Date: 2020-04-15 20:10
* @Description:
*/
public class ProducerConfirmAsync {
public final static String EXCHANGE_NAME = "producer_async_confirm";
public static void main(String[] args) throws Exception {
/**
* 创建连接连接到MabbitMQ
*/
ConnectionFactory factory = new ConnectionFactory();
// 设置MabbitMQ所在主机ip或者主机名
factory.setHost("127.0.0.1");
// 创建一个连接
Connection connection = factory.newConnection();
// 创建一个信道
Channel channel = connection.createChannel();
// 指定转发
channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.DIRECT);
// 启用发送者确认模式
channel.confirmSelect();
channel.addConfirmListener(new ConfirmListener() {
@Override
public void handleAck(long deliveryTag, boolean multiple) throws IOException {
System.out.println("deliveryTag:" + deliveryTag
+ ",multiple:" + multiple);
}
@Override
public void handleNack(long deliveryTag, boolean multiple) throws IOException {
System.out.println("Nack");
}
});
channel.addReturnListener(new ReturnListener() {
public void handleReturn(int replyCode, String replyText,
String exchange, String routingKey,
AMQP.BasicProperties properties,
byte[] body)
throws IOException {
String message = new String(body);
System.out.println("RabbitMq路由失败: " + routingKey + "." + message);
}
});
String[] severities = {"error", "warning"};
for (int i = 0; i < 100; i++) {
String severity = severities[i % 2];
// 发送的消息
String message = "Hello World_" + (i + 1) + ("_" + System.currentTimeMillis());
channel.basicPublish(EXCHANGE_NAME, severity, true, null, message.getBytes());
System.out.println("----------------------------------------------------");
System.out.println(" Sent Message: [" + severity +"]:'"+ message + "'");
}
// 关闭频道和连接
//channel.close();
//connection.close();
}
}
3.备用交换器
在第一次声明交换器时被指定,用来提供一种预先存在的交换器,如果主交换器无法路由消息,那么消息将被路由到这个新的备用交换器。
如果发布消息时同时设置了mandatory会发生什么?如果主交换器无法路由消息,RabbitMQ并不会通知发布者,因为,向备用交换器发送消息,表示消息已经被路由了。注意,新的备用交换器就是普通的交换器,没有任何特殊的地方。
使用备用交换器,向往常一样,声明Queue和备用交换器,把Queue绑定到备用交换器上。然后在声明主交换器时,通过交换器的参数,alternate-exchange,,将备用交换器设置给主交换器。
建议备用交换器设置为faout类型,Queue绑定时的路由键设置为“#”
- 生产者
package backupexchange;
import com.rabbitmq.client.BuiltinExchangeType;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import java.util.HashMap;
/**
* @Author: Rab
* @Date: 2020-04-15 20:34
* @Description:
*/
public class BackupExProducer {
public final static String EXCHANGE_NAME = "main-exchange";
public final static String BAK_EXCHANGE_NAME = "ae";
public static void main(String[] args) throws Exception {
/**
* 创建连接连接到RabbitMQ
*/
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("127.0.0.1");
// 创建一个连接
Connection connection = factory.newConnection();
// 创建一个信道
Channel channel = connection.createChannel();
// 声明备用交换器
HashMap<String, Object> argsMap = new HashMap<>();
argsMap.put("alternate-exchange", BAK_EXCHANGE_NAME);
//主交换器
channel.exchangeDeclare(EXCHANGE_NAME, "direct", false, false, argsMap);
//备用交换器
channel.exchangeDeclare(BAK_EXCHANGE_NAME, BuiltinExchangeType.FANOUT, true, false, null);
//所有日志严重性级别
String[] severities = {"error", "info", "warning"};
for (int i = 0; i < 3; i++) {
//每一次发送一条不同严重性的日志
String severity = severities[i % 3];
// 发送的消息
String message = "Hello World_" + (i + 1);
channel.basicPublish(EXCHANGE_NAME, severity, null, message.getBytes());
System.out.println(" [x] Sent '" + severity +"':'"
+ message + "'");
}
// 关闭频道和连接
channel.close();
connection.close();
}
}
- 主交换器消费者
package backupexchange;
import com.rabbitmq.client.*;
import java.io.IOException;
import java.util.HashMap;
import java.util.concurrent.TimeoutException;
/**
*/
public class MainConsumer {
public static void main(String[] argv)
throws IOException, TimeoutException {
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("127.0.0.1");
// 打开连接和创建频道,与发送端一样
Connection connection = factory.newConnection();
final Channel channel = connection.createChannel();
// 声明备用交换器
HashMap<String, Object> argsMap = new HashMap<>();
argsMap.put("alternate-exchange", BackupExProducer.BAK_EXCHANGE_NAME);
//主交换器
channel.exchangeDeclare(BackupExProducer.EXCHANGE_NAME, "direct", false, false, argsMap);
// 声明一个队列
String queueName = "focuserror";
channel.queueDeclare(queueName,
false, false,
false, null);
String severity = "error";//只关注error级别的日志,然后记录到文件中去。
channel.queueBind(queueName,
BackupExProducer.EXCHANGE_NAME, severity);
System.out.println(" [*] Waiting for messages......");
// 创建队列消费者
final Consumer consumerB = new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag,
Envelope envelope,
AMQP.BasicProperties properties,
byte[] body)
throws IOException {
String message = new String(body, "UTF-8");
//记录日志到文件:
System.out.println("Received ["
+ envelope.getRoutingKey() + "] " + message);
}
};
channel.basicConsume(queueName, true, consumerB);
}
}
- 备用交换器消费者
package backupexchange;
import com.rabbitmq.client.*;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
/**
*/
public class BackupExConsumer {
public static void main(String[] argv)
throws IOException, TimeoutException {
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("127.0.0.1");
// 打开连接和创建频道,与发送端一样
Connection connection = factory.newConnection();
final Channel channel = connection.createChannel();
channel.exchangeDeclare(BackupExProducer.BAK_EXCHANGE_NAME,
BuiltinExchangeType.FANOUT,
true, false, null);
// 声明一个队列
String queueName = "fetchother";
channel.queueDeclare(queueName,
false,false,
false,null);
// 绑定到备用交换器
channel.queueBind(queueName,
BackupExProducer.BAK_EXCHANGE_NAME, "#");
System.out.println(" [*] Waiting for messages......");
// 创建队列消费者
final Consumer consumerB = new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag,
Envelope envelope,
AMQP.BasicProperties properties,
byte[] body)
throws IOException {
String message = new String(body, "UTF-8");
//记录日志到文件:
System.out.println( "Received ["
+ envelope.getRoutingKey() + "] "+message);
}
};
channel.basicConsume(queueName, true, consumerB);
}
}
4.事务
事务的实现主要是对信道(Channel)的设置,主要的方法有三个:
- 1.channel.txSelect()声明启动事务模式;
- 2.channel.txComment()提交事务;
- 3.channel.txRollback()回滚事务;
在发送消息之前,需要声明channel为事务模式,提交或者回滚事务即可。
开启事务后,客户端和RabbitMQ之间的通讯交互流程:
- 客户端发送给服务器Tx.Select(开启事务模式)
- 服务器端返回Tx.Select-Ok(开启事务模式ok)
- 推送消息
- 客户端发送给事务提交Tx.Commit
- 服务器端返回Tx.Commit-Ok
以上就完成了事务的交互流程,如果其中任意一个环节出现问题,就会抛出IoException移除,这样用户就可以拦截异常进行事务回滚,或决定要不要重复消息。
那么,既然已经有事务了,为何还要使用发送方确认模式呢,原因是因为事务的性能是非常差的。根据相关资料,事务会降低2~10倍的性能。
- 带有事务的生产者
package transaction;
import com.rabbitmq.client.BuiltinExchangeType;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
/**
* @Author: Rab
* @Date: 2020-04-15 18:11
* @Description:
*/
public class ProducerTransaction {
public final static String EXCHANGE_NAME = "producer_transaction";
public static void main(String[] args) throws Exception {
/**
* 创建连接连接到RabbitMQ
*/
ConnectionFactory factory = new ConnectionFactory();
// 设置MabbitMQ所在主机ip或者主机名
factory.setHost("127.0.0.1");
// 创建一个连接
Connection connection = factory.newConnection();
// 创建一个信道
Channel channel = connection.createChannel();
// 指定转发
channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.DIRECT);
String[] severities = {"error", "info", "warning"};
// 开启事务模式
channel.txSelect();
try {
for (int i = 0; i < 3; i++) {
String severity = severities[i % 3];
// 发送的消息
String message = "Hello World_" + (i + 1)
+ ("_" + System.currentTimeMillis());
channel.basicPublish(EXCHANGE_NAME, severity, true, null, message.getBytes());
System.out.println("----------------------------------");
System.out.println(" Sent Message: [" + severity +"]:'"
+ message + "'");
Thread.sleep(200);
}
channel.txCommit();
} catch (Exception e) {
e.printStackTrace();
// 回滚
channel.txRollback();
}
}
}
- 普通的消费者
package transaction;
import com.rabbitmq.client.*;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
/**
*/
public class ConsumerProducerTransaction {
public static void main(String[] argv)
throws IOException, TimeoutException {
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("127.0.0.1");
// 打开连接和创建频道,与发送端一样
Connection connection = factory.newConnection();
final Channel channel = connection.createChannel();
channel.exchangeDeclare(ProducerTransaction.EXCHANGE_NAME,
BuiltinExchangeType.DIRECT);
String queueName = "producer_confirm";
channel.queueDeclare(queueName,false,false,
false,null);
//只关注error级别的日志
String severity="error";
channel.queueBind(queueName, ProducerTransaction.EXCHANGE_NAME,
severity);
System.out.println(" [*] Waiting for messages......");
// 创建队列消费者
final Consumer consumerB = new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag,
Envelope envelope,
AMQP.BasicProperties properties,
byte[] body) throws IOException {
String message = new String(body, "UTF-8");
//记录日志到文件:
System.out.println( "Received ["+ envelope.getRoutingKey()
+ "] "+message);
}
};
channel.basicConsume(queueName, true, consumerB);
}
}