本文主要参考了Understanding AMQP, the protocol used by RabbitMQ
要学习rabbitmq,就要先学习amqp协议,amqp全称Advanced Message Queuing Protocol (AMQP),rabbitmq实现了amqp,当然ActiveMQ也实现了amqp。amqp中有几个重要的概念,producer(publisher)、exchange、queue、consumer
谈及MQ时,我们还会经常谈及RocketMQ,可参考Why choose RocketMQ
producer发送message到指定的exchange,exchange根据message header中的routing-key与队列的binding-key决定绑定到哪个队列;那么consumer怎么获取消息呢?queue push或者consumer pull,这取决于配置。
一个消息由header、properties、data3部分组成,header是amq规范规定的类似http规范中的header,比如后边会讲到的routing-key,properties是业务自定义的,data是二进制数据;
通常我们把message分配到queue的过程叫binding,那消息结构跟binding有什么关系呢?exchange拿着message header中的routing-key与queue中的binding-key(每个queue都有一个binding-key)做匹配。一般的consumer创建queue时,会同时创建exchange,并将queue关联到exchange,producer发送到指定exchange,从而实现消息流转;
exchange常见的有以下几种类型,
*
、#
会当作普通文本处理;*
、#
当作通配符处理,*
只能匹配一个word,#
匹配零个或多个点号分隔的word;比如现有一个message,它的routing key是NYSE.TECH.MSFT,exchange是topic类型,那么匹配queue的binding key结果
以上包含了绝大多数情况。实际上,一个queue可以有多个consumer,一个exchange可以有多个queue,一个queue可以有多个exchange。
至于这个过程是blocking还是non-blockding,就取决于client library,无论是哪种都可以做到。AmqpTemplate#sendAndReceive就是对应的这种模式
一个消息会被多个消费者消费,也叫广播模式,在activemq中叫topic。
一个message只会被多个consumer中的一个消费,在activemq中叫p2p(点对点)。
参考消息队列(为什么要用消息队列,常见消息队列对比,JMS和AMQP谁更好用?),简单来说JMS是java的规范,跟语言是相关的,而amqp是协议,与语言无关,jms提供了点对点、发布/订阅两种模式,amq也提供了这两种模式,并在此基础上,对路由提供了多种选择。
springboot可以很方便的集成rabbitmq,引入spring-boot-starter-amqp
,然后在配置文件中添加配置
spring:
rabbitmq:
host: "localhost"
port: 5672
username: "admin"
password: "secret"
发送消息
@Component
public class MyBean {
private final AmqpAdmin amqpAdmin;
private final AmqpTemplate amqpTemplate;
public MyBean(AmqpAdmin amqpAdmin, AmqpTemplate amqpTemplate) {
this.amqpAdmin = amqpAdmin;
this.amqpTemplate = amqpTemplate;
}
// ...
}
默认的template禁用了重试,可以启用重试,注意这是发送(AmqpTemplate )重试;
rabbitmq:
template:
retry:
enabled: true
initial-interval: "2s"
接收消息
@Component
public class MyBean {
@RabbitListener(queues = "someQueue")
public void processMessage(String content) {
// ...
}
}
注意,消费者有重试和重入队列两个概念;默认的rabbitmq.listener.simple.retry.enabled
=false,重试被禁用;因为rabbitmq.listener.simple.default-requeue-rejected
=true,如果listener抛出异常,那么会重入队列,从而会无限重试;如果不想要无限重试,有2种方法解决,
rabbitmq:
listener:
simple:
acknowledge-mode: auto # 超过重试次数后,自动确认,消息会从队列中删除
retry:
# 打开重试,设置最大重试次数
enabled: true
max-attempts: 3
max-interval: 6000
initial-interval: 2000
# 如果消费失败,不再入队列
default-requeue-rejected: false
发送可以重试,Listener也可以重试,注意区分;
确认机制指的是,当消费者在处理消息时,如果出错或者网络出错,broker应不应该删除消息的问题。AMQP 0-9-1规范提供了机制供消费者控制,
消费者消费时可能会出现处理成功、处理失败、多条处理失败的情况,相应的就有ack、reject、nack(rabbitmq扩展了AMQP 0-9-1规范,自创的)三条命令,跟spring源码中可能不太一样,
package org.springframework.amqp.core;
public enum AcknowledgeMode {
/**
* No acks - {@code autoAck=true} in {@code Channel.basicConsume()}.
*/
//自动确认,消费者收到消息后,不管消费成功与否,从broker中删除
NONE,
/**
* Manual acks - user must ack/nack via a channel aware listener.
*/
//手动确认,消费者明确返回确认后,才会从broker中删除
MANUAL,
/**
* Auto - the container will issue the ack/nack based on whether
* the listener returns normally, or throws an exception.
* Do not confuse with RabbitMQ {@code autoAck} which is
* represented by {@link #NONE} here.
*/
//手动确认,java实现时自创的,container根据listener执行结果,成功时返回ack、失败时返回nack
AUTO;
}
参考Message acknowledgment、AMQP 0-9-1 Message Acknowledgements
因为网络的不确定性,可能在publisher发送到broker时,丢了消息,通常的解决方法是publisher发送前写入数据库,如果真的mq server丢失了,再手动补偿;还可能broker在处理message时出错了,导致了消息丢失;
如果mq server重启了,那么队列及消息都丢失了,可以配置队列及消息是持久化的,这样重启也不会丢失队列和消息。但这不是绝对的不会丢失,因为mq server收到message会暂时放入cache在合适的时机再写入磁盘,如果真的需要绝对可靠可以考虑publisher confirm;
默认的,broker发送message给了consumer后,就会立刻删掉message。如果处理这个message要花费一段时间,还未处理完时,consumer终止了,那么这个message就丢失了,还有其他已经收到还未来得及处理的message;怎么解决这种问题呢?consumer 确认机制。处理完message后,发送ack,broker收到ack再删除message,如果一直没收到ack(默认是30分钟),则会放入messages_unacknowledged 队列。
rabbitmq中,实现点对点模式还是广播模式的关键在于exchange的类型,