rabbitmq 基础
基础#
什么事消息中间件#
消息中间件有两种模式:点对点模式和发布、订阅模式。
点对点模式是基于队列的,消息生产者发送消息到队列,消息消费者从队列中接收消息,队列的存在使得消息的异步成为可能。
消息中间件的作用:
解耦、冗余(存储)、扩展性、削峰、可恢复性,顺序保证、缓冲、异步通信。
安装rabbitmq:#
安装完成后,默认情况下,访问rabbitmq服务的用户名和密码都是guest
,这个账户有限制,默认只能通过本地网络(localhost)访问,远程网络访问受限,所以在实现生产和消费消息之前,需要添加一个用户,并设置相应的访问权限。
# 添加环境变量 在 /etc/profile export PATH=$PATH:/opt/rabbitmq/sbin export RABBITMQ_HOME=/opt/rabbitmq source /etc/profile # 添加用户 $ rabbitmqctl add_user root root # 为root用户设置所有权限 $ rabbitmqctl set_permissions -p / root ".*" ".*" ".*" # 设置root用户为管理员角色 $ rabbitmqctl set_user_tags root administrator
相关概念#
rabbitmq整体上是一个生产者与消费者模型,主要负责接收、存储和转发消息。
生产者和消费者:
Producer:生产者,就是投递消息的一方。生产者创建消息,然后发布到rabbitmq中。
消息一般包含两个部分:消息体和标签。消息体也可称为payload。消息体一般是一个带有业务逻辑结构的数据,比如一个JSON字符串。消息的标签用来表述这条消息,比如一个交换器的名称和一个路由键。
生产者把消息交由 rabbitmq,rabbitmq之后根据标签把消息发送给感兴趣的消费者。
Consumer:消费者,就是接收消息的一方。
消费者连接到rabbitmq服务器,并订阅到队列上。当消费者消费一条消息时,只是消费消息的消息体(payload)。在消息路由的过程中,消息的标签会丢失,存入到队列的消息只有消息体,消费者也只会消费到消息体,也不直达消息的生产者是谁。
Broker:消息中间件的服务节点。
对于rabbitmq来说,一个rabbitmq Broker 可以简单的看作一个rabbitmq服务节点,或者rabbitmq服务实例。
# 业务流程 producer -> 业务方数据 -> 序列化之后的数据 -> 指定Exchange和RoutingKey等即为添加Label -> 消息 -> 发送至Broker中 -> Rabbitmq Broker -> Consumer 订阅并接收消息 -> consumer 反序列化数据 -> 业务处理
首先生产者将业务方数据封装成消息,发送到Broker中。消费者订阅并接收消息,并处理得到原始的数据,之后在进行业务处理逻辑。
QUEUE:队列,是rabbitmq的内部对象,用于存储消息。rabbitmq中消息只能存储在队列中。
多个消费者可以订阅同一个队列,这是队列中的消息会被平均分摊(round-robin,即轮询)给多个消费者进行处理,而不是每个消费者都能收到所有的消息并处理。
交换器、路由键、绑定:
Exchange:交换器。生产者将消息发送到Exchange(交换器),由交换器将消息路由到一个活多个队列中。如果路由不到,或许会返回生产者,或许直接丢弃。
RoutingKey:路由键。生产者将消息发给交换器的时候,一般会指定一个RoutingKey,用来指定这个消息的路由规则,而这个Routing Key需要与交换器类型和绑定键(BindingKey)联合使用才能最终生效。
在交换器类型和绑定键固定的情况下,生产者可以在发送消息给交换器时,通过指定 Routing Key来决定消息流向哪里。
Binding:绑定。Rabbitmq中通过绑定将交换器与队列关联起来,在绑定的时候一般会指定一个绑定键,这样rabbitmq就知道如何正确的将消息路由到队列了。
producer -> exchange -> binding key -> queue
生产者叫消息发送给交换器时,需要一个RoutingKey,当BindingKey和RoutingKey相匹配时,消息会被路由到对应的队列中。
一个交换器可以当定多个队列,这些绑定允许使用相同的BindingKey。BindingKey并不是在所有的情况下都生效,它依赖与交换器类型。
交换器类型:
rabbitmq常用的交换器类型有fanout、direct、topic、headers这四种。
fanout:它会把所有发送到该交换器的消息路由到所有与该交换器绑定的队列中。
direct:它会把消息路由到那些BindingKey 和 RoutingKey完全匹配的队列中。
producer -> Exchange(type="direct")-> info、waring、debug->queue2 -> ocnsumer2 -> warning -> queue1 -> consumer1
如果在发送消息的时候设置路由键位info和debug,消息只会路由到queue2。
Topic:direct的交换器路由规则是完全匹配BindingKey和RoutingKey,topic是模糊匹配。
Headers:根据发送消息内容中的headers属性进行匹配。
rabbitmq运转流程
生产者发送消息的过程:
1、生成者连接到 rabbitmq Broker,建立一个连接(Connection),开启一个信道(Channel)。
2、生产者声明一个交换器,并设置相关属性,比如交换机类型,是否持久化等。
3、生产者声明一个队列并设置相关属性,比如是否排他、是否持久化、是否自动删除等。
4、生产者通过路由键将交换器和队列绑定起来。
5、生产者发送消息至 Rabbitmq Broker,其中包含路由键、交换器等信息。
6、相应的交换器根据接收到的路由键查找相匹配的队列。
7、如果找到,则将从生产者发送过来的消息存入到相应的队列中。
8、如果没有找到,则根据生产者配置的属性选择丢弃还是回退给生成者。
9、关闭信道。
10、关闭连接。
消费者就收消息的过程:
1、消费者连接到 Rabbitmq Broker,建立一个连接,开启一个信道。
2、消费者向 Rabbitmq Broker请求消费相应队列中的消息,可能会设置相应的回调函数,以及做一些准备工作。
3、等待Rabbitmq Broker回应并投递相应队列中的消息,消费者接收消息。
4、消费者确认(ack)接收到消息
5、Rabbitmq 从队列中删除相应已经被确认的消息
6、关闭信道。
7、关闭连接。
生产者将消息发送给交换器,交换器和队列绑定。当生产者发送消息时所携带的RoutingKey 与绑定的 BindingKey 想匹配时,消息即被存入相应的队列之中。消费者可以订阅相应的队列来获取消息。
conneciton 和 channel:
无论是生产者还是消费者,都需要和 Rabbitmq Broker 建立连接,这个连接就是一条 TCP连接,也就是Connection。一旦TCP连接建立起来,客户端紧接着可以创建一个AMQP信道(Channel),每个信道都会被指派一个唯一的ID。信道是建立在Connection之上的虚拟连接,Rabbitmq处理的每条AMQP指令都是通过信道完成的。
注意:Connection可以用来创建多个channel实例,但是channel实例不能再线程间共享。
每个信道的流量不是很大时,复用单一Connection可以在产生性能瓶颈的情况下有效的节省TCP连接资源。当信道本身的流量很大时,需要开辟多个Connection,将这些信道均摊到这些Connection中,需要进行相关的调优策略。
交换器相关参数:
durable:设置交换器是否持久化:为true则表示持久化,反之是非持久化。持久化可以将交换器存盘,在服务器重启的时候不会丢失相关信息。
autoDelete:设置是否自动删除。设置为true则表示自动删除。自动删除的前提是至少有一个队列或者交换器与这个交换器绑定,之后所有与这个交换器绑定的队列或者交换器都与此解绑。注意:不能错误的把这个参数理解为:"当与此交换器连接的客户端都断开时,Rabbitmq 会自动删除本交换器。"
internal:设置是否是内置的。如果设置为true,则表示是内置的交换器,客户端程序无法直接发送消息到这个交换器中,只能通过交换器路由到这个交换器这种方式。
队列相关参数:
durable:设置队列是否持久化:为true则设置队列为持久化,持久化的队列会存盘,在服务器重启的时候可以保证不会丢失相关信息。
exclusive:设置是否排他。为true则设置队列为排他的。如果一个队列被声明为排他队列,该队列队首次声明它的连接可见,并在连接断开时自动删除。注意三点:排他队列是基于连接可见的,同一个连接的不同信道是可以同时访问同一个连接创建的排他队列;"首次"是指如果一个连接已经声明了一个排他队列,其他连接是不允许建立同名的排他队列的,这个与普通队列不同;即使该队列是持久化的,一旦连接关闭或客户端退出,该排他队列都会自动删除,这种队列使用一个客户端同时发送和读取消息的应用场景。
autoDelete:设置是否自动删除。为true则设置队列为自动删除。自动删除的前提是:至少有一个消费者连接到这个队列,之后所有与这个队列连接的消费者都断开时,才会自动删除。注意:不能把这个参数错误的理解为:"当连接到此队列的所有客户端断开时,这个队列自动删除",因为生产者客户端创建这个队列,或者没有消费者客户端与这个队列连接时,都不会自动删除这个队列。
注意:生产者和消费者都能够声明一个队列,但是如果消费者在同一个信道上订阅了另一个队列,就无法再声明队列了。必须取消订阅,然后将信道设置为"传输"模式,之后才能声明队列。
我们不仅可以将队列和交换器绑定起来,也可以将交换器和交换器绑定。
Rabbitmq 的消息存储在队列中,交换器的使用并不真正耗费服务器的性能,而队列会。如果业务本身在架构设计之初已经充分预估了队列的使用情况,完全可以在业务程序上线之前在服务器上创建好(比如通过页面管理、Rabbitmq命令或者更好的是从配置中心下发),这样业务程序也可以免去声明的过程,直接使用即可。
预先创建好资源还有一个好处是,可以确保交换器和队列之间正确的绑定匹配。
消费消息:#
Rabbitmq的消费模式分两种:推(Push)模式和拉(pull)模式。推模式采用Basic.Consume进行消费,而拉模式则是调用Basic.Get进行消费。
注意:Basic.Consume将信道(Channel)置为接收模式,直到取消队列的订阅为止。在接收模式期间,Rabbitmq会不断地推送消息给消费者,当然推送消息的个数还是会受到 Basic.Qos的限制。如果只想从队列获得单条消息而不是持续订阅,建议还是使用Basic.Get进行消费。但是不能将Basic.Get放在一个循环里来代替Basic.Consume,这样做会严重影响 Rabbitmq的性能。如果要实现高吞吐量,消费者理应使用Basic.Consume方法。
推模式:消息中间件主动将消息推送给消费者。(比较常用)
拉模式:消费者主动从消息中间件拉取消息。
推模式将消息提前推送给消费者,消费者必须设置一个缓冲区缓存这些消息。好处很明显,消费者总是有一堆在内存中待处理的消息,效率高。缺点是缓冲区可能溢出。
拉模式在消费者需要时才去消息中间件拉取消息,这段网络开销会明显增加消息延迟,降低系统吞吐量。
// go语言版本 // 推模式 func (ch *Channel) Consume(queue, consumer string, autoAck, exclusive, noLocal, noWait bool, args Table) (<-chan Delivery, error) queue:队列名称 consumer:消费者标签,用于区分不同的消费者 autoAck:是否自动回复ACK,true是。建议为false,手动回复,这样可控性强。 exclusive:设置是否排他。 noLocal:如果是true,表示生产者和消费者不能是同一个connect。 nowait:是否阻塞,true表示是。阻塞:表示请求发送后,阻塞等待MQ server返回信息。 args:其他额外参数 注意返回值:返回一个 <-chan Delivery类型,遍历返回值,有消息则往下走,没有则阻塞。 // 拉模式 func (ch *Channel) Get(queue string, autoAck bool) (msg Delivery, ok bool, err error) queue:队列名称 autoAck:是否自动回复
消费端的确认和拒绝:
为了保证消息从队列可靠地到达消费者,Rabbitmq 提供了消息确认机制(message acknowlegement)。消费者在订阅队列时,可以指定autoAck参数,当 autoAck 等于 fasle时,Rabbitmq 会等待消费者显示地回复确认信号后才从内存(或磁盘)中移除消息(实质上先打上删除标记,之后再删除)。当autoAck等于true时,Rabbitmq会自动把发送出去的消息置为确认,然后从内存(或磁盘)中删除,而不管消费者是否真正地消费到了这些消息。
Rabbitmq进阶#
消息何去何从#
mandatory 和 immediate 是 channel.basicPublish 方法中的两个参数,他们都有当消息传递过程中不可达目的地时将消息返回给生产者。Rabbitmq 提供的备份交换器可以将未能被交换器路由的消息(没有绑定队列或者没有匹配的绑定)存储起来,而不用返回给客户端。
mandatory
当mandatory 参数设为 true时,交换器无法根据自身的类型和路由键找到一个符合条件的队列时,那么Rabbitmq 会调用 Basic.Return 命令将消息返回给生产者。当 mandatory 参数设置为 false时,出现上述情形,则消息直接被丢弃。
生产者如何获取到没有被正确路由到合适队列的消息呢?这时候可以通过调用channel.AddReturnListener 来添加 ReturnListener 监听器实现。
immediate
当 immediate 参数设置为true时,如果交换器在将消息路由到队列时发现队列上并不存在任何消费者,那么这条消息将不会存入队列。当与路由键匹配的所有队列都没有消费者时,该消息会通过 Basic.Return 返回至生产者。
概括来说,mandatory 参数告诉服务器至少将该消息路由到一个队列中,否则将消息返回给生产者。immediate 参数告诉服务器,如果该消息关联的队列上有消费者,则立刻投递;如果所有匹配的队列上都没有消费者,则直接将消息返给生产者,不用将消息存入队列二等待消费者了。
Rabbitmq3.0去掉了对immediate 参数的支持,建议采用 TTL 和 DLX 的方法替代。
备份交换器#
简称AE,或者更直白的成为 备胎交换器。生产者在发送消息的时候如果不设置 mandatory 参数,那么消息在未被路由的情况下将会丢失;如果设置了 mandatory 参数,那么需要添加 ReturnListener 的编程逻辑,生成者的代码将变得复杂。如果既不想复杂化生产者的编程逻辑,又不想消息丢失,那么可以使用备份交换器,这样可以将未被路由的消息存储在Rabbitmq 中,再在需要的时候去处理这些消息。
可以通过在声明交换器(调用 channel.exchangeDeclare 方法)的时候添加 alternate-exchange 参数实现,也可以通过策略(Policy)的方式实现。如果两者同时使用,则前者的优先级更高,会覆盖掉 Policy 的设置。
备份交换器的特殊情况:
- 如果设置的备份交换器不存在,客户端和Rabbitmq 服务端都不会有异常出现,此时消息会丢失。
- 如果备份交换器没有绑定任何队列,客户端和Rabbitmq 服务端都不会有异常出现,此时消息丢失。
- 如果备份交换器没有任何匹配的队列,客户端和Rabbitmq 服务端都不会有异常出现,此时消息丢失。
- 如果备份交换器和 mandatory 参数一起使用,那么 mandatory 参数无效。
过期时间(TTL)#
Rabbitmq 可以对消息和队列设置TTL。
设置消息的TTL:
有两种方法设置消息的TTL。第一种方式是通过队列属性设置,队列中所有消息都有相同的过期时间。第二种方法是对消息本身进行单独设置,每条消息的TTL可以不同。如果两种方法一起使用,则消息的TTL以两者之间较小的那个数值为准。消息在队列中的生存时间一旦超过设置的TTL值时,就会变成 "死信",消费者将无法再收到该消息。
通过队列属性设置消息的TTL 的方法是在 channel.queueDeclare 方法中加入 x-message-ttl 参数实现的,这个参数的单位是毫秒。
如果不设置TTL,则表示此消息不会过期;如果将TTL 设置为 0,则表示除非此时可以直接将消息投递到消费者,否则该消息会立即丢弃,这个特性可以部分替代 Rabbitmq3.0 版本之前的 immediate 参数,之所以部分代替,是因为 immediate 参数在投递失败时会用 Basic.Return 将消息返回(这个功能可以用死信队列来实现)。
针对每条消息设置TTL 的方法是在 channel.basicPublish 方法中加入 expiration 的属性参数,单位为毫秒。
对于第一种设置队列的TTL属性的方法,一旦消息过期,就会从队列中抹去,而第二种方法中,即使消息过期,也不会马上从队列中抹去,因为每条消息是否过期是在即将投递到消费者之前判断的。
设置队列的TTL:
通过channel.queueDeclare 方法中的 x-expires 参数可以控制队列被自动删除前处于未使用状态的时间。未使用的意思是队列上没有任何的消费者,队列也没有被重新声明,并且在过期时间段内也未调用过 Basic.Get 命令。
设置队列里的TTL 可以应用于类似RPC 方式的回复队列,在RPC中,许多队列会被创建出来,但是却是未被使用的。
在Rabbitmq 重启后,持久化的队列的过期时间会被重新计算。
用于表示过期时间的 x-expires 参数以毫秒为单位,并且服从和 x-message -ttl 一样的约束条件,不过不能设置为0。比如该参数设置为1000,则表示该队列如果在 1 秒钟之内未使用则会被删除。
死信队列#
DLX(Dead-Letter-Exchange),死信交换器。当消息在一个队列中变成死信后,它能被重新被发送到另一个交换器中,这个交换器就是DLX,绑定 DLX 的队列称为死信队列。
消息变成死信一般是由于以下几种情况:
- 消息被拒绝(Basic.Reject/Basic.Nack),并且设置 requeue 参数为false。
- 消息过期
- 队列达到最大长度
DLX 也是一个正常的交换器,和一般的交换器没去呗,它能在任何的队列上被指定,实际上就是设置某个队列的属性。当这个队列中存在死信时,Rabbitmq 就会自动地将这个消息重新发布到设置的 DLX 上去,进而被路由到另一个队列,即死信队列。可以监听这个队列中的消息以进行相应的处理。
通过设置channel.queueDeclare 方法中设置 x-dead-letter-exchange 参数来为这个队列添加 DLX。
对于Rabbitmq来说,DLX 是一个非常有用的特性。它可以处理异常情况下,消息不能够被消费者正确消费(消费者调用了 Basic.Nack 或者 Basic.Reject) 而被置入死信队列中的情况,后续分析程序可以通过消费这个死信队列中的内容来分析当时所遇到的异常情况,进而可以改善和优化系统。
// go版本 import ( "github.com/streadway/amqp" ) // 建立与RabbitMQ服务器的连接: // 根据自己的配置修改地址、用户名和密码 conn, err := amqp.Dial("amqp://guest:guest@localhost:5672") if err != nil { log.Fatalf("Failed to connect to RabbitMQ: %v", err) } defer conn.Close() // 创建通道(channel)来处理消息传输: ch, err := conn.Channel() if err != nil { log.Fatalf("Failed to open a channel: %v", err) } defer ch.Close() // 声明死信交换器:(还需要绑定死信队列) err = ch.ExchangeDeclare( "dlx_exchange", // 死信交换器的名称 "direct", // 类型为直连模式 true, // durable参数表示持久化该交换器 false, // autoDelete参数表示不自动删除该交换器 false, // internal参数表示外部客户端无法访问该交换器 false, // noWait参数表示等待确认结果 nil, // arguments参数表示额外的参数 ) if err != nil { log.Fatalf("Failed to declare the dead-letter exchange: %v", err) } // 声明队列时设置死信交换器及路由键: _, err = ch.QueueDeclare( "my_queue", // 队列的名称 true, // durable参数表示持久化该队列 false, // deleteWhenUnused参数表示当最后一个消费者断开连接时自动删除该队列 false, // exclusive参数表示只能被当前连接访问 false, // noWait参数表示等待确认结果 map[string]interface{}{ "x-dead-letter-exchange": "dlx_exchange", // 设置死信交换器 // 如果没有设置 routingkey,则使用原来携带的路由键, // 设置死信routingkey是为了消息路由到死信交换器后,具体推送到哪个队列上 "x-dead-letter-routing-key": "dlx_routing_key", // 设置死信路由键 }, // 设置附加参数 ) if err != nil { log.Fatalf("Failed to declare the queue: %v", err) }
延迟队列#
延迟队列存储的对象是对应的延迟消息,"延迟消息"是指当消息被发送以后,并不想让消费者立刻拿到消息,而是等待特定时间后,消费者才能拿到这个消息进行消费。
Rabbitmq 本身没有直接支持延迟队列的功能,但是可以通过前面所介绍的DLX和TT模拟出延迟队列的功能。
假设一个应用中需要将每条消息都设置为10秒的延迟,生产者通过 exchange.normal 这个交换器将发送的消息存储在 queue.normal 这个队列中。消费者订阅的并非是 queue.normal 这个队列,而是 quue.dlx 这个队列。当消息从 queue.normal 这个队列中过期之后被存入 queue.dlx 这个队列中,消费者就恰好消费到了延迟10秒的这条消息。
当消息过期时,就会转存到相应的死信队列(延迟队列)中,这样消费者根据业务自身的情况,分别选择不同延迟等级(5秒、10秒、5分钟、1小时等)的延迟队列进行消费。
第一种是使用 message TTL 即消息过期时间和 Dead Letter Exchange 死信队列来实现。
第二种是官网出的插件:RabbitMQ Delayed Message
用户可以定义一种 exchange type=x-delayed-message,在发布消息的时候,在消息的头部(header)加上x-delay(定义毫秒),消息将会在定义的毫秒后送达。
安装插件 rabbitmq-plugins enable rabbitmq_delayed_message_exchange
// go语言版本 package main import ( "fmt" "log" "time" "github.com/streadway/amqp" ) func failOnError(err error, msg string) { if err != nil { log.Fatalf("%s: %s", msg, err) } } func main() { // 连接到RabbitMQ服务器 conn, err := amqp.Dial("amqp://guest:guest@localhost:5672") failOnError(err, "Failed to connect to RabbitMQ") defer conn.Close() ch, err := conn.Channel() failOnError(err, "Failed to open a channel") defer ch.Close() // 创建延迟交换机(Exchange) delayedExchangeName := "delayed_exchange" err = ch.ExchangeDeclare( delayedExchangeName, // Exchange名称 "x-delayed-message", // Exchange类型为延迟消息 true, // Durable设置为持久化 false, // AutoDelete设置为不自动删除 false, // Internal设置为非内部使用 nil, // Arguments参数为空 ) failOnError(err, fmt.Sprintf("Failed to declare exchange '%s'", delayedExchangeName)) // 创建延迟队列(Queue) queueName := "delayed_queue" _, err = ch.QueueDeclare( queueName, // Queue名称 true, // Durable设置为持久化 false, // AutoDelete设置为不自动删除 false, // Exclusive设置为非独占模式 false, // NoWait设置为等待确认 amqp.Table{}, // Args参数为空 ) failOnError(err, fmt.Sprintf("Failed to declare queue '%s'", queueName)) // 将延迟队列与延迟交换机进行绑定 err = ch.QueueBind( queueName, // Queue名称 "", // RoutingKey为空字符串表示全局路由 delayedExchangeName, // Exchange名称 false, // NoWait设置为等待确认 nil, // Args参数为空 ) failOnError(err, fmt.Sprintf("Failed to bind queue '%s' with exchange '%s'", queueName, delayedExchangeName)) // 开始消费消息 msgs, err := ch.Consume( queueName, // Queue名称 "consumer", // Consumer标识 false, // AutoAck设置为手动应答 false, // Exclusive设置为非独占模式 false, // NoLocal设置为可以从其他Connection中获取消息 false, // NoWait设置为等待确认 nil, // Args参数为空 ) failOnError(err, "Failed to register consumer") forever := make(chan bool) go func() { for d := range msgs { log.Printf("Received message: %s\n", d.Body) d.Ack(false) // 手动应答消息已被处理完成 } }() log.Println("Start consuming messages...") <-forever }
注意:不论业务逻辑是否处理成功,最终都要将消息手动签收。
使用Go语言异常捕获:TryCatch
,TryCatch
是一个开源的Go语言库。
// go版本 // 在使用Try-Except-Finally时,程序会在Try块中执行代码, // 如果代码执行期间发生异常,则会跳入Except块进行相应的处理; // 不管代码是否发生异常,Finally块中的代码都将会被执行: func foo(){ // 函数体 } Try(func(){ // 执行业务逻辑 foo() fmt.Println("Try Block") }).Except(func(exception interface{}) { // 记录异常 fmt.Printf("发生异常:%v\n", exception) }).Finally(func(){ fmt.Println("Finally Block") })
// go版本 // 构造要发送的消息内容 messageBody := []byte("Hello, RabbitMQ! This is a delayed message.") // 获取当前时间作为消息的延迟时间(单位:毫秒) currentTime := time.Now().UnixNano() / int64(time.Millisecond) // 计算延迟时间(这里设置为10秒后发送) int64(time.Second)/int64(time.Millisecond) 系数常量 delayInMilliSeconds := currentTime + 10*int64(time.Second)/int64(time.Millisecond) // 设置消息的延迟属性 properties := make(map[string]interface{}) properties["deliveryMode"] = uint8(2) // 持久化消息 properties["headers"] = map[string]interface{}{ "x-delay": delayInMilliSeconds, } // 发布消息到交换机 err = ch.Publish( "my_exchange", // exchange name "routingKey", // routing key false, // mandatory false, // immediate amqp.Publishing{ ContentType: "text/plain", Body: messageBody, Properties: properties, }, )
优先级队列#
优先级队列,具有高优先级的队列具有高的优先权,优先级高的消息具备优先被消费的特权。
可以通过设置队列的 x-max-priority 参数来实现。
持久化#
Rabbitmq 的持久化分为三个部分:交换器的持久化、队列的持久化和消息的持久化。
交换器的持久化是通过在声明队列时将durable 参数设置为true 实现的。如果交换器不设置持久化,那么Rabbitmq 服务重启后,相关的交换器元数据会丢失,不过消息不会丢失,只是不能将消息发送到这个交换器中了。对一个长期使用的交换器来说,建议将其置为持久化的。
队列的持久化是通过在声明队列时将 durable 参数设置为 true 实现的。如果队列不设置持久化,那么在Rabbitmq 服务重启后,相关队列的元数据会丢失,此时数据也会丢失。
队列的持久化能保证其本身的元数据不会因异常情况而丢失,但是并不能保证内部所存储的消息不会丢失。要保证消息不会丢失,需要将其设置为持久化。通过将消息的投递模式(BasicProperties 中的 deliveryMode属性) 设置为 2 即可实现消息的持久化。
设置了队列和消息的持久化,当Rabbitmq 服务重启之后,消息依旧存在。
注意:将所有消息都设置为持久化,会严重影响Rabbitmq的性能。写入磁盘的速度比写入内存的速度慢得不只一点点。对于可靠性不是那么高的消息可以不采用持久化处理以提高整体的吞吐量。
将交换器、队列、消息都设置了持久化之后能百分百保证数据不丢失吗?答案是否定的。
首先从消费者来说,如果在订阅消费队列时将 autoAck 参数设置为 true,那么消费者接受到相关消息之后,还没来得及处理就宕机了,这样也算数据丢失。这种情况很好解决,将 autoAck 设置为 false,并进行手动确认。
其次,在持久化的消息正确存入 Rabbitmq 之后,还需要一段时间(虽然很短)才能存入磁盘之中。如果在这段时间内 Rabbitmq 服务节点发生宕机、重启等异常情况,消息保存还没来得及落盘,那么这些消息将会丢失。
这个问题的解决,需要一人 Rabbitmq 的镜像队列机制,相当于配置了副本,如果主节点在此特殊时间内挂掉,可以自动切换到从节点,这样保证了高可用性。实际生产环境中的关键业务队列一般都会设置镜像队列。
生产者确认#
生产者(消息发送方)发送消息后,并不知道消息是否正确到达了 RabbitMQ。
Rabbitmq 这对这个问题,提供了两种解决方式:
- 通过事务机制实现。
- 通过发送方确认(publisher confirm)机制实现。
事务机制:
Rabbitmq 客户端中与事务机制相关的方法有三个:channel.txSelect、channel.txCommit 和 channel.txRollback.
channel.txSelect 用于将当期的信道设置成事务模式,channel.txCommit 用于提交事务,channel.txRollback 用于事务回滚。在通过 channel.txSelect 方法开启事务之后,我们便可以发布消息给 Rabbitmq了,如果事务提交成功,则消息一定到达了 Rabbitmq 中,如果在事务提交执行之前由于 Rabbitmq 异常崩溃或者其他原因抛出异常,这个时候我们便可以将其捕获,进而通过执行channel.txRollback方法来实现事务回滚。
开启事务机制与不开启相比多了四个步骤:
- 客户端发送 Tx.Select,将信道置为事务模式
- Broker 回复 Tx.Select-Ok,确认已将信道置为事务模式
- 在发送完消息之后,客户端发送Tx.Commit提交事务
- Broker 回复 Tx.Commit-Ok,确认事务提交。
事务机制特别有影响 Rabbitmq 的性能,有没有更好的方法既能保证消息发送方确认消息已经正确送达,又能基本上不带来性能上的损失呢?Rabbitmq 提供了一个改进方案,即发送方确认机制。
发送方确认机制:
生产者将信道设置为 confirm(确认)模式,一旦信道进入 confirm 模式,所有在该信道上面发布的消息都会被指派一个唯一的ID(从 1 开始),一旦消息被投递到所有匹配的队列之后,Rabbitmq 就会发送一个确认(Basic.Ack) 给生产者(包含消息的唯一 ID),这使得生产者知晓消息已经正确到达目的地。如果消息和队列时可持久化的,那么确认消息会在消息写入磁盘之后发出。Rabbitmq 回传给生产者的确认消息中的 deliveryTag 包含了确认消息的序号,此外 Rabbitmq 也可以设置 channel.basicAck方法中的 muliple 参数,表示到这个序号之前的所有消息是否都已经得到处理。
事务机制在一条消息发送之后会使发送端阻塞,以等待 Rabbitmq的回应,之后才能继续发送下一条消息。
发送方确认机制最大的好处在于它是异步的。
生成者通过调用 channel.confirmSelect 方法(即 Confirm.Select命令) 将信道设置为 confirm 模式,之后 Rabbitmq 会返回 Confirm.Select-Ok 命令表示同意生产者将当前信道设置为 confirm模式。所有被发送的后续消息都被 ack 或者 nack 一次,不会出现一条消息即被 ack 又被 nack 的情况。
注意:
1、事务机制和publisher confirm 机制两者是互斥的,不能共存。
2、事务机制和publisher confirm 机制确保的消息能够正确发送至Rabbitmq,这里发送至Rabbitmq 的含义是指消息被正确地发送至 Rabbitmq 交换器,如果此交换器没有匹配的队列,那么消息也会丢失。
publisher confirm 的两种方法:
1、批量 confirm 方法:每发送一批消息后,调用 channel.waitForConfirms方法,等待服务器的确认返回。
2、异步 confirm 方法:提供一个回调方法,服务端确认了一条或者多条消息后客户端会回调这个方法进行处理。
消费端#
Rabbitmq 消费端需要注意几点:
- 消息分发
- 消息顺序性
- 弃用 QueueingConsumer
消息分发:
当Rabbitmq 队列拥有多个消费者时,队列收到的消息将以轮询的分发方式发送给消费者。每条消息只会发送给订阅列表里的一个消费者。
轮询分发机制也不是很优雅。默认情况下,如果有 n 个消费者,那么 Rabbitmq 会将第 m 条消息分发给 第 m%n(取余的方式)个消费者, Rabbitmq 不管消费者是否消费并已经确认(Basic.Ack) 了消息。 试想一下,如果某些消费者任务繁重,来不及消费那么多的消息,而某些其他消息者由于某些原因(比如业务逻辑简单、机器性能好)很快的处理完所分配的消息,进而进程空闲,这样就会造成整体应用吞吐量的下降。
这种情况如何处理?要用到channel.basicQos这个方法,channel.basicQos 方法允许限制信道上的消费者所能保持的最大未确认消息的数量。
注意要点:
- Basic.Qos 的使用对于拉模式的消费方式无效。
消息顺序性:
消息的顺序性是指消费者消费到的消息和发送者发布的消息的顺序是一致的。
如果要保证消息的顺序性,需要业务方使用Rabbitmq 之后做进一步的处理,比如在消息体内添加全局有序标识来实现。
弃用 QueueingConsumer:
Rabbitmq4.x版本弃用。
QueueingConsumer 有内存溢出的问题,如果由于某些原因,队列之中堆积了比较多的消息,就可能导致消费者客户端内存溢出假死,于是发生恶性循环,队列消息不断堆积而得不到消化。
消息传输保障#
消息可靠传输一般是业务系统接入消息中间件时首要考虑的问题,一般消息中间件的消息传输保障分为三个层级:
- At most once: 最多一次。消息可能会丢失,但绝不会重复传输。
- At least once: 最少一次。消息绝不会丢失,但可能会重复传输。
- Exactly onece: 恰好一次。每条消息肯定会被传输一次且仅传输一次。
Rabbitmq 中 "最少一次"投递实现需要考虑一下几个方面:
1、消息生产者需要开启事务机制或者publisher confirm 机制,以确保消息可以可靠的传输到Rabbitmq中。
2、消息生产者需要配合使用 mandatory 参数或者备份交换器来确保消息能够从交换器路由到队列中,进而能够保存下来而不会被丢弃。
3、消息和队列都需要持久化处理,以确保 Rabbitmq 服务器在遇到异常情况时不会造成消息丢失。
4、消费者在消费消息的同时需要将 autoAck 设置为 false,然后通过手动确认的方式去确认已经正确消费的消息,以避免在消费端引起不必要的消息丢失。
Rabbitmq 有没有去重机制来保证 "恰好一次"呢?答案是没有,目前大多数主流的消息中间件都没有消息的去重机制。去重处理一般是在业务客户端实现,比如引入 GUID(Globally Unique Identifier)的概念。建议在实际生产环境中,业务方更加自身的业务特性进行去重。
Rabbitmq配置:
优化网络配置的一个重要目标就是提高吞吐量。比如增大TCP缓冲区的大小。每个TCP连接都分配了缓冲区。一般来说,缓冲区越大,吞吐量也会越高,但是每个连接上耗费的内存也就越多。
要提高吞吐量可以使用 rabbit.tcp_listen_options 来加大配置。
消息追踪#
在Rabbitmq 中可以使用 Firehose 功能来实现消息的追踪,Firehose 可以记录每一次发送或者消费消息的记录,方法Rabbitmq的使用者进行调试、排查等。
开启Firehose命令:rabbitmqctl trace_on [-p vhost] 其中 [-p vhost] 是可选参数,用来指定西主机 vhost。
关闭命令:rabbitmqctl trace_off [-p vhost]
Firehost 默认情况下处于关闭状态,并且Firehose 的状态也是非持久化的,会在Rabbitmq服务重启的时候还原成默认的状态。
rabbitmq_tracing 插件:相当于Firehose的GUI版本。。
命令:rabbitmq-plugin enable rabbitmq_tracing 启动。
日志文件路径默认 /var/tmp/rabbitmq-tracing
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 【自荐】一款简洁、开源的在线白板工具 Drawnix
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY
· Docker 太简单,K8s 太复杂?w7panel 让容器管理更轻松!