rabbitmq是我们非常熟悉的分布式消息中间件了,他不仅提供了消息异步通信、业务服务模块解耦、接口限流、消息延迟处理等特性,而且还具有在消息发送、传输、接收过程中,可以保证消息发送成功、不会丢失以及被确认消费机制。本篇文章将介绍一下,rabbitmq在高可用过程中所出现的问题以及如何解决这些问题从而保证高可用。
事务都并非十全十美的,rabbitmq也一样,如果rabbitmq的配置和使用不当,将会出现各种头疼的问题,比如:
1、发出去的 消息不知道到底有没有发送成功?即开发者自认为已经将消息发送出去了,但是在某些情况下,比如交换机、路由和队列绑定构成的消息模型不存在时,将很有可能发送失败。
2、由于某些特殊的原因,Rabbitmq服务出现宕机和崩溃问题,导致其需要重启,如果此时队列中仍然由大量的消息还未被消费,则很有可能在重启Rabbitmq服务过程中发生消息丢失现象。
3、当Rabbitmq服务器上有上万条未处理的消息,这时候打开一个消费者客户端,将会导致巨量的消息瞬间推过来,单个客户端无法同时处理那么多消息,导致消费端崩溃。
4、消费者在监听消息的时候,可能会出现监听失败或者直接崩溃的情况,这种情况也会导致消息所在的队列找不到对应的消费者而不断的重新进入队列,最终出现消息被重复消费的情况。
5、如果我们在发送消息的时候,当前的exchange不存在或者指定的路由key路由不到。
针对上面的问题,rabbitmq给出了相应的 解决 方案,保证消息高可用和被准确消费:
1、针对上面第一种情况,rabbitmq要求生产者在发送完消息之后进行“发送确认”,当确认成功是即代表消息已经成功发送出去了。
实例:
生产者
public class Producer {
public static void main(String[] args) throws Exception {
//1 创建ConnectionFactory
ConnectionFactory connectionFactory = new ConnectionFactory();
connectionFactory.setHost("192.168.237.139");
connectionFactory.setPort(5672);
connectionFactory.setVirtualHost("/");
connectionFactory.setUsername("zhy");
connectionFactory.setPassword("zhy");
//2 获取Connection
Connection connection = connectionFactory.newConnection();
//3 通过Connection创建一个新的Channel
Channel channel = connection.createChannel();
//4 指定我们的消息投递模式: 消息的确认模式
channel.confirmSelect();
String exchangeName = "test_confirm_exchange";
String routingKey = "confirm.save";
String queueName = "test_confirm_queue";
//4 声明交换机和队列 然后进行绑定设置, 最后制定路由Key
channel.exchangeDeclare(exchangeName, "topic", true);
channel.queueDeclare(queueName, true, false, false, null);
channel.queueBind(queueName, exchangeName, routingKey);
//5 发送一条消息
String msg = "Hello RabbitMQ Send confirm message!";
channel.basicPublish(exchangeName, routingKey, null, msg.getBytes());
//6 添加一个确认监听
channel.addConfirmListener(new ConfirmListener() {
@Override
public void handleNack(long deliveryTag, boolean multiple) throws IOException {
System.err.println("-------no ack!-----------");
}
@Override
public void handleAck(long deliveryTag, boolean multiple) throws IOException {
System.err.println("-------ack!-----------");
}
});
}
}
消费者
public class Consumer {
public static void main(String[] args) throws Exception {
//1 创建ConnectionFactory
ConnectionFactory connectionFactory = new ConnectionFactory();
connectionFactory.setHost("192.168.237.139");
connectionFactory.setPort(5672);
connectionFactory.setVirtualHost("/");
connectionFactory.setUsername("zhy");
connectionFactory.setPassword("zhy");
//2 获取Connection
Connection connection = connectionFactory.newConnection();
//3 通过Connection创建一个新的Channel
Channel channel = connection.createChannel();
String queueName = "test_confirm_queue";
//5 创建消费者
QueueingConsumer queueingConsumer = new QueueingConsumer(channel);
channel.basicConsume(queueName, true, queueingConsumer);
while(true){
QueueingConsumer.Delivery delivery = queueingConsumer.nextDelivery();
String msg = new String(delivery.getBody());
System.err.println("消费端: " + msg);
}
}
}
2、针对上面第二种情况,rabbitmq强烈建议开发者在创建队列、交换机时设置期持久化参数为true,即durable参数取值为true,同时在创建消息是,设置消息持久化模式为“持久化”,从而保证出现宕机重启情况时,队列交换机仍然存在消息不丢失。
3、针对上面第三种情况,我们可以对消息进行限流操作,RabbitMQ提供了一种qos (服务质量保证)功能,即在非自动确认消息的前提下,如果一定数目的消息(通过基于consume或者channel设Qos的值)未被确认前,不进行消费新的消息。
void BasicQos(uint prefetchSize, ushort prefetchCount, bool global);
这里有一个前提是“在非自动确认消息的前提下”,如果我们想让上面的语句生效,必须保证no_ack=false。
实例:
生产者
public class Producer {
public static void main(String[] args) throws Exception{
ConnectionFactory connectionFactory = new ConnectionFactory();
connectionFactory.setHost("192.168.237.139");
connectionFactory.setPort(5672);
connectionFactory.setVirtualHost("/");
Connection connection = connectionFactory.newConnection();
Channel channel = connection.createChannel();
String exchange = "test_qos_exchange";
String routingKey = "qos.save";
String msg = "Hello RabbitMQ QOS Message";
for(int i =0; i<5; i ++){
channel.basicPublish(exchange, routingKey, true, null, msg.getBytes());
}
}
}
消费者
public class Consumer {
public static void main(String[] args) throws Exception {
ConnectionFactory connectionFactory = new ConnectionFactory();
connectionFactory.setHost("192.168.237.139");
connectionFactory.setPort(5672);
connectionFactory.setVirtualHost("/");
Connection connection = connectionFactory.newConnection();
Channel channel = connection.createChannel();
String exchangeName = "test_qos_exchange";
String queueName = "test_qos_queue";
String routingKey = "qos.#";
//声明和绑定
channel.exchangeDeclare(exchangeName, "topic", true, false, null);
channel.queueDeclare(queueName, true, false, false, null);
channel.queueBind(queueName, exchangeName, routingKey);
//1 限流方式 第一件事就是 autoAck设置为 false,每次给消费者推送一个消息
channel.basicQos(0, 1, false);
channel.basicConsume(queueName, false, new MyConsumer(channel));
}
}
自定义消费者
/**
* 自定义消费者
*/
public class MyConsumer extends DefaultConsumer {
private Channel channel;
public MyConsumer(Channel channel) {
super(channel);
this.channel=channel;
}
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
System.err.println("-----------consume message----------");
System.err.println("consumerTag: " + consumerTag);
System.err.println("envelope: " + envelope);
System.err.println("properties: " + properties);
System.err.println("body: " + new String(body));
//确认消息的方法,回调成功以后再执行下一条,表示这条消息我已经处理完了,你可以给我下一条了。false表示不批量签收
// channel.basicAck(envelope.getDeliveryTag(),false);
}
}
运行上面代码发现(先启动消费者,在启动生产者),发现消费者控制台中只打印出了一条消息,因为我们在声明消费者的时候关闭了自动确认机制,需要进行手动确认,但是手动确认的代码我们给注释掉了,导致rabbitmq一直收不到确认消息,rabbitmq服务器将会任务这个消费者存在问题,就不会在次给推送消息了。
下面我们把消费者代码中的手动确认代码放开,再次运行,发现消息可以被全部消费了。
4、针对上面第四种情况,即如何保证消息能够被准确消费、不重复消费,RabbitMq提供了消息确认机制,即ACK模式。
这种机制分为三种类型:
none(无须确认)
auto(自动确认)
manual(手动确认)
在实际生产环境中,为了提高消息的高可用、防止消息重复消费,一般都会使用确认机制,只有当消息被确认消费之后,消息才会从队列中被移除,这也是避免消息被重复消费的实现方式。
手动ack实例
生产者
public class Producer {
public static void main(String[] args) throws Exception {
ConnectionFactory connectionFactory = new ConnectionFactory();
connectionFactory.setHost("192.168.237.139");
connectionFactory.setPort(5672);
connectionFactory.setVirtualHost("/");
Connection connection = connectionFactory.newConnection();
Channel channel = connection.createChannel();
String exchange = "test_ack_exchange";
String routingKey = "ack.save";
for(int i =0; i<5; i ++){
Map<String, Object> headers = new HashMap<String, Object>();
headers.put("num", i);
AMQP.BasicProperties properties = new AMQP.BasicProperties.Builder()
.deliveryMode(2) //传送方式
.contentEncoding("UTF-8") //编码方式
.headers(headers) //自定义属性
.build();
String msg = "Hello RabbitMQ ACK Message " + i;
channel.basicPublish(exchange, routingKey, true, properties, msg.getBytes());
}
}
}
消费者
public class Consumer {
public static void main(String[] args) throws Exception {
ConnectionFactory connectionFactory = new ConnectionFactory();
connectionFactory.setHost("192.168.237.139");
connectionFactory.setPort(5672);
connectionFactory.setVirtualHost("/");
Connection connection = connectionFactory.newConnection();
Channel channel = connection.createChannel();
String exchangeName = "test_ack_exchange";
String queueName = "test_ack_queue";
String routingKey = "ack.#";
channel.exchangeDeclare(exchangeName, "topic", true, false, null);
channel.queueDeclare(queueName, true, false, false, null);
channel.queueBind(queueName, exchangeName, routingKey);
// 手工签收 必须要关闭 autoAck = false
channel.basicConsume(queueName, false, new MyConsumer(channel));
}
}
自定义消费者
public class MyConsumer extends DefaultConsumer {
private Channel channel ;
public MyConsumer(Channel channel) {
super(channel);
this.channel = channel;
}
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
System.err.println("-----------consume message----------");
System.err.println("body: " + new String(body));
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
if((Integer)properties.getHeaders().get("num") == 0) {
//multiple:是否批量 requeue:true 是否重回队列,确认失败,重新将消息放到队列尾部执行。
channel.basicNack(envelope.getDeliveryTag(), false, true);
} else {
channel.basicAck(envelope.getDeliveryTag(), false);
}
}
查看消费者控制台输出
由于第一条一直设置为重回队列,确认失败,所以rabbitmq服务器将会一直推送第一条消息给消费者。
4、针对上面第四种情况,我们需要监听这种不可达的消息,需要使用Return Listener
在基础API中有一个关键的配置项参数: .Mandatory:如果为true,则监听器会接收到路由不可达的消息,然后进行后续处理,如果为false,那么broker端自动删除该消息!
实例:
我们编写一个生产者,让其队列和交换机绑定的routingkey和消息发送时填写的routingkey不一致,查看情况:
public class Producer {
public static void main(String[] args) throws Exception {
ConnectionFactory connectionFactory = new ConnectionFactory();
connectionFactory.setHost("192.168.237.139");
connectionFactory.setPort(5672);
connectionFactory.setVirtualHost("/");
Connection connection = connectionFactory.newConnection();
Channel channel = connection.createChannel();
String exchange = "test_return_exchange";
String routingKey = "return.save";
String routingKeyError = "abc.save";
String queueName = "test_return_queue";
String msg = "Hello RabbitMQ Return Message";
channel.exchangeDeclare(exchange, "topic", true, false, null);
channel.queueDeclare(queueName, true, false, false, null);
channel.queueBind(queueName, exchange, routingKey);
// 添加返回监听不可达消息的处理结果
channel.addReturnListener(new ReturnListener() {
@Override
public void handleReturn(int replyCode, String replyText, String exchange,
String routingKey, AMQP.BasicProperties properties, byte[] body) throws IOException {
System.err.println("---------handle return----------");
System.err.println("replyCode: " + replyCode);
System.err.println("replyText: " + replyText);
System.err.println("exchange: " + exchange);
System.err.println("routingKey: " + routingKey);
System.err.println("properties: " + properties);
System.err.println("body: " + new String(body));
}
});
//设置一个错误的路由规则就可以返回return消息
// 第三个参数为Mandatory,如果为true,则监听器会接受到路由不可达的消息,然后进行后续处理
// 如果为false,那么broker端(mq服务器)自动删除该消息
channel.basicPublish(exchange, routingKeyError, true, null, msg.getBytes());
//设置正确的路由规则就会正常消费消息
// channel.basicPublish(exchange, routingKey, true, null, msg.getBytes());
}
}