当数据通过生产者产生到发送到exchange交换器,再通过设定的路由规则,经过routingKey,最终会落地到queue 中。这个时候引出了Listener消费者了。在第一章中有对RabbitAnnotationDrivenConfiguration该类中源码做过基本分析,该类最大的作用就是通过配置文件的设定用于构建RabbitListenerContainerFactory(该接口的实现,工厂设计模式用以创建核心的MessageListenerContainer
容器类,而该类MessageListenerContainer用以管理MessageListener,最终实现的该接口类操作Connection最终来消费数据)
对于spring提供了两种工厂实现,SimpleRabbitListenerContainerFactory与DirectRabbitListenerContainerFactoryConfigurer,
用于构建SimpleMessageListenerContainer
在章节一中也描述了该模式下,rabbitmq的消费模式,该监听容器通过客户端多线程来并行的处理消息,通过上述的配置设置并行度。也可以监控多个队列并在运行时对队列进行增删
。系统通过 concurrentConsumers、maxConcurrentConsumers 灵活设定当前容器中消费者的数量,不论监控多少个队列,channel与当前线程都会一一对应(这也是为何命令为Simple的因素),即使用同一consumer线程来会处理所有的队列。即这里我们先使用该模式来进行消费。
SimpleMessageListenerContainer
当设置完成,我们如果在应用中创建SimpleMessageListenerContainer,来监听queue呢。spring也提供了两种方式
yml配置:
listener:
type: simple
simple:
##这里使用none后续对此进行详细描述
acknowledge-mode: none
auto-startup: true
##设置消费并行度
concurrency: 2
##设置最大并行度
max-concurrency: 2
##设置一个批次最大拉取数量
batch-size: 3
##不开启重试 避免重复消费
retry:
enabled: false
## 每个消费者可能未完成的未确认消息的最大数量
#prefetch:
## 若容器中声明的的queue不存在了(queue在代理上不可用或在运行时被删除了) 是否需要停止对应容器默认true
missing-queues-fatal: true
## 默认情况下,拒绝交付是否重新排队
default-requeue-rejected: true
# 应该多久发布一次空闲容器事件 可用于监控
idle-event-interval: 10000
单条消费,可以通过配置开启自动ack或不开启ack
@Autowired
private SimpleRabbitListenerContainerFactory simpleRabbitListenerContainerFactory;
//单条消费
@Bean
public MessageListenerContainer simpleMessageListenerContainer(){
SimpleMessageListenerContainer listenerContainer = simpleRabbitListenerContainerFactory.createListenerContainer();
//设置被监控的queue
listenerContainer.setQueueNames(CommonConstant.queue_direct1,CommonConstant.queue_direct2);
//等待消息到达超时时间默认1s
listenerContainer.setReceiveTimeout(1000);
//手动确认
listenerContainer.setAcknowledgeMode(AcknowledgeMode.NONE);
//默认10s
listenerContainer.setStartConsumerMinInterval(10*1000);
//默认60s
listenerContainer.setStopConsumerMinInterval(60*1000);
//设置消费者唯一标记 基于queue设置
listenerContainer.setConsumerTagStrategy(queue -> {
return queue+"_"+ UUID.randomUUID().toString();
});
listenerContainer.setMessageListener(message->{
//获取数据
byte[] body = message.getBody();
String str = new String(body);
Thread thread=Thread.currentThread();
log.info("message:{} ThreadId is:{} ConsumerTag:{} Queue:{}"
,str,thread.getId(),message.getMessageProperties().getConsumerTag(),message.getMessageProperties().getConsumerQueue());
});
return listenerContainer;
}
上述设置了2个并行情况,有两个线程(consumer)并行消费
包含channel与message可以进行手动ack确定
@Autowired
private SimpleRabbitListenerContainerFactory simpleRabbitListenerContainerFactory;
@Bean
public MessageListenerContainer simpleChannelMessageListenerContainer(){
SimpleMessageListenerContainer listenerContainer = simpleRabbitListenerContainerFactory.createListenerContainer();
//设置被监控的queue
listenerContainer.setQueueNames(CommonConstant.queue_direct1,CommonConstant.queue_direct2);
//等待消息到达超时时间默认1s
listenerContainer.setReceiveTimeout(1000);
//手动确认
listenerContainer.setAcknowledgeMode(AcknowledgeMode.MANUAL);
//默认10s
listenerContainer.setStartConsumerMinInterval(10*1000);
//默认60s
listenerContainer.setStopConsumerMinInterval(60*1000);
//设置消费者唯一标记 基于queue设置
listenerContainer.setConsumerTagStrategy(queue -> {
return queue+"_"+ UUID.randomUUID().toString();
});
//包含channel
listenerContainer.setMessageListener((ChannelAwareMessageListener) (message, channel) -> {
Thread thread=Thread.currentThread();
long maxDeliveryTag= 0;
//获取数据
maxDeliveryTag = message.getMessageProperties().getDeliveryTag();
//获取数据
byte[] body = message.getBody();
String str = new String(body);
log.info("deliveryTag:{} message:{} ThreadId is:{} ConsumerTag:{} Queue:{} channel:{}"
,maxDeliveryTag,str,thread.getId(),message.getMessageProperties().getConsumerTag()
,message.getMessageProperties().getConsumerQueue(),channel.getChannelNumber());
//批量确认
try {
channel.basicAck(maxDeliveryTag,false);
} catch (IOException e) {
//确认失败 处理message数据需要回滚
e.printStackTrace();
}
});
return listenerContainer;
}
上述设置了2个并行情况,有两个线程(consumer)并行消费,并进行手动ack确定 ,法系爱你channel与消费线程为一一对应
@Bean
public MessageListenerContainer batchMessageListenerContainer(){
DirectMessageListenerContainer listenerContainer = directRabbitListenerContainerFactory.createListenerContainer();
//设置被监控的queue
listenerContainer.setQueueNames(CommonConstant.queue_direct1,CommonConstant.queue_direct2);
//手动确认
listenerContainer.setAcknowledgeMode(AcknowledgeMode.NONE);
//为每个队列添加多个消费者 增加并行度
listenerContainer.setConsumersPerQueue(3);
//设置消费者唯一标记 基于queue设置
listenerContainer.setConsumerTagStrategy(queue -> {
return queue+"_"+ UUID.randomUUID().toString();
});
listenerContainer.setMessageListener((BatchMessageListener) messages->{
//批量消费
for (Message message:messages ) {
//获取数据
byte[] body = message.getBody();
String str = new String(body);
Thread thread=Thread.currentThread();
log.info("message:{} ThreadId is:{} ConsumerTag:{} Queue:{}"
,str,thread.getId(),message.getMessageProperties().getConsumerTag(),message.getMessageProperties().getConsumerQueue());
}
});
return listenerContainer;
}
最终结果:
构建代码如下所示:
@Bean
public MessageListenerContainer batchChannelMessageListenerContainer(){
//设置开启批量处理
simpleRabbitListenerContainerFactory.setConsumerBatchEnabled(true);
simpleRabbitListenerContainerFactory.setBatchListener(true);
SimpleMessageListenerContainer listenerContainer = simpleRabbitListenerContainerFactory.createListenerContainer();
//设置被监控的queue
listenerContainer.setQueueNames(CommonConstant.queue_direct1,CommonConstant.queue_direct2);
//等待消息到达超时时间默认1s
listenerContainer.setReceiveTimeout(1000);
//手动确认
listenerContainer.setAcknowledgeMode(AcknowledgeMode.MANUAL);
//默认10s
listenerContainer.setStartConsumerMinInterval(10*1000);
//默认60s
listenerContainer.setStopConsumerMinInterval(60*1000);
//设置消费者唯一标记 基于queue设置
listenerContainer.setConsumerTagStrategy(queue -> {
return queue+"_"+ UUID.randomUUID().toString();
});
listenerContainer.setMessageListener((ChannelAwareBatchMessageListener) (messages,channel)->{
Thread thread=Thread.currentThread();
long maxDeliveryTag= 0;
//批量消费
for (Message message:messages ) {
//获取数据
maxDeliveryTag = message.getMessageProperties().getDeliveryTag();
//获取数据
byte[] body = message.getBody();
String str = new String(body);
log.info("deliveryTag:{} message:{} ThreadId is:{} ConsumerTag:{} Queue:{} channel:{}"
,maxDeliveryTag,str,thread.getId(),message.getMessageProperties().getConsumerTag()
,message.getMessageProperties().getConsumerQueue(),channel.getChannelNumber());
}
//批量确认
try {
channel.basicAck(maxDeliveryTag,true);
} catch (IOException e) {
//确认失败 处理message数据需要回滚
e.printStackTrace();
}
});
return listenerContainer;
}
最终发现设置2个并发下,存在2个Thread消费,并且2个Channel通道,其中deliverTag为每一条消费者中消息对应的id,因为是2个consumer,所以50(deliverTag)*2(2个consumer)*10(每个批次10条数据)=1000总数
2:使用@
RabbitListener注解的方式该方式的本质是springboot通过扫描@
RabbitListener读取注解中设定的参数,去构建SimpleMessageListenerContainer,并进行监听,实际上1中繁琐的代码由spring框架来完成,极大的减少了业务的开发工作量,这种方式也是在实际工作中使用。
这里分析下springboot是如何做到:
点开@EnableRabbit
点进RabbitBootstrapConfiguration发现
这里核心说的是关于RabbitListenerAnnotationBeanPostProcessor,该类就是真正进行注解扫描并初始化的
在processListener中
实际使用:
/**
* 使用注解方式进行消费
* @author fangyaun
*/
@Component
@Slf4j
public class SimpleRabbitmqListenerAnnotation {
/**
*
* @param message
*/
@RabbitListener(containerFactory = "rabbitListenerContainerFactory",ackMode = "AUTO",
queues = {CommonConstant.queue_direct1})
public void handleMessage(String message){
Thread thread=Thread.currentThread();
log.info("==========auto-ack=======>message:{} ThreadId is:{} "
,message,thread.getId());
}
/**
* 手动确认
*
* @param message 需要处理消息
* @param deliveryTag :使用@Header接口获取messageProperties中的DELIVERY_TAG属性。
*
*/
@RabbitListener(containerFactory = "rabbitListenerContainerFactory",ackMode = "MANUAL",
queues = {CommonConstant.queue_direct2})
public void handleMessage2(@Payload String message, Channel channel, @Header(AmqpHeaders.DELIVERY_TAG) long deliveryTag){
Thread thread=Thread.currentThread();
log.info("==========ack=======>message:{} ThreadId is:{} channel:{}"
,message,thread.getId(),channel.getChannelNumber());
try {
// channel.basicNack(deliveryTag,false,true);
channel.basicAck(deliveryTag,false);
} catch (IOException e) {
e.printStackTrace();
}
}
/**
*
* @param message
* @param channel
* @param deliveryTag
*/
@RabbitListener(containerFactory = "rabbitListenerContainerFactory",ackMode = "MANUAL",
queues = {CommonConstant.queue_direct2})
public void handleMessage3(@Payload String message, Channel channel, @Header(AmqpHeaders.DELIVERY_TAG) long deliveryTag){
Thread thread=Thread.currentThread();
log.info("==========nack=======>message:{} ThreadId is:{} channel:{}"
,message,thread.getId(),channel.getChannelNumber());
try {
channel.basicNack(deliveryTag,false,true);
// channel.basicAck(deliveryTag,false);
} catch (IOException e) {
e.printStackTrace();
}
}
}
最终结果:
自动ack:
手动ack:
nack:
验证SimpleMessageListenerContainer上述的并发特性:即多个queue多个消费者下会公用一个channel。这里测试代码为2个queue,2个consumer.
@RabbitListener(containerFactory = "rabbitListenerContainerFactory",ackMode = "MANUAL",
queues = {CommonConstant.queue_direct2,CommonConstant.queue_direct1})
public void handleMessage2(@Payload String message, Channel channel,@Header(AmqpHeaders.CONSUMER_QUEUE) String consumer_queue, @Header(AmqpHeaders.DELIVERY_TAG) long deliveryTag){
Thread thread=Thread.currentThread();
log.info("==========ack=======>message:{} ThreadId is:{} channel:{} consumer_queue:{}"
,message,thread.getId(),channel.getChannelNumber(),consumer_queue);
try {
channel.basicAck(deliveryTag,false);
} catch (IOException e) {
e.printStackTrace();
}
}
最终的结果:
这也是为何上述会说到 一个 channel 要游走于多个 consumer 当中,这无疑增加了系统在上下文切换中的开销。并且一旦某个 channel 关闭或重启,意味着每个队列 queue 中使用当前 channel 的 consumer 都会受到影响
DirectMessageListenerContainer: 与SMLC不同DMLC的并发性基于配置的队列和consumersPerQueue,
每个队列的每个consumer都使用单独的channel(一个consumer消费线程会监控多个channel通道),并发性由Rabbit客户端库控制。默认情况下(基于当前版本),它使用DEFAULT_NUM_THREADS=Runtime.getRuntime().availableProcessors()(有效内核数) * 2
线程池。可以通过配置taskExecutor
提供所需的最大并发量。
DMLC的使用方式和SMLC大致相同,这里不主要叙说,这里只测试它的并发原理
@Bean
public MessageListenerContainer directChannelMessageListenerContainer(){
DirectMessageListenerContainer listenerContainer = directRabbitListenerContainerFactory.createListenerContainer();
//设置被监控的queue
listenerContainer.setQueueNames(CommonConstant.queue_direct1,CommonConstant.queue_direct2);
//手动确认
listenerContainer.setAcknowledgeMode(AcknowledgeMode.MANUAL);
//为每个队列添加多个消费者 增加并行度
listenerContainer.setConsumersPerQueue(2);
// listenerContainer.setTaskExecutor();
//设置消费者唯一标记 基于queue设置
listenerContainer.setConsumerTagStrategy(queue -> {
return queue+"_"+ UUID.randomUUID().toString();
});
//包含channel
listenerContainer.setMessageListener((ChannelAwareMessageListener) (message, channel) -> {
Thread thread=Thread.currentThread();
//获取数据
long maxDeliveryTag = message.getMessageProperties().getDeliveryTag();
//获取数据
byte[] body = message.getBody();
String str = new String(body);
log.info("deliveryTag:{} message:{} ThreadId :{} channel:{} ConsumerTag:{} Queue:{} "
,maxDeliveryTag,str,channel.getChannelNumber(),thread.getId(),message.getMessageProperties().getConsumerTag()
,message.getMessageProperties().getConsumerQueue());
//批量确认
try {
channel.basicAck(maxDeliveryTag,false);
} catch (IOException e) {
//确认失败 处理message数据需要回滚
e.printStackTrace();
}
});
return listenerContainer;
}
从测试代码以及最终结果可以验证出,每一个队列中的每个consumer(ThreadId)对应多个个channel,一个consumer消费线程会监控多个channel通道,所以在相同情况下DMLC的并发处理能力要高于SMLC,并且避免在RabbitMQ客户端线程和使用者线程之间进行上下文切换。
死信队列(Dead-Letter-Exchange) ,从它的命令也可以看出它适用于存储已经死亡的消息的队列,那么什么情况下消息代表了死亡呢。
从上述描述可以看出,基本上死信队列的产生都与消费者有关,所以也在此进行描述。死信队列也是一个正常的队列,只不过它存储的是从其它队列中转发过来的信息,所以我们仍然需要按照创建队列的方式创建它,唯一不同在于,我们需要通过x-dead-letter-exchange与x-dead-letter-routing-key两个header将需要监控的队列与死信队列进行绑定,如下所示:
@Bean
public Queue directQueue1(){
Map arguments = new HashMap<>();
//声明当前死信的 Exchange
arguments.put("x-dead-letter-exchange",exchange_deadLetter_direct);
arguments.put("x-dead-letter-routing-key",routingKey_deadLetter);
return new Queue(queue_direct1,true,false,false,arguments);
}
/**
* 构建死
* @return
*/
@Bean
public Queue deadLetterQueue(){
return new Queue(queue_deadLetter,true,false,false);
}
//绑定死信队列
@Bean
public Binding deadLetterBinding(){
return BindingBuilder.bind(deadLetterQueue()).to(deadLetterExchange()).with(routingKey_deadLetter).noargs();
}
@Bean
public Exchange deadLetterExchange(){
return new DirectExchange(exchange_deadLetter_direct);
}
测试代码:
@RabbitListener(containerFactory = "rabbitListenerContainerFactory",ackMode = "MANUAL",
queues = {CommonConstant.queue_direct2,CommonConstant.queue_direct1})
public void handleMessage2(@Payload String message, Channel channel,@Header(AmqpHeaders.CONSUMER_QUEUE) String consumer_queue, @Header(AmqpHeaders.DELIVERY_TAG) long deliveryTag) throws IOException {
Thread thread=Thread.currentThread();
log.info("==========ack=======>message:{} ThreadId is:{} channel:{} consumer_queue:{}"
,message,thread.getId(),channel.getChannelNumber(),consumer_queue);
channel.basicReject(deliveryTag,false);
}
最终的结果: