Rabbitmq使用姿势

前言:

RabbitMQ是一个由Erlang开发的AMQP(AdvancedMessage Queue )的开源实现,用于在分布式系统中存储转发消息。

AMQP,即Advanced Message Queuing Protocol,一个提供统一消息服务的应用层标准高级消息队列协议,是应用层协议的一个开放标准,为面向消息的中间件设计。基于此协议的客户端与消息中间件可传递消息,并不受客户端/中间件不同产品,不同的开发语言等条件的限制。它主要特征是面向消息、队列、路由(包括点对点和发布/订阅)、可靠性、安全。

简单的介绍一下AMQP的协议栈,AMQP协议本身包含三层,如下:

Rabbitmq使用姿势_第1张图片

Model Layer,位于协议最高层,主要定义了一些供客户端调用的命令,客户端可以通过这些命令实现自己的业务逻辑,例如,客户端可以通过queue declare声明一个队列,利用consume命令获取队列的消息。

Session Layer,主要负责将客户端命令发送给服务器,在将服务器端的应答返回给客户端,主要为客户端与服务器之间通信提供可靠性、同步机制和错误处理。

Transport Layer,主要传输二进制数据流,提供帧的处理、信道复用、错误检测和数据表示。

这种分层架构,可替换各层实现而不影响与其它层的交互。AMQP定义了合适的服务器端域模型,用于规范服务器的行为(AMQP服务器端可称为消息代理中间人)。在这里Model层决定这些基本域模型所产生的行为,这种行为在AMQP中用command表示。Session层定义客户端与broker之间的通信(通信双方可互称做搭档),为command的可靠传输提供保障。Transport层专注于数据传送,并与Session保持交互,接受上层的数据,组装成二进制流,传送到接受者后再解析数据,交付给Session层。Session层需要Transport层完成网络异常情况的汇报,顺序传送command等工作。

AMQP中的一些概念。

Broker(Server):接受客户端连接,实现AMQP消息队列和路由功能的进程,作为消息提供者和消息消费者之间的中间人。

Virtual Host:其实是一个虚拟概念,类似于权限控制组,一个Virtual Host里面可以有若干个Exchange和Queue,但是权限控制的最小粒度是Virtual Host。

Exchange:接受生产者发送的消息,并根据Binding规则将消息路由给服务器中的队列。ExchangeType决定了Exchange路由消息的行为,例如,在RabbitMQ中,ExchangeType有direct、Fanout和Topic三种,不同类型的Exchange路由的行为是不一样的。

Message Queue:消息队列,用于存储还未被消费者消费的消息。

Message:由Header和Body组成,Header是由生产者添加的各种属性的集合,包括Message是否被持久化、由哪个Message Queue接受、优先级是多少等。而Body是真正需要传输的APP数据。

Binding:Binding联系了Exchange与MessageQueue。Exchange在与多个MessageQueue发生Binding后会生成一张路由表,路由表中存储着Message Queue所需消息的限制条件即Binding Key。当Exchange收到Message时会解析其Header得到Routing Key,Exchange根据Routing Key与ExchangeType将Message路由到MessageQueue。Binding Key由Consumer在Binding Exchange与MessageQueue时指定,而Routing Key由Producer发送Message时指定,两者的匹配方式由ExchangeType决定。

Connection:连接,对于RabbitMQ而言,其实就是一个位于客户端和Broker之间的TCP连接。

Channel:信道,仅仅创建了客户端到Broker之间的连接后,客户端还是不能发送消息的。AMQP协议规定只有通过Channel才能执行AMQP的命令,所以需要为每一个Connection创建Channel。一个Connection可以包含多个Channel。之所以需要Channel,是因为TCP连接的建立和释放都是十分昂贵的,如果一个客户端每一个线程都需要与Broker交互,如果每一个线程都建立一个TCP连接,暂且不考虑TCP连接是否浪费,就算操作系统也无法承受每秒建立如此多的TCP连接。RabbitMQ建议客户端线程之间不要共用Channel,至少要保证共用Channel的线程发送消息必须是串行的,但是建议尽量共用Connection。

Command:AMQP的命令,客户端通过Command完成与AMQP服务器的交互来实现自身的逻辑

一、Rabbitmq的启动运行(windows10系统)

2019-08-11 09:23:38.475 [info] <0.33.0> Application lager started on node 'rabbit@DESKTOP-MFL4IGI'
2019-08-11 09:23:38.491 [info] <0.5.0> Log file opened with Lager
2019-08-11 09:23:40.083 [info] <0.33.0> Application xmerl started on node 'rabbit@DESKTOP-MFL4IGI'
2019-08-11 09:23:40.208 [info] <0.33.0> Application inets started on node 'rabbit@DESKTOP-MFL4IGI'
2019-08-11 09:23:40.208 [info] <0.33.0> Application recon started on node 'rabbit@DESKTOP-MFL4IGI'
2019-08-11 09:23:40.208 [info] <0.33.0> Application jsx started on node 'rabbit@DESKTOP-MFL4IGI'
2019-08-11 09:23:40.223 [info] <0.33.0> Application mnesia started on node 'rabbit@DESKTOP-MFL4IGI'
2019-08-11 09:23:40.286 [info] <0.33.0> Application os_mon started on node 'rabbit@DESKTOP-MFL4IGI'
2019-08-11 09:23:40.286 [info] <0.33.0> Application asn1 started on node 'rabbit@DESKTOP-MFL4IGI'
2019-08-11 09:23:40.286 [info] <0.33.0> Application crypto started on node 'rabbit@DESKTOP-MFL4IGI'
2019-08-11 09:23:40.286 [info] <0.33.0> Application cowlib started on node 'rabbit@DESKTOP-MFL4IGI'
2019-08-11 09:23:40.286 [info] <0.33.0> Application public_key started on node 'rabbit@DESKTOP-MFL4IGI'
2019-08-11 09:23:40.364 [info] <0.33.0> Application ssl started on node 'rabbit@DESKTOP-MFL4IGI'
2019-08-11 09:23:40.379 [info] <0.33.0> Application ranch started on node 'rabbit@DESKTOP-MFL4IGI'
2019-08-11 09:23:40.379 [info] <0.33.0> Application cowboy started on node 'rabbit@DESKTOP-MFL4IGI'
2019-08-11 09:23:40.379 [info] <0.33.0> Application ranch_proxy_protocol started on node 'rabbit@DESKTOP-MFL4IGI'
2019-08-11 09:23:40.379 [info] <0.33.0> Application rabbit_common started on node 'rabbit@DESKTOP-MFL4IGI'
2019-08-11 09:23:40.474 [info] <0.259.0>
 Starting RabbitMQ 3.7.4 on Erlang 20.3
 Copyright (C) 2007-2018 Pivotal Software, Inc.
 Licensed under the MPL.  See http://www.rabbitmq.com/
2019-08-11 09:23:40.474 [info] <0.259.0>
 node           : rabbit@DESKTOP-MFL4IGI
 home dir       : C:\WINDOWS\system32\config\systemprofile
 config file(s) : e:/RabbitMQ/rabbitmq_server-3.7.4/etc/rabbitmq.config
 cookie hash    : 8iOMTukzSRyT0kv4kWjyQQ==
 log(s)         : C:/Users/zht/AppData/Roaming/RabbitMQ/log/RABBIT~1.LOG
                : C:/Users/zht/AppData/Roaming/RabbitMQ/log/rabbit@DESKTOP-MFL4IGI_upgrade.log
 database dir   : c:/Users/zht/AppData/Roaming/RabbitMQ/db/RABBIT~1

2019-08-11 09:23:42.692 [info] <0.267.0> Memory high watermark set to 3241 MiB (3398439731 bytes) of 8102 MiB (8496099328 bytes) total
2019-08-11 09:23:42.723 [info] <0.269.0> Enabling free disk space monitoring
 

Rabbitmq的启动是否成功取决于日志中打印的此配置文件信息是否正确,如果配置文件不存在或者是配置内容格式不正确将无法正常启动rabbitmq,配置文件以及数据文件的位置通过日志中可以了解到如下信息,参阅环境变量默认值

config file(s) : e:/RabbitMQ/rabbitmq_server-3.7.4/etc/rabbitmq.config

 database dir   : c:/Users/zht/AppData/Roaming/RabbitMQ/db/${RABBIT节点名称}-mnesia

二、Rabbitmq的Vhost的创建以及子节点结构组成

Vhost(虚拟主机)提供逻辑分组和资源分离。物理资源的分离不是虚拟主机的目标,应该被视为实现细节,比如权限最小管控就是Vhost. 客户端连接到某个Vhost时,身份验证成功并且提供的用户名被授予该vhost的权限,则建立连接。才能操作该vhost下的交换、队列、绑定等操作。换句话说,如果我们想要实现两个Vhost下的队列互连消息中转,则需要分别连接上两个对应的vhost才行,可以简单的认为是一种多源情况。

默认情况下,所有的vhost命名、队列名称、消息体等信息都是存在如下目录下相应的子文件夹中,当然也是可以通过RABBITMQ_MNESIA_DIR环境变量进行设置

C:\Users\zht\AppData\Roaming\RabbitMQ\db\rabbit@DESKTOP-MFL4IGI-mnesia\msg_stores\vhosts

每当我们新建一个vhost,则此目录下就会多一个文件夹以及对应的一些子文件目录。

对应的日志如下:

2019-08-11 10:29:12.421 [info] <0.643.0> Adding vhost 'test'
2019-08-11 10:29:12.436 [info] <0.654.0> Making sure data directory 'c:/Users/zht/AppData/Roaming/RabbitMQ/db/RABBIT~1/msg_stores/vhosts/KDISMNX5MOYU6Q6PZT8TQDPY' for vhost 'test' exists
2019-08-11 10:29:12.436 [info] <0.654.0> Starting message stores for vhost 'test'
2019-08-11 10:29:12.436 [info] <0.658.0> Message store "KDISMNX5MOYU6Q6PZT8TQDPY/msg_store_transient": using rabbit_msg_store_ets_index to provide index
2019-08-11 10:29:12.436 [info] <0.654.0> Started message store of type transient for vhost 'test'
2019-08-11 10:29:12.436 [info] <0.661.0> Message store "KDISMNX5MOYU6Q6PZT8TQDPY/msg_store_persistent": using rabbit_msg_store_ets_index to provide index
2019-08-11 10:29:12.436 [warning] <0.661.0> Message store "KDISMNX5MOYU6Q6PZT8TQDPY/msg_store_persistent": rebuilding indices from scratch
2019-08-11 10:29:12.436 [info] <0.654.0> Started message store of type persistent for vhost 'test'
2019-08-11 10:29:12.452 [info] <0.643.0> Setting permissions for 'guest' in 'test' to '.*', '.*', '.*'

每个对应的vhost的目录下又包含了如下信息:

Rabbitmq使用姿势_第2张图片

\msg_store_persistent下存储的以.rdq结尾的持久化消息文件,如下图所示

\msg_store_transient下存储的以.rdq结尾的临时消息文件,取决于内存队列的使用情况从而在磁盘与内存间周转,如下图所示

\queues下包含该vhost下的所有队列子目录,每个子目录代表一个队列,子目录下包含对应的队列命名文件以及.idx结尾的队列索引文件,如下图所示

Rabbitmq使用姿势_第3张图片

这里每个队列目录下对应着如下文件信息

Rabbitmq使用姿势_第4张图片

\.vhost文件存储当前该vhost的命名

\recovery.dets文件存储的可恢复的元数据

三、RabbitMQ的交换机(exchange)

Rabbitmq使用姿势_第5张图片

DirectExchange(直连模式)发送场景:

Rabbitmq使用姿势_第6张图片

一、发送消息的时候不指定交换机,指定routing-key发送:
1、两个队列绑定在相同DirectExchange交换机(默认的交换机在内),不同的routing-key上,消息能够准确到达和routing-key同名的目标队列
2、两个队列绑定在相同DirectExchange交换机,相同routing-key上,消息也是只到达和routing-key同名的队列上,
反之,如果发送时指定的routing-key和所有队列名称不同,消息则丢失。

二、发送消息的时候指定交换机,也指定routing-key发送:
1、两个队列绑定在一个DirectExchange交换机,不同的routing-key上,消息能够准确到达和routing-key绑定的目标队列
2、两个队列绑定在相同DirectExchange交换机,相同routing-key上,消息被两个队列消费

结论:发送消息时不指定Exchange,采用的是默认绑定策略,查找与routing-key同名队列进行路由,指定了Exchange则按照使用者设置的绑定策略查找路由。至于发送消息的时候什么都不指定以及只指定Exchange不指定routing-key,消息都将丢失。

TopicExchange(通配符模式)发送场景:

 

Rabbitmq使用姿势_第7张图片

一、发送消息的时候不指定交换机,指定routing-key发送消息达到目标队列的情况与DirectExchange一致
二、发送消息的时候指定交换机,也指定了routing-key发送:
1、两个队列绑定在一个TopicExchange交换机,不同的非通配符的routing-key上,消息能够准确到达和routing-key绑定的目标队列,此时与直连交换一致
2、两个队列绑定在相同TopicExchange交换机,相同的非通配符的routing-key上,消息被两个队列消费,此时与直连交换一致
3、两个队列绑定在相同TopicExchange交换机,相同的通配符的routing-key上,消息将发送到两个队列

4、两个队列绑定在相同TopicExchange交换机,不相同的通配符的routing-key上,消息将根据routing-key的通配符*/#模式来路由到所绑定的队列

结论:发送消息时不指定Exchange,采用的是默认绑定策略,查找与routing-key同名队列进行路由,指定了Exchange则按照使用者设置的绑定策略查找路由,如果队列所绑定的routing-key的通配符模式,则按照通配符规则进行路由,*代表匹配一个单词,#代码匹配0个或多个单词。至于发送消息的时候什么都不指定以及只指定Exchange不指定routing-key,消息都将丢失。

FanoutExchange(发布/订阅模式)发送场景:

扇出类型的交换忽略routing-key 的一种路由方式,它只是将收到的所有消息广播到它知道的所有队列中,也就是说在发送消息到这个exchange上时,只要跟该exchange进行绑定的队列都可以接收到消息

Rabbitmq使用姿势_第8张图片

HandersExchange(消息头模式)发送场景:

Headers类型的exchange使用的比较少,它是忽略routingKey的一种路由方式。是使用Headers来匹配的。Headers是一个键值对,可以定义成Hashtable。发送者在发送的时候定义一些键值对,接收者也可以再绑定时候传入一些键值对,两者匹配的话,则对应的队列就可以收到消息。 

匹配有两种方式all和any。这两种方式是在消费者端必须要用键值”x-mactch”来定义。all代表定义的多个键值对都要满足,any代表只要满足一个就可以。fanout,direct,topic exchange的routingKey都需要要字符串形式的,而headers exchange则没有这个要求,因为键值对的值可以是任何类型。

Rabbitmq使用姿势_第9张图片

四、RabbitMQ的队列(queue)

        在RabbitMQ中,消息永远不能直接发送到队列,它总是需要通过交换。当我们在使用时只创建一个队列不做其它任何绑定时,RabbitMQ会使用由空字符串标识的默认exchange,同时队列名称作为默认的routing-key。这种交换很特殊 - 它允许我们准确地指定消息应该去哪个队列。

1、普通队列

 @Bean
    public Queue directQueue(){
        return new Queue(DIRECT_QUEUE);
    }

创建一个队列不进行任何绑定设置时,RabbitMQ默认将其与默认的交换进行绑定,通过管理台的Queues->Bindings可以看到


2、延迟队列

  • x-message-ttl(设置队列 / 消息过期时间)
  @Bean
    public Queue ttlQueue(){
        Map args = new HashMap();
        args.put("x-message-ttl",10000);
        args.put("x-dead-letter-exchange","");
        args.put("x-dead-letter-routing-key",OVERFLOW_QUEUE);
        return QueueBuilder.durable(TTL_QUEUE).withArguments(args).build();
    }
  @Bean
    public Queue ttlMessageQueue(){
        Map args = new HashMap();
        args.put("x-dead-letter-exchange","");
        args.put("x-dead-letter-routing-key",OVERFLOW_QUEUE);
        return QueueBuilder.durable(TTL_MESSAGE_QUEUE).withArguments(args).build();
    }
 public void sendToTtlMessage(String args,Integer timeout) {
        rabbitTemplate.convertAndSend(RabbitmqConfigure.TTL_MESSAGE_QUEUE,args, message -> {
            message.getMessageProperties().setExpiration(String.valueOf(timeout*1000));
            return message;
        });
    }

这里需要注意的是设置消息的过期时间,即使一个消息比在同一队列中的其他消息提前过期,提前过期的也不会优先进入死信队列,它们还是按照入库的顺序让消费者消费。 如果第一进去的消息过期时间是1小时,那么死信队列的消费者也许等1小时才能收到第一个消息。 参考官方文档发现“Only when expired messages reach the head of a queue will they actually be discarded (or dead-lettered).” 只有当过期的消息到了队列的顶端(队首),才会被真正的丢弃或者进入死信队列。 所以在考虑使用RabbitMQ来实现延迟任务队列的时候,需要确保业务上每个任务的延迟时间是一致的。如果遇到不同的任务类型需要不同的延时的话,需要为每一种不同延迟时间的消息建立单独的消息队列,就像上面ttlQueue定义一样。


3、优先级队列

  • 队列的最大优先级:x-max-priority (支持有限数量的优先级:255。建议值介于1和10之间,数字越大表示优先级越高)
  • 消息的优先级:priority
 @Bean
    public Queue priorityQueue(){
        Map args = new HashMap();
        args.put("x-max-priority",10);
        return new Queue(PRIORITY_QUEUE,true,false,false,args);
    }
 public void sendToPriority(String agrs,Integer priority){
            rabbitTemplate.convertAndSend(RabbitmqConfigure.PRIORITY_QUEUE,agrs+priority,message ->{
            message.getMessageProperties().setPriority(priority);
            return message;
        });
    }

这里需要注意的是,当我们将一个设置了优先级的消息发送到非优先级队列时,消息的优先级并没有什么作用。消息的优先级只体现在队列中存在消息的情况下,也就是说当队列中都是空的时候发一个消费一个,这时根本不存在消息的优先级概念。


4、排他队列

首先排他队列只对首次声明它的连接(Connection)可见,另外一个连接无法声明一个同样的排他性队列;其次是只区别连接(Connection)而不是通道(Channel),从同一个连接创建的不同的通道可以同时访问某一个排他性的队列。如果试图在一个不同的连接中重新声明或访问(如publish,consume)该排他性队列,会得到资源被锁定的错误:

ESOURCE_LOCKED - cannot obtain exclusive access to locked queue ' exclusive-queue'

最后,排他队列会在其所属连接断开的时候自动删除,而不管这个队列是否被声明成持久性的(Durable =true)。

 @Bean
    public Queue exclusiveQueue(){
        return QueueBuilder.durable(EXCLUSIVE_QUEUE).exclusive().build();
    }

5、定制策略队列

队列长度限制

  •  x-max-length(队列中就绪消息的条数进行限制)
  •  x-max-length-bytes(所有消息体长度的总和进行限制,忽略消息属性等)

命令设置:

rabbitmqctl set_policy my-pol “^ one-meg $” \
   “{”max-length-bytes“:1048576}' \
  - 应用到队列

在my-pol策略保证了one-meg 队列包含不超过消息数据长度的1MB。达到1MB限制时,将从队列头部丢弃最旧的消息。

编码方式: 

 @Bean
    public Queue queue(){
        Map args = new HashMap();
        args.put("x-max-length",2);
        return new Queue(TOPIC_QUEUE,true,false,false,args);
    }

队列溢出行为策略

  • x-overflow

使用overflow设置配置队列溢出行为。如果overflow设置为reject-publish,则将丢弃最近发布的消息。此外,如果 启用了发布确认,则会通过basic.nack消息通知发布者拒绝 。如果邮件被路由到多个队列并被至少其中一个队列拒绝,则该频道将通过basic.nack通知发布者。该消息仍将发布到可以将其排队的所有其他队列。

命令设置:

rabbitmqctl set_policy my-pol “^ one-messages $” \
   “{”max-length“:2,”overflow“:”reject-publish“}' \
  - 应用到队列

在my-pol策略保证了one-meg 队列 包含不超过2个消息和所有附加出版发送basic.nack只要队列包含2个消息和发行商确认启用响应。

编码方式:


 @Bean
    public Queue limitQueue(){
        Map args = new HashMap();
        args.put("x-max-length",2);
        args.put("x-overflow","reject-publish");
        return new Queue(LIMIT_QUEUE,true,false,false,args);
    }

队列受限消息被拒不删除转发到其余队列

  • x-dead-letter-exchange
  • x-dead-letter-routing-key

编码方式:

 public static final String OVERFLOW_QUEUE = "overflow_queue";

    @Bean
    public Queue limitQueue(){
        Map args = new HashMap();
        args.put("x-max-length",2);
//        args.put("x-overflow","reject-publish");
        args.put("x-dead-letter-exchange","");
        args.put("x-dead-letter-routing-key",OVERFLOW_QUEUE);
        return new Queue(LIMIT_QUEUE,true,false,false,args);
    }

    @Bean
    public Queue overflowQueue(){
        return QueueBuilder.durable(OVERFLOW_QUEUE).build();
    }

这里需要注意的是,当你为长度限制了的队列设置了x-overflow溢出行为策略时,同时又设置x-dead-letter-exchange进行消息的转发,x-dead-letter-exchange不会生效

 

队列过期

  • x-expires

多长时间没有消费者访问该队列的时候,该队列会自动删除

  @Bean
    public Queue ttlQueue(){
        Map args = new HashMap();
        args.put("x-expires",10000);
        return QueueBuilder.durable(TTL_QUEUE).withArguments(args).build();
    }

 


6、惰性队列

RabbitMQ从3.6.0版本开始引入了惰性队列(Lazy Queue)的概念。惰性队列会尽可能的将消息存入磁盘中,而在消费者消费到相应的消息时才会被加载到内存中,它的一个重要的设计目标是能够支持更长的队列,即支持更多的消息存储。当消费者由于各种各样的原因(比如消费者下线、宕机亦或者是由于维护而关闭等)而致使长时间内不能消费消息造成堆积时,惰性队列就很有必要了。

默认情况下,当生产者将消息发送到RabbitMQ的时候,队列中的消息会尽可能的存储在内存之中,这样可以更加快速的将消息发送给消费者。即使是持久化的消息,在被写入磁盘的同时也会在内存中驻留一份备份。当RabbitMQ需要释放内存的时候,会将内存中的消息换页至磁盘中,这个操作会耗费较长的时间,也会阻塞队列的操作,进而无法接收新的消息。

惰性队列会将接收到的消息直接存入文件系统中,而不管是持久化的或者是非持久化的,这样可以减少了内存的消耗,但是会增加I/O的使用,如果消息是持久化的,那么这样的I/O操作不可避免,惰性队列和持久化消息可谓是“最佳拍档”。注意如果惰性队列中存储的是非持久化的消息,内存的使用率会一直很稳定,但是重启之后消息一样会丢失。

队列具备两种模式:default和lazy。默认的为default模式,在3.6.0之前的版本无需做任何变更。lazy模式即为惰性队列的模式,可以通过调用channel.queueDeclare方法的时候在参数中设置,也可以通过Policy的方式设置,如果一个队列同时使用这两种方式设置的话,那么Policy的方式具备更高的优先级。如果要通过声明的方式改变已有队列的模式的话,那么只能先删除队列,然后再重新声明一个新的
 

  • x-queue-mode(default/lazy)
 @Bean
    public Queue lazyQueue(){
        return QueueBuilder.durable(LAZY_QUEUE).withArgument("x-queue-mode","lazy").build();
    }

 

五、Rabbitmq的持久性:

持久层有两个组件:队列索引 和消息存储

1、队列索引负责维护队列中落盘的消息,包括消息的存储地点、是否已经被交付给消费者、是否已被消费者ack等,每个队列都有一个与之对应的队列索引管理。

队列索引的位置位于如下图所示:

Rabbitmq使用姿势_第10张图片

队列索引管理按顺序存储段文件,文件编号从0开始,后缀.idx,且每个段文件包含固定的SEGMENT_ENTRY_COUNT条记录。SEGMENT_ENTRY_COUNT默认是16384,最大值为queue_index_max_journal_entries,是SEGMENT_ENTRY_COUNT的2倍。每个队列索引都需要在内存中保留至少一个段文件,当序列化后的消息(消息属性,消息头,消息体)整体大小小于rabbitmq.config中配置的queue_index_embed_msgs_below项的值,queue_index_embed_msgs_below默认为4096字节时,该消息将直接存储在队列索引中。因此,如果queue_index_embed_msgs_below的值增加会导致使用大量内存。

小消息存储在队列索引中

主要优点是:

  • 消息可以在一次操作中写入磁盘而不是两次; 对于微小的消息。
  • 写入队列索引的消息不需要消息存储索引中的条目,因此在分页时不会有内存开销。

缺点是:

  • 队列索引将固定数量的记录块保存在内存中; 如果将非微小的消息写入队列索引,则内存使用可能很大。
  • 如果消息被交换路由到多个队列,则需要将消息写入多个队列索引。如果将此类消息写入消息存储库,则只需要写入一个副本。
  • 目标为队列索引的未确认消息始终保留在内存中。

2、消息存储都会序列化后以追加的方式写入到文件中,文件名从0开始累加,后缀是.rdq,当一个文件的大小超过指定的限制(rabbitmq.config中msg_store_file_size_limit)后,关闭这个文件再创建一个新的文件存储。增大该值可能会加快(顺序)磁盘写入速度,但会减慢段GC进程。GC的评率由rabbitmq.config中background_gc_target_interval设置

消息以以下格式存在于文件中:

1

<>

MsgId为RabbitMQ通过rabbit_guid:gen()每一个消息生成的GUID,MsgBody会包含消息对应的exchange,routing_keys,消息的内容,消息对应的协议版本,消息内容格式。

在进行消息存储时,RabbitMQ会在ETS表中记录消息在文件中的位置映射和文件的相关信息。读取消息的时候先根据消息的msg_id找到对应的文件,如果文件存在且未被锁住则直接打开文件,如果文件不存在或者锁住了则发请求到rabbit_msg_store进程进行处理。

注意:生产者为了确保消息的持久性,通常是创建持久交换机,持久队列,以及消息的Delivery_mode(发布模型)为2,这样能够尽量确保rabbitmq重启后消息不会丢失。

RabbitMQ会在ETS表使用了两个数据结构:

Index: MsgId到#msg_location结构的映射,说明消息在文件中的位置:

        {MsgId, RefCount, File, Offset, TotalSize}

RefCount代表消息的引用计数;File代表消息保存的文件名(数字);Offset代表消息在文件中的偏移量;TotalSize代表消息的大小。

FileSummary: File 到 #file_summary{}的映射,说明持久化文件的信息:

        {File, ValidTotalSize, Left, Right, FileSize, Locked, Readers}

File代表持久化文件名称(数字);ValitTotalSize代表文件中的有效消息大小;Left代表该文件左右的文件名;Right代表该文件右边的文件(Left与Right的存在主要与文件的合并有关,前面提到文件以数字命名,并连续递增,但文件合并会引起文件名称不连续);FileSize代表文件当前大小;Locked代表当前文件是否被锁定,当有GC在操作该文件时,文件被锁定;Readers代表该文件当前的读者数量。

3、消息的删除只是从ETS表删除执行消息的相关信息,同时更新对应的存储文件的相关信息,并不立即对文件中的消息进行删除,后续会有专门的垃圾回收进程负责合并待回收消息文件。当所有文件中的垃圾消息(已经被删除的消息)比例大于阈值(GARBAGE_FRACTION = 0.5)时,会触发文件合并操作(至少有三个文件存在的情况下),以提高磁盘利用率。publish消息时写入内容,ack消息时删除内容(更新该文件的有用数据大小),当一个文件的有用数据等于0时,删除该文件。

六、RabbitMQ的确认机制

1、消息发送确认

需要开启配置的消息发送确认功能: publisher-confirms: true //默认是false

生产者将信道设置成confirm模式,一旦信道进入confirm模式,所有在该信道上面发布的消息都会被指派一个唯一的ID(从1开始),一旦消息被投递到所有匹配的队列之后,broker就会发送一个确认给生产者(包含消息的唯一ID),这就使得生产者知道消息已经正确到达目的队列了,如果消息和队列是可持久化的,那么确认消息会将消息写入磁盘之后发出,broker回传给生产者的确认消息中deliver-tag域包含了确认消息的序列号,confirm模式最大的好处在于他是异步的,一旦发布一条消息,生产者应用程序就可以在等信道返回确认的同时继续发送下一条消息,当消息最终得到确认之后,生产者应用便可以通过回调方法来处理该确认消息,如果RabbitMQ因为自身内部错误导致消息丢失,就会发送一条nack消息,生产者应用程序同样可以在回调方法中处理该nack消息。


普通confirm模式:每发送一条消息后,调用waitForConfirms()方法,等待服务器端confirm。实际上是一种串行confirm了。
批量confirm模式:每发送一批消息后,调用waitForConfirms()方法,等待服务器端confirm。
异步confirm模式:提供一个回调方法,服务端confirm了一条或者多条消息后Client端会回调这个方法。

ConfirmCallback
     通过实现 ConfirmCallback 接口,消息发送到 Broker 后触发回调,确认消息是否到达 Broker 服务器,也就是只确认是否正确到达 Exchange 中。

需要开启配置的消息发送确认功能: publisher-returns: true //默认是false

ReturnCallback
     通过实现 ReturnCallback 接口,设置Mandatory为true标识,启动消息失败返回,比如路由不到队列时触发回调,可以在回调函数处理该失败的消息

2、消息消费确认

消费者消费确认模型(acknowledge-mode)有手动(manual)、自动(none)、视情况(auto),

消息通过 ACK 确认是否被正确接收,每个 Message 都要被确认(acknowledged),可以手动去 ACK 或自动 ACK
自动确认会在消息发送给消费者后立即确认,但存在丢失消息的可能,如果消费端消费逻辑抛出异常,也就是消费端没有处理成功这条消息,那么就相当于丢失了消息
如果手动确认则当消费者调用 ack、nack、reject 几种方法进行确认,手动确认可以在业务失败后进行一些操作,比如重回队尾、转发到其余队列、或者直接丢弃。

auto它会根据方法的执行情况来决定是否确认还是拒绝,如果消息成功被消费(成功的意思是在消费的过程中没有抛出异常),则自动确认
当抛出 AmqpRejectAndDontRequeueException 异常的时候,则消息会被拒绝,且 requeue = false(不重新入队列)
当抛出 ImmediateAcknowledgeAmqpException 异常,则消费者会被确认
其他的异常,则消息会被拒绝,且 requeue = true(如果此时只有一个消费者监听该队列,则有发生死循环的风险,多消费端也会造成资源的极大浪费,这个在开发过程中一定要避免的)。可以通过 setDefaultRequeueRejected(默认是true)去设置
 

deliveryTag(唯一标识 ID):当一个消费者向 RabbitMQ 注册后,会建立起一个 Channel ,RabbitMQ 会用 basic.deliver 方法向消费者推送消息,这个方法携带了一个 delivery tag, 它代表了 RabbitMQ 向该 Channel 投递的这条消息的唯一标识 ID,是一个单调递增的正整数,delivery tag 的范围仅限于 Channel
multiple:为了减少网络流量,手动确认可以被批处理,当该参数为 true 时,则可以一次性确认 delivery_tag 小于等于传入值的所有消息

//当前当个消息成功确认channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);
//小于等于当前消息标识的所有消息都确认          
channel.basicAck(message.getMessageProperties().getDeliveryTag(),true);
  
channel.basicNack("消息唯一标识","是否批量","是否重新入队");

channel.basicReject("消息唯一标识","是否重新入队");

七、RabbitMQ的队列机制

       RabbitMQ作为一个经纪人,中间代理方,当无法直接将消息投递到消费者时,需要暂时将消息入队列,以便重新投递。RabbitMQ内部提供了一套用于消息状态转换的机制来管理消息。从而来维护系统的性能稳定性和消息的有序性。

大概处理逻辑如下:

      首先处理消息的mandatory标志,和confirm属性。mandatory标志告诉服务器至少将该消息route到一个队列中,否则将消息返还给生产者。confirm则是消息的发布确认。
然后判断队列中是否有消费者正在等待,如果有则直接调用对应的接口给客户端发送消息。
如果队列上没有消费者,根据当前相关设置判断消息是否需要丢弃,不需要丢弃的情况下调用backing_queue的接口将消息入队。
初始默认情况下,非持久化消息直接进入内存队列,此时效率最高,当内存占用逐渐达到一个阈值时,消息和消息索引逐渐往磁盘中移动,随着消费者的不断消费,内存占用的减少,消息逐渐又从磁盘中被转到内存队列中。

在RabbitMQ中,队列中的消息可能会处于以下四种状态:

alpha:消息内容以及消息在队列中的位置(消息索引)都保存在内存中;

beta:消息内容只在磁盘上,消息索引只在内存中;

gamma:消息内容只在磁盘上,消息索引在内存和磁盘上都存在;

delta:消息的内容和索引都在磁盘上。

注意:对于持久化消息,消息内容和消息索引都必须先保存在磁盘上,然后才处于上述状态中的一种。对于以上几种状态来说,alpha状态是最消耗内存的。

RabbitMQ提供这种分类的主要作用是满足不同机器的内存和CPU。alpha最耗内存,但很少消耗CPU;delta基本不消耗内存,但是要消耗更多的CPU以及磁盘I/O操作(delta需要两次I/O操作,一次读索引,一次读消息内容;beta及gamma只需要一次I/O操作来读取消息内容),下面通过一张图来大概的表示状态间的流转变更流程。

Rabbitmq使用姿势_第11张图片

        上图通过不同的颜色将箭头做了区分,每一组颜色的箭头代表着当前因为什么原因触发了消息的状态流转机制,消息入队时先判断Q3是否为空,如果Q3为空,则直接进入Q4,否则进入Q1,因为Q3为空,Delta一定为空,因为Delta不为空,那么Q3取出最后一个消息的时候Delta会触发把消息转移到Q3了,这样Q3就自然不是空了。消息入队后,需要判断内存使用情况,比如由于内存不足引起的Q1->Q2 , Q4 ->Q3的状态转换。 如果依旧内存不足,Q3->delta ,Q2->delta的状态转换,当然也存在着消息直接进入Q4,或者消息从Q1->Q4的情况。

        消息的读取首先从Q4开始,如果Q4为空,则从Q3获取,如果Q3为空则直接就认定为没有消息了,因为当在读取Q3的最后一条消息时,会触发delta中的消息往Q3中进行转移。当delta队列为空时,q2队列可以直接跳到q3队列,q1队列也可以直接跳到q3。如果Q3和Delta都是空的,此时将Q1的消息转移到Q4,直接从Q4消费即可

你可能感兴趣的:(RabbitMQ)