1、概念
基于Erlang编写(Erlang语言天生具备分布式特性,通过同步Erlang集群各节点的magic cookie来实现),因此天然支持Clustering。这使得RabbitMQ本身不需要像ActiveMQ、Kafka那样通过ZooKeeper分别来实现HA(High Availability高可用)方案和保存集群的元数据,是属于AMQP( 高级消息队列协议 ) 标准的一个实现。是应用层协议的一个开放标准。
AMQP即Advanced Message Queuing Protocol,高级消息队列协议,是面向消息中间件设计的应用层协议的一个开放标准AMQP是一个应用层协议,可以把它类比为HTTP协议,底层都是基于TCP/IP协议的,只不过它是针对消息中间件设定的,它的设计都是为了实现在生产者和消费者中间传递消息。默认端口为5672
2、组成结构:
在一个 Virtual Host 中,可以有多个不同名称的Exchange ,而一个 Exchange 可以与多个 Channel 进行绑定,同时,一个 Queue 也可以和多个 Channel 进行绑定
- producer:消息生产者,就是投递消息的程序。
- ConnectionFactory 连接管理器:Connection工厂,负责创建和管理Connection的。
- Connection :生产者/消费者与broker建立的TCP 连接
无论是生产者还是消费者,都需要和 Broker 建立连接,这个连接就是Connection,是一条 TCP 连接 ,一个生产者或一个消费者与 Broker 之间只有一个Connection,即只有一条TCP连接。
- channel:消息通道,在客户端的每个连接里,可建立多个channel,每个channel代表一个会话任务。
信道是生产消费者与rabbit通信的渠道,生产者publish或是消费者subscribe一个队列都是通过信道来通信的。信道是建立在TCP连接上的虚拟连接,rabbitmq在一条TCP上建立成百上千个信道来达到多个线程处理,这个TCP被多个线程共享(不是每个线程各自开辟一个TCP,实现共用TCP、减少TCP创建和销毁的开销),每个线程对应一个信道,信道在rabbit都有唯一的ID,保证了信道私有性,对应上唯一的线程使用。
类似概念:TCP是电缆,信道就是里面的光纤,每个光纤都是独立的,互不影响
-
Broker:简单来说就是消息队列服务器实体。部署RabbitMQ的机器称为节点。
-
Binding:绑定,它的作用就是把exchange和queue按照路由规则绑定起来。
-
Routing Key:路由键, 生产者将消息发送给交换器的时候,会指定一个RoutingKey,用来指定这个消息的路由规则,这个RoutingKey需要与交换器类型和绑定键(BindingKey)联合使用才能最终生效,exchange根据这个路由键进行消息投递。
Routing key是消息头的属性,生产者将消息发送到交换机时,会在消息头上携带一个 key,这个 key就是routing key,来指定这个消息的路由规则。等同于数据库中的where条件一样
-
Exchange:消息交换机,它指定消息按什么规则,路由到哪个队列。
-
vhost:虚拟主机,一个broker里可以开设多个vhost,用作不同用户的权限分离。默认vhost:“/”
可以理解为虚拟broker,即mini-RabbitMQ server,其内部均含有独立的queue、bind、exchange等,最重要的是拥有独立的权限系统,可以做到vhost范围内的用户控制,当在RabbitMQ中创建一个用户时,用户通常会被指派给至少一个vhost,并且只能访问被指派vhost内的队列、交换器和绑定
-
Queue:消息队列载体,每个消息都会被投入到一个或多个队列。
-
consumer:消息消费者,就是接受消息的程序。
2.1 Exchange分类
- direct 路由模式 (1:1)
消息的 Routing key 与Binding key完全匹配,一个消息对应一个队列,也支持不同的队列可以用相同的Binding key与同一交换机绑定
- fanout 广播模式(1:N)
可以把一个消息并行发布到多个队列上去,简单的说就是,当多个队列绑定到fanout的交换机,那么交换器一次性拷贝多个消息分别发送到绑定的队列上,每个队列有这个消息的副本
- topic 主题模式(N:1)
模糊匹配路由到队列,多个交换机可以路由消息到同一个队列。根据模糊匹配,比如一个队列的routingkey为*.test,那么凡是到达交换器的消息中的routing key后缀.test都被路由到这个队列上
- headers 参数模式
不常用,headers交换机是通过Headers头部来将消息映射到队列的,Headers头部携带一个Hash结构,Hash结构中要求携带一个键"x-match",这个键的Value可以是any或者all,这代表消息携带的Hash是需要全部匹配(all),还是仅匹配一个键(any)就可以了。相比直连交换机,headers交换机的优势是匹配的规则不被限定为字符串String类型
- default Exchange 默认交换机的名字是空字符串。
发送消息时不指定交换机的名称,则会发到"默认交换机"上。默认的Exchange不进行Binding操作,任何发送到该Exchange的消息都会被转发到"Queue名字和Routing key相同的队列"中;
如果vhost中不存在和Routing key同名的队列,则该消息会被抛弃。
2.2 broker的分类
-
RAM Node 只会将元数据存放在RAM
-
Disk node 会将元数据持久化到磁盘。
RabbitMQ 要求集群中至少有一个 disk node , 其它的都可以是 RAM node。
这样出现故障时才能恢复数据,节点加入或退出集群一定至少要通知集群中的一个 disk node
2.3 元数据
元数据可以持久化在 RAM 或 Disk,元数据包含以下内容:
-
queue元数据:queue名称、属性
-
exchange:exchange名称、属性
-
binding元数据:路由信息
-
vhost元数据:vhost内部的命名空间、安全属性数据等
元数据保存在每个节点上。当创建一个新的交换器或队列时,RabbitMQ会把元数据同步到所有节点上
如果集群中的唯一一个磁盘节点不可用了,因元数据每个节点都同步保存,集群依然可以继续路由消息,但无法做以下操作
创建队列、交换器、绑定
添加用户
更改权限
添加、删除集群节点
2.4 Queue结构
队列组成
- AMQPQueue:负责AMQP协议相关的消息处理,即接收生产者发布的消息、向消费者投递消息、处理消息confirm、acknowledge等等
- BackingQueue:提供AMQQueue调用的接口,完成消息的存储和可能的持久化工作
队列生命周期
BackingQueue由Q1,Q2,Delta,Q3,Q4五个子队列构成,以持久化消息为例,在Backing中,消息的生命周期有四个状态:
- Alpha:消息的内容和消息索引都在RAM中。(Q1,Q4)
- Beta:消息的内容保存在Disk上,消息索引保存在RAM中。(Q2,Q3)
- Gamma:消息的内容保存在Disk上,消息索引在DISK和RAM上都有。(Q2,Q3)
- Delta:消息内容和索引都在Disk上。(Delta)
从Q1到Q4,消息实际经历了一个RAM->DISK->RAM这样的过程。队列负载很高时,通过将部分消息放到磁盘上来节省内存空间,当负载降低时,消息又从磁盘回到内存中,让整个队列有很好的弹性。但消息在Disk和RAM之间转换,者实际会降低RabbitMQ的处理性能。
因此触发消息流动的主要因素是:1.消息被消费;2.内存不足
RabbitMQ会根据消息的传输速度来计算当前内存中允许保存的最大消息数量(Traget_RAM_Count),
当:内存中保存的消息数量+等待ACK的消息数量>Target_RAM_Count时,RabbitMQ才会把消息写到磁盘上,所以说虽然理论上消息会按照Q1->Q2->Delta->Q3->Q4的顺序流动,但是并不是每条消息都会经历所有的子队列以及对应的生命周期
3、生存时间TTL
全称为 Time to Live ,即生存时间,TTL 这一概念是作用于消息和消息队列上,即为消息或者为消息队列规定了一个生存时间。
1) 设置消息TTL
有2种方法设置消息TTL, 如果两种方法一起使用,则消息的 TTL 以两者之间较小的那个数值为准。
- 第一种方法是通过队列属性设置,队列中所有消息都有相同的过期时间。
这种设置方法下,一旦消息过期,就会从队列中抹去,因为这个队列中所有的消息都是相同的过期时间,队列中若存在已过期的消息必定在队列头部。
Map<String, Object> args = new HashMap<String, Object>() ; args.put( "x-message-ttl" , 6000); channel.queueDeclare("myqueue" , durable, exclusive, autoDelete, args) ;
- 第二种方法是对消息本身进行单独设置,每条消息的 TTL 可以不同。
这种设置方式下,消息的生存时间超过了 TTL所规定的消息生存时间,那么,这条消息会立即失效,并且不会被任何消费者消费,且会变成一种死信,并最终会被 RabbitMQ 放入死信队列中
AMQP.BasicProperties.Builder builder = new AMQP.BasicProperties.Builder(); builder.deliveryMode(2);//持久化消息 builder.expiration("6000");//设置TTL AMQP.BasicProperties properties = builder.build(); channel.basicPublish(exchangeName, routingKey, mandatory, properties, "myTTLMessage".getBytes());
这里为什么是 最终
这条消息即使过期失效,但不会马上从队列中抹去,因为每条消息是否过期时在即将投递到消费者之前判定的,等到此消息即将被消费时判定是否过期,否则需要扫描整个队列。
死信队列中的消息顺序仍是原队列中的顺序
即使一个消息比在同一队列中的其他消息提前过期,提前过期的也不会优先进入死信队列,它们还是按照入库的顺序让消费者消费。只有当过期的消息到了队列的顶端(队首)即将被消费时,才会被真正的丢弃或者进入死信队列。
2) 设置队列TTL
消息队列的生存时间超过了 TTL所规定的消息队列的生存时间,那么消息队列会立即失效,且该消息队列中的消息也会随着消息队列的失效而失效,相当于清空队列,队列中的所有消息都成为死信,不管消息有没有过期
Map<String, Object> args = new HashMap<String, Object>() ; args.put( "x-expires" , 1800000); channel.queueDeclare("myqueue" , durable, exclusive, autoDelete, args) ;
4、死信队列
在 RabbitMQ 中充当主角的就是消息,在不同场景下,消息会有不同地表现。死信就是消息在特定场景下的一种表现形式,消息在这几种场景中时,被称为死信
- 消息被拒绝访问,即 RabbitMQ Server 返回 nack 的信号时
- 消息的 TTL 过期时
- 消息队列达到最大长度,消息不能入队时
- 对于无序消息集群消费下的重试消费,默认允许每条消息最多重试 16 次,如果消息重试 16 次后仍然失败,消息将被投递至死信队列
死信队列就是用于储存死信的消息队列,在死信队列中,有且只有死信构成,不会存在其余类型的消息,这就是死信队列,一条消息进入死信队列,意味着不做特殊处理下消费者无法正常消费该消息
死信队列在 RabbitMQ中并不会单独存在,往往死信队列都会绑定这一个普通的消息队列,当所绑定的消息队列中,有消息变成死信了,那么这个消息就会重新被交换机路由到指定的死信队列中去,我们可以通过对这个死信队列进行监听,从而手动的去对这一消息进行补偿。(可实现延时队列)
5、重回队列与重试队列
重回队列
即在 RabbitMQ 诸多的消息队列中,专门用来存储那些没有被消费者成功消费掉的消息的队列,这种队列就被称为重回队列。实际应用中,一般都会关闭自动重回队列(默认false关闭)。
当消息队列中的消息没有被消费者成功消费后,首先 RabbitMQ Server 会返回给消费端一个 nack 的信号;其次,该条没有被消费的消息会被放入重回队列中;最后,RabbitMQ Server 会从这个重回队列中获取该条消息,并重新发送到 RabbitMQ Server 中等待消费
重回队列的条件:
- 关闭自动确认ack(手动进行ack或nack),
- 收到的确认消息是nack,
- requeue设置为true,表示消息需重回队列进行重新投递
Connection connection = connectionFactory.newConnection();
Channel channel = connection.createChanel();
DefaultConsumer defaultConsumer = new DefaultCnsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
if (properties.getHeaders().get("type") != "utf-8") {
// multiple 是否批量 requeue是否启用重回队列
channel.basicNack(envelope.getDeliveryTag(), multiple, requeue)
}
}
};
//手工签收 必须要关闭 autoAck = false
channel.basicConsume(queueName, autoAck, defaultConsumer);
可能导致的问题:
- 阻塞队列:若消息体本身有问题,即使再次重新消费仍不能消费成功,则会一直循环一直存在
重试队列
重试队列其实可以看成是一种回退队列,具体指消费端消费消息失败时,为防止消息无故丢失而重新将消息回滚到Broker中。与回退队列不同的是重试队列一般分成多个重试等级,每个重试等级一般也会设置重新投递延时,重试次数越多投递延时就越大。
举个例子:消息第一次消费失败入重试队列Q1,Q1的重新投递延迟为5s,在5s过后重新投递该消息;如果消息再次消费失败则入重试队列Q2,Q2的重新投递延迟为10s,在10s过后再次投递该消息。以此类推,重试越多次重新投递的时间就越久,为此需要设置一个上限,超过投递次数就入死信队列。
重试队列的名称是在原队列的名称前加上%RETRY%,死信队列的名称是在原队列名称前加%DLQ%
(队列自动创建,重试队列是针对消费组,而不是针对每个Topic设置的)
consumer.registerMessageListener(
new MessageListenerConcurrently() {
@Override
public ConsumeConcurrentlyStatus consumeMessage(
List<MessageExt> msgs,
ConsumeConcurrentlyContext context) {
try {
// 消息处理逻辑
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
} catch (Exception e) {
// 比如因为数据库宕机了,没法对消息完成处理
// 返回重试的消费状态
return ConsumeConcurrentlyStatus.RECONSUME_LATER;
}
}
}
}
);
优先级
使用nack将消息重新推回队列,这条消息被视为一条新的消息,MQ认为新消息的消费失败次数是0,消费失败再退回再消费失败,因此表现为重试机制不生效。
6、持久化
RabbitMQ的持久化分为三个部分:交换器的持久化、队列的持久化和消息的持久化。
交换器和队列的持久化是通过在声明时是将 durable 参数置为 true 实现的,是持久化元数据
消息的持久化是通过将消息的投递模式 (BasicProperties 中的 deliveryMode 属性)设置为 2 即可实现消息的持久化,持久化的是消息内容
RabbitMQ在两种情况下会将消息写入磁盘:
- 消息本身在publish的时候就要求消息写入磁盘;
- 内存紧张,需要将部分内存中的消息转移到磁盘;
非持久化消息也会写磁盘? 是的,在内存不够的时候将消息写入磁盘(重启后不保证还存在)
刷盘时机:
- 写入文件前会有一个Buffer,大小为1M(1048576),数据在写入文件时,首先会写入到这个Buffer,如果Buffer已满,则会将Buffer写入到文件(未必刷到磁盘);
- 有个固定的刷盘时间:25ms,也就是不管Buffer满不满,每隔25ms,Buffer里的数据及未刷新到磁盘的文件内容必定会刷到磁盘;
- 每次消息写入后,如果没有后续写入请求,则会直接将已写入的消息刷到磁盘:使用Erlang的receive x after 0来实现,只要进程的信箱里没有消息,则产生一个timeout消息,而timeout会触发刷盘操作。
文件何时删除
-
当所有文件中的垃圾消息(已经被删除的消息)比例大于阈值(GARBAGE_FRACTION = 0.5)时,会触发文件合并操作(至少有三个文件存在的情况下),以提高磁盘利用率。
-
publish消息时写入内容,ack消息时删除内容(更新该文件的有用数据大小),当一个文件的有用数据等于0时,删除该文件。
7、工作模式
8、集群模式
9、问题与应用
参考文档:
https://www.cnblogs.com/zhaobinrui/p/14618235.html
https://blog.youkuaiyun.com/w15558056319/article/details/123373383
https://www.5axxw.com/wiki/topic/lqjn40
https://www.rabbitmq.com/documentation.html