包含rabbitmq生产的消息批量发送、异步发送、事物、手动确认,消费者的消息确认回执、消息拒绝;
一. Queue 的消息投送
当一个Queue 有多个consumer, 多个消费者可以订阅同一个Queue,这时Queue中的消息会被平均分摊给多个消费者进行处理,而不是每个消费者都收到所有的消息并处理。
可见3条消息分别被3个消费者获取,所以RabbitMQ是采用轮询机制将消息队列Queue中的消息依次发给不同的消费者;
如果需要保证消费者每个消费者的压力相同可以更改消息的投送
我们可以使用basicQos方法和prefetchCount = 1设置。 这告诉RabbitMQ一次不要向工作人员发送多于一条消息。 或者换句话说,不要向工作人员发送新消息,直到它处理并确认了前一个消息。 相反,它会将其分派给不是仍然忙碌的下一个工作人员
在消费者上加入:
channel.basicQos(1);
生产者消费者都不需要指定exchange、QUEUE_NAME 使用同一个,消费者1,消费者2的QUEUE_NAME 都相同
二. 发送消息确认
我们除了要考虑消费者消息失败可能失败的情况,我们还需要考虑,消息的发布者在将消息发送出去之后,消息到底有没有正确到达消息中间件呢,如果没到达,我们又需要怎么处理呢?
abbitMQ为我们提供了两种方式来解决这个问题:
- 事务:通过AMQP事务机制实现,这也是AMQP协议层面提供的解决方案。(性能太差,基本不用)
- confirm:通过将channel设置成confirm模式来实现;
1.事物
RabbitMQ事物的处理包装channel调用代码为:
Tx.SelectOk txSelect() throws IOException;
Tx.CommitOk txCommit() throws IOException;
Tx.RollbackOk txRollback() throws IOException;
txSelect主要用于将当前channel设置成transaction模式,txCommit用于提交事务,txRollback用于回滚事务。
只要我们使用过事物的同学都知道,事物没有什么好解释的,我这边就给大家一个案例,不了解事物的同学也不要着急,因为它性能太差,官方主动弃用,所以我们不了解也无所谓,重点关注第二种:
try {
// 开启事务
channel.txSelect();
// 往test队列中发出一条消息
channel.basicPublish("test", "test", null, messageBodyBytes);
// 提交事务
channel.txCommit();
} catch (Exception e) {
e.printStackTrace();
// 事务回滚
try {
channel.txRollback();
} catch (IOException e1) {
e1.printStackTrace();
}
}
2.手动确认confirm
confirm机制:
在confirm机制下,我们可以将channel设置成confirm模式,一旦channel进入confirm模式,所有在该channel上面发布的消息都将会被指派一个唯一的ID(从1开始),一旦消息被投递到所有匹配的队列之后,broker就会发送一个确认给生产者(包含消息的唯一ID),这就使得生产者知道消息已经正确到达目的队列了,如果消息和队列是可持久化的,那么确认消息会在将消息写入磁盘之后发出,broker回传给生产者的确认消息中delivery-tag域包含了确认消息的序列号,此外broker也可以设置basic.ack的multiple域,表示到这个序列号之前的所有消息都已经得到了处理;
confirm模式最大的好处在于它是异步的,一旦发布一条消息,生产者应用程序就可以在等信道返回确认的同时继续发送下一条消息,当消息最终得到确认之后,生产者应用便可以通过回调方法来处理该确认消息,如果RabbitMQ因为自身内部错误导致消息丢失,就会发送一条nack消息,生产者应用程序同样可以在回调方法中处理该nack消息;
confirm机制和transaction事务模式是不能够共存的,已经处于transaction事务模式的channel不能被设置为confirm模式,同理,反过来也一样。通常我们可以通过调用channel的confirmSelect方法将channel设置为confirm模式。如果没有设置no-wait标志的话,broker会返回confirm.select-ok表示同意生产者当前channel信道设置为confirm模式。
1.普通confirm方法
//开启confirm模式
channel.confirmSelect();
//模拟发送50条消息
for(int i =0;i<1000;i++){
String message = "Hello World RabbitMQ";
//发送消息
channel.basicPublish(EXCHANGE_NAME,"",MessageProperties.PERSISTENT_TEXT_PLAIN,message.getBytes());
//每发送2条判断一次是否回复
if(i%2==0){
//waitForConfirms可以换成带有时间参数的方法waitForConfirms(Long mills)指定等待响应时间
if(channel.waitForConfirms()){
System.out.println("Message send success.");
}
}
}
2.confirm 批量确认
class Sender
{
private ConnectionFactory factory;
private int count;
private String exchangeName;
private String queueName;
private String routingKey;
private String bindingKey;
public Sender(ConnectionFactory factory,int count,String exchangeName,String queueName,String routingKey,String bindingKey) {
this.factory = factory;
this.count = count;
this.exchangeName = exchangeName;
this.queueName = queueName;
this.routingKey = routingKey;
this.bindingKey = bindingKey;
}
public void run() {
Channel channel = null;
try {
Connection connection = factory.newConnection();
channel = connection.createChannel();
//创建exchange
channel.exchangeDeclare(exchangeName, "direct", true, false, null);
//创建队列
channel.queueDeclare(queueName, true, false, false, null);
//绑定exchange和queue
channel.queueBind(queueName, exchangeName, bindingKey);
channel.confirmSelect();
//发送持久化消息
for(int i = 0;i < count;i++)
{
//第一个参数是exchangeName(默认情况下代理服务器端是存在一个""名字的exchange的,
//因此如果不创建exchange的话我们可以直接将该参数设置成"",如果创建了exchange的话
//我们需要将该参数设置成创建的exchange的名字),第二个参数是路由键
channel.basicPublish(exchangeName, routingKey,MessageProperties.PERSISTENT_BASIC, ("第"+(i+1)+"条消息").getBytes());
}
long start = System.currentTimeMillis();
channel.waitForConfirmsOrDie();
System.out.println("执行waitForConfirmsOrDie耗费时间: "+(System.currentTimeMillis()-start)+"ms");
} catch (Exception e) {
e.printStackTrace();
}
}
批量的方法从数量级上降低了confirm的性能消耗,提高了效率,但是批量confmn方式的问题在于遇到RabbitMQ服务端返回Basic.Nack 需要重发批量消息而导致的性能降低,也可能导致消息重复消费
3.confirm异步确认(推荐)
提供一个回调方法,服务端确认了一条或者多条消息后客户端会回调这个方法进行处理
public class ConfirmProducer {
public static void main(String[] args) throws Exception {
//1 创建ConnectionFactory
ConnectionFactory connectionFactory = new ConnectionFactory();
connectionFactory.setHost("192.168.1.28");
connectionFactory.setPort(5672);
connectionFactory.setVirtualHost("/");
connectionFactory.setUsername("toher");
connectionFactory.setPassword("toher888");
//2 创建Connection
Connection connection = connectionFactory.newConnection();
//3 创建Channel
Channel channel = connection.createChannel();
//4 指定我们的消息投递模式: 消息的确认模式
channel.confirmSelect();
//5 声明交换机 以及 路由KEY
String exchangeName = "test_confirm_exchange";
String routingKey = "confirm.send";
//6 发送一条消息
String msg = "Test Confirm Message";
channel.basicPublish(exchangeName, routingKey, null, msg.getBytes());
//7 添加确认监听
channel.addConfirmListener(new ConfirmListener(){
@Override
public void handleNack(long deliveryTag, boolean multiple) throws IOException {
System.err.println("收到NACK应答");
}
@Override
public void handleAck(long deliveryTag, boolean multiple) throws IOException {
System.err.println("收到ACK应答");
}
});
}
}
从上面代码我们可以看到有重写了ConfirmListener两个方法:handleNack 和 handleAck,分别用来处理RabbitMQ 回传的Basic.Nack和Basic.Ack;
它们都有两个参数:
long deliveryTag : 前面介绍确认消息的ID
boolean multiple : multiple 是否批量 如果是True 则将比该deliveryTag小的所有数据都移除 否则只移除该条;
我们简单的用一个数组来说明 [1,2,3,4]存储着4条消息ID , 此时确认消息返回的是 deliveryTag = 3 ,multiple = true那么RabbitMQ会通知我们小于ID3的消息得到确认了,如果multiple = false, 就通知我们ID3的确认了
//声明一个用来记录消息唯一ID的有序集合SortedSet
final SortedSet<Long> confirmSet = Collections.synchronizedSortedSet(new TreeSet<Long>());
//开启confirm模式
channel.confirmSelect();
//异步监听方法 处理ack与nack方法
channel.addConfirmListener(new ConfirmListener() {
//处理ack multiple 是否批量 如果是批量 则将比该条小的所有数据都移除 否则只移除该条
public void handleAck(long deliveryTag, boolean multiple) throws IOException {
if (multiple) {
confirmSet.headSet(deliveryTag).clear();
} else {
confirmSet.remove(deliveryTag);
}
}
//处理nack 与ack相同
public void handleNack(long deliveryTag, boolean multiple) throws IOException {
System.out.println("There is Nack, SeqNo: " + deliveryTag + ", multiple: " + multiple);
if (multiple) {
confirmSet.headSet(deliveryTag).clear();
} else {
confirmSet.remove(deliveryTag);
}
}
});
for(int i =0;i<100;i++) {
// 查看下一个要发送的消息的序号
long nextSeqNo = channel.getNextPublishSeqNo();
try {
channel.basicPublish("test", "test", MessageProperties.PERSISTENT_TEXT_PLAIN, messageBodyBytes);
} catch (IOException e) {
e.printStackTrace();
}
confirmSet.add(nextSeqNo);
}
以上代码按照每一个comfirm的通道维护一个集合,每发送一条数据,集合增加一个元素,每异步响应一条ack或者nack的数据,集合删除一条。SortedSet是一个有序的集合,它的有序是值大小的有序,不是插入时间的有序。JDK中waitForConfirms()方法也是使用了SortedSet集合
3. 发送消息的批量确认
三.消费者消息确认
在实际应用中,可能会发生消费者收到Queue中的消息,但没有处理完成就宕机(或出现其他意外)的情况,这种情况下就可能会导致消息丢失。为了避免这种情况发生,我们可以要求消费者在消费完消息后发送一个回执给RabbitMQ,RabbitMQ收到消息回执(Message acknowledgment)后才将该消息从Queue中移除;如果RabbitMQ没有收到回执并检测到消费者的RabbitMQ连接断开,则RabbitMQ会将该消息发送给其他消费者(如果存在多个消费者)进行处理。这里不存在timeout概念,一个消费者处理消息时间再长也不会导致该消息被发送给其他消费者,除非它的RabbitMQ连接断开。
如何来实现呢?只需要将consumer消费者端中 no_ack = True去掉就行了
no_ack 就 no acknowlegment的意思,这个参数会导致RabbitMQ并不关心消费者有没有处理完成,可能在消费者获取消息后就将该消息从Queue中移除。去掉这个参数,如果在消费者执行过程当初出现了意外(宕机),RabbitMQ没有收到消息回执,就会发送给其他消费者执行。
从安全角度考虑,网络是不可靠的,消费者是有可能在处理消息的时候失败。而我们总是希望我们的消息不能因为处理失败而丢失,基于此原因,rabbitmq提供了一个消息确认(message acknowledgements) 的概念:当一个消息从队列中投递给消费者(consumer)后,消费者会通知一下消息中间件(rabbitmq),这个可以是系统自动autoACK的也可以由处理消息的应用操作。
当 “消息确认” 被启用的时候,rabbitmq不会完全将消息从队列中删除,直到它收到来自消费者的确认回执(acknowledgement)。
为了解决这个问题,rabbitmq提供了2种处理模式来解决这个问题:
- 自动确认模式(automatic acknowledgement model):当RabbbitMQ将消息发送给应用后,消费者端自动回送一个确认消息。(使用AMQP方法:basic.deliver或basic.get-ok)。
- 显式确认模式(explicit acknowledgement model):RabbbitMQ不会完全将消息从队列中删除,直到消费者发送一个确认回执(acknowledgement)后再删除消息。(使用AMQP方法:basic.ack)。
1. 消费显式确认模式
在显式确认模式下,消费者可以自由选择什么时候发送确认回执(acknowledgement)。消费者可以在收到消息后立即发送,或将未处理的消息存储后发送,或等到消息被处理完毕后再发送确认回执。
如果一个消费者在尚未发送确认回执的情况下挂掉了,那rabbitmq会将消息重新投递给另一个消费者。如果当时没有可用的消费者了,消息代理会死等下一个注册到此队列的消费者,然后再次尝试投递。
消费者在获取队列消息时,可以指定autoAck参数,采用显式确认模式,需要指定autoAck = flase,在显式确认模式,RabbitMQ不会为未ack的消息设置超时时间,它判断此消息是否需要重新投递给消费者的唯一依据是消费该消息的消费者连接是否已经断开。如果断开连接,RabbitMQ也没有收到ACK,则Rabbit MQ会安排该消息重新进入队列,等待投递给下一个消费者。
在显式确认模式,确认回执的案例如下:
//设置非自动回执
boolean autoAck = false;
Channel finalChannel = channel;
try {
channel.basicConsume("test", autoAck, "test-consumer-tag",
new DefaultConsumer(finalChannel) {
@Override
public void handleDelivery(String consumerTag,
Envelope envelope,
AMQP.BasicProperties properties,
byte[] body)
throws IOException
{
//发布的每一条消息都会获得一个唯一的deliveryTag,deliveryTag在channel范围内是唯一的
long deliveryTag = envelope.getDeliveryTag();
//第二个参数是批量确认标志。如果值为true,则执行批量确认,此deliveryTag之前收到的消息全部进行确认; 如果值为false,则只对当前收到的消息进行确认
finalChannel.basicAck(deliveryTag, true);
}
});
} catch (IOException e) {
e.printStackTrace();
}
上面我们显式的成功回执了我们的消息,但是假如我们发现我们的消费者处理不了这个消息需要其他的消费者处理怎么办呢,我们还可以拒绝消息。
每一个consumer都有一个唯一标识,rabbitmq称其为consumer tag,通过consumer tag可以对consumer进行操作,比如取消某个consumer等。
消费确认消息API
/** Acknowledge one or several received * messages. *
@param deliveryTag 发布的每一条消息都会获得一个唯一的deliveryTag,deliveryTag在channel范围内是唯一的 *
@param multiple 批量确认标志。如果值为true,则执行批量确认,此deliveryTag之前收到的消息全部进行确认; 如果值为false,则只对当前收到的消息进行确认
@throws java.io.IOException if an error is encountered */
void basicAck(long deliveryTag, boolean multiple) throws IOException;
消费拒绝消息API
/** * Reject a message. *
@param deliveryTag 发布的每一条消息都会获得一个唯一的deliveryTag,deliveryTag在channel范围内是唯一的 *
@param requeue 是否重回队列 如果值为true,则重新放入RabbitMQ的发送队列,如果值为false,则通知RabbitMQ销毁这条消息 * @throws java.io.IOException if an error is encountered */
void basicReject(long deliveryTag, boolean requeue) throws IOException;
有没有发现,我们似乎只能拒绝一条消息,后面rabbitMQ又补充了basicNack一次对多条消息进行拒绝
/** * Reject one or several received messages. *
@param deliveryTag 发布的每一条消息都会获得一个唯一的deliveryTag,deliveryTag在channel范围内是唯一的 *
@param multiple 批量确认标志。如果值为true,则执行批量确认,此deliveryTag之前收到的消息全部进行拒绝; 如果值为false,则只对当前收到的消息进行拒绝 *
@param requeue 是否重回队列 如果值为true,则重新放入RabbitMQ的发送队列,如果值为false,则通知RabbitMQ销毁这条消息 * @throws java.io.IOException if an error is encountered */
void basicNack(long deliveryTag, boolean multiple, boolean requeue)
throws IOException;
这里我们需要注意一下,如果我们的队列目前只有一个消费者,请注意不要拒绝消息并放回队列导致消息在同一个消费者身上无限循环无法消费的情况发生。