经过前面三篇文章的学习,对于RabbitMQ中间件应该处于拨开云雾见青天阶段。本文将趁热打铁,完善RabbitMQ基础应用最后一个消费版块。当然文中会持续深入讲解有关消息分发、消费端确认等中阶特性
MQ队列可以理解为物品寄存中心,放进去总要拿出来用,一直放着没有利息还会持久增加成本引发系列问题。MQ存储的消息使用有两种途径,RabbitMQ服务推送、消费者客户端拉取
通过baiscGet()拉取RabbitMQ服务端消息有以下几点需要注意:
一次只能消费一条消息,千万别使用用循环代替后面的baiscConsume()
自动删除前提为至少有一个消费者连接到队列
,并且当所有消费者断开时删除,这里通过basicGet()消费不包含在内
// 设置队列自动删除
channel.queueDeclare("autoDeleteQueue", true, false, true, null);
channel.basicPublish("", "autoDeleteQueue", null, "测试".getBytes());
// 验证basicGet不触发队列自动删除
channel.basicGet("autoDeleteQueue", true);
当然basicGet()
方法自身API比较简单,第一个参数指明消费队列,第二个参数设置是否自动应答即AutoAck。返回对象也就封装Envelope
、BasicProperties
、body
消息体等,具体信息如下表所示:
序号 | 方法参数 | 含义 |
---|---|---|
1 | queue | 队列名称,指定消费者消费队列 |
2 | autoAck | 自动应答,打开为true后RabbitMQ应用送出消息将立即删除 |
序号 | 返回值 | 备注 |
---|---|---|
1 | envelope | 包含deliveryTag、exchange、routingKey等信息 |
2 | props | BasicProperties对象,即消息生产时设置的该对象特性 |
3 | body | 消息体byte数组 |
4 | messageCount | 消息数量 |
相对于拉取消息而言,basicConsume()
推送消息更加符合生产环境的需求,持续监控消费队列。自然其API也更加复杂,常用系列重载参数如下表所示:
序号 | 方法参数 | 含义 |
---|---|---|
1 | queue | 消费队列名称 |
2 | autoAck | 自动确认提交 |
3 | consumerTag | 消费者唯一标识 |
4 | noLocal | 不消费同一Connection连接生产的消息 |
5 | consumer | 具体组织消费逻辑对象,里面提供系列重载方法用户消费逻辑组装 |
推送消息消费最后一般都是采用实现Consumer
接口亦或是继承DefaultConsumer
类,DefaultConsumer实现了接口Consumer,但是大多数方法都是空实现,需要重写其中的逻辑。其中重要方法执行时间如下表所示:
DefaultConsumer defaultConsumer = new DefaultConsumer(channel){
@Override
public void handleDelivery (String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
// 处理消息逻辑
}
};
channel.basicConsume("queueName",true,"consumerTag",defaultConsumer);
序号 | 方法 | 执行时间 |
---|---|---|
1 | handleDelivery() | 消费消息逻辑 |
2 | handleConsumeOk() | 第一篇文章就就讲到Consume-Ok命令在delivery命令前,即每个消费者开始消费前都会执行该方法一次 |
3 | handleShutdownSignal() | 当连接Connection / 信道Channel断开关闭时执行一次 |
4 | handleRecoverOk() | baiscRecover()命令队列重发未确认消息,当未确认消息被重发前执行这个方法 |
5 | handleCancelOk() | baiscCancel()显示取消消费者,当消费者被取消时指定这个方法 |
上面讲到Consumer的方法handleRecoverOk()
将会在消息重发时调用,显示的调用消息重发方法为basicRecover()
。该方法只有一个参数:
第一篇文章中有一个命令是Basic.Ack用于客户端向服务端反馈确认消息已经正常消费,当接收到命令后RabbityMQ服务端才会删除消息,从消费者客户端确保消息不会丢失。自然有确认就有拒绝确认,本节将介绍basicAck()、basicReject()、basicNack()
RabbitMQ反馈确认消费通过命令basicAck()
实现,该方法具备两个参数deliveryTag
和multiple
channel.basicAck(envelope.getDeliveryTag(),false);
特别注意:消息的编码是每个信道Channel范围的,批量确认操作也是针对当前Channel信道的操作。请务必记住这个范围
程序在消费消息过程中抛出异常,或者是消息需要重复消费,这时候就可以将消费的消息拒绝确认。拒绝确认的消息有两种去处,删除、放回队列,通过参数requeue
控制,拒绝确认的消息放回队列时会放置在队列首位,拒绝消息不放回队列可以搭配死信队列使用
void basicReject(long deliveryTag, boolean requeue) throws IOException;
确认消费可以批量确认,为什么拒绝确认消息不能批量拒绝?所以为了补充basicReject()不足提出了basicNack()
。这个API相对于basicReject()而言多了一个参数multiple
,效果与批量确认一致
void basicNack(long deliveryTag, boolean multiple, boolean requeue)
throws IOException;
消息消费RabbitMQ采取的策略就是轮询机制,将每个消息发送给唯一的消费者。每个消费者获取到的消息都是平均的,这样的机制会导致下列问题:
通过下面代码可以告诉RabbitMQ服务端,我只接受prefetchCount
数量的未确认消息,当消费者客户端未确认消息达到限定值后服务端将不会给该消费者推送数据。第二个参数的含义如下表:
参数值 | 含义 |
---|---|
false | 默认值,单独应用于信道上所有消费者 |
true | 信道上所有消费者共享 |
void basicQos(int prefetchCount, boolean global) throws IOException;
合理的消息预取配合消费端手动ACK确认机制可以很好的优化平衡消费者性能,这个预取数量问题可以根据队列消息增长率与消费端消息处理效率平衡
使用RabbitMQ完成RPC操作其实比较简单,就是使用了前面讲到过的BasicPeoperties
对象。发送消息时消息可以携带该对象,前面使用到了对象的deliveryMod
持久化、priority
优先级、expiration
自动过期删除属性。这里RPC将会使用到replyTo
属性告诉RPC服务端执行完毕后回调队列地址,correlationId
用于标识请求唯一ID
@SneakyThrows
public static void main (String[] args) {
Channel channel = TemplateConfigServiceImpl.createChannel();
// 创建BasicProperties赋值replyTo回调队列名称、correlationId请求唯一标识ID
String correlationId = UUID.randomUUID().toString();
String replyQueue = "queue1";
AMQP.BasicProperties basicProperties = new AMQP.BasicProperties.Builder().replyTo(replyQueue).correlationId(correlationId).build();
// 客户端向服务端监控队列发送消息
String rpcQueue = "queueName";
channel.basicPublish("",rpcQueue,basicProperties,"RPC测试".getBytes());
// 创建阻塞队列等到消息回调
ArrayBlockingQueue<String> arrayBlockingQueue = new ArrayBlockingQueue<>(1);
// 监控回调队列消息获取远程调用结果
DefaultConsumer defaultConsumer = new DefaultConsumer(channel){
@Override
public void handleDelivery (String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
// 校验消息唯一标识是否匹配
String replyCorrelationId = properties.getCorrelationId();
long deliveryTag = envelope.getDeliveryTag();
if (correlationId.equals(replyCorrelationId)){
// 将回调消息放到阻塞队列中
arrayBlockingQueue.offer(new String(body));
channel.basicAck(deliveryTag,false);
}else {
// 不匹配消息放回队列
channel.basicReject(deliveryTag,true);
}
}
};
channel.basicConsume(replyQueue,false,"ConsumerTag",defaultConsumer);
// 阻塞等待阻塞队列中消息处理后续逻辑
String take = arrayBlockingQueue.take();
System.out.println(take);
}
整体RPC服务端、客户端实现都是最简陋的自行车设计,如果想要更复杂的逻辑请自行完成
@SneakyThrows
public static void main (String[] args) {
Channel channel = TemplateConfigServiceImpl.createChannel();
// 监控RPC队列消息执行任务
String rpcQueue = "queueName";
DefaultConsumer defaultConsumer = new DefaultConsumer(channel){
@Override
public void handleDelivery (String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
// 执行计算逻辑
System.out.println("RPC远程服务端开始执行任务");
System.out.println(new String(body));
// 组装回调消息
String replyTo = properties.getReplyTo();
channel.basicPublish("",replyTo,properties,"RPC远程计算任务完成".getBytes());
// 确认消息消费
long deliveryTag = envelope.getDeliveryTag();
channel.basicAck(deliveryTag,false);
}
};
channel.basicConsume(rpcQueue,false,"ConsumerTag",defaultConsumer);
}