RabbitMQ 高级功能
一、mandatory 和 immediate
生产者发送消息的方法 Channel.Publish() 各个参数解析如下:
err := ch.Publish(
"helloEx", //exchange:源交换器名称,如果设置为空字符串,则消息会被发送到RabbitMQ默认的交换器中。
"helloEx2helloQ", //key:路由键
false, //mandatory:
false, //immediate:
msg, //msg:发送消息,数据类型为 amqp.Publishing
)
其中 mandatory 和 immediate 定义了消息的最终去向。
1. mandatory
消息发送到交换器以后,当交换器无法根据自身的类型和路由键找到一个符合条件的队列,若 mandatory 为 true,RabbitMQ 会调用 Basic.Return 命令将消息返回给生产者;若 mandatory 为 false,消息会被直接丢弃。
AMQP 协议流转过程:
2. immediate
当 immediate 为 true 时,如果交换器在将消息路由到队列时发现队列上并不存在任何消费者,那么这条消息将不会存入队列中。当与路由键匹配的所有队列都没有消费者时,该消息会通过 Basic.Return 返回至生产者。
概括来说,mandatory 参数告诉服务器至少将该消息路由到一个队列中,否则将消息返回给生产者。immediate 参数告诉服务器,如果该消息关联的队列上有消费者,则立刻投递;如果所有匹配的队列上都没有消费者,则直接将消息返还给生产者,不用将消息存入队列而等待消费者了。
RabbitMQ 3.0 版本开始去掉了对 immediate 参数的支持,对此 RabbitMQ 官方解释是:immediate 参数会影响镜像队列的性能,增加了代码复杂性,建议采用 TTL+DLX 的方法替代。
3. Channel.NotifyReturn
通过调用 Channel.NotifyReturn() 添加监听器,生产者可以获取到没有被正确路由到合适队列的消息。
代码示例:
package main
import (
"fmt"
"time"
"github.com/streadway/amqp"
)
func main() {
// connect to rabbitmq
conn, err := amqp.Dial("amqp://root:shiajun666@192.168.10.4:5672")
if err != nil {
fmt.Println("Connect to RabbitMQ failed: ", err)
return
}
defer conn.Close()
// open a channel
ch, err := conn.Channel()
if err != nil {
fmt.Println("Open channel failed: ", err)
return
}
defer ch.Close()
// declare an exchange
err = ch.ExchangeDeclare("HelloEx2", "direct", true, false, false, false, nil)
if err != nil {
fmt.Println("Declare exchange failed: ", err)
return
}
// declare an queue
_, err = ch.QueueDeclare("helloQ2", true, false, false, false, nil)
if err != nil {
fmt.Println("Declare queue failed: ", err)
return
}
// bind the exchange with the queue
err = ch.QueueBind("helloQ2", "HelloEx22helloQ", "HelloEx2", false, nil)
if err != nil {
fmt.Println("Bind the exchange with the queue failed: ", err)
return
}
// add return listener
returnChan := make(chan amqp.Return, 0)
ch.NotifyReturn(returnChan)
// send a message
msg := amqp.Publishing{
ContentType: "text/plain", //消息内容类型
DeliveryMode: amqp.Persistent, //消息传输类型:1 amqp.Transient 不管队列是否持久化,消息都不会被持久
// 2 amqp.Persistent 只有队列是持久化的,消息才会持久化,否则消息同样不会持久化
Priority: 0, //消息优先级:0 - 9
ReplyTo: "", //address to to reply to (ex: RPC)
Expiration: "", //消息有效期
MessageId: "", //message identifier
Timestamp: time.Now(), //消息发送时间戳
Type: "", //消息类型
UserId: "", //creating user id - ex: "guest
AppId: "", //creating application id
Body: []byte("Hello World"), //消息内容
}
err = ch.Publish(
"HelloEx2", //exchange:源交换器名称,如果设置为空字符串,则消息会被发送到RabbitMQ默认的交换器中。
"HelloEx22helloQ2", //key:路由键
true, //mandatory:
false, //immediate:
msg, //msg:发送消息,数据类型为 amqp.Publishing
)
if err != nil {
fmt.Println("Send message failed: ", err)
return
}
// get return messages
for {
select {
case returnMsg := <-returnChan:
fmt.Printf("Get return message from exchange[%s], routing key[%s], content: %s]\n",
returnMsg.Exchange, returnMsg.RoutingKey, string(returnMsg.Body))
}
}
}
上述程序中,交换器 HelloEx2 与队列 helloQ2 绑定,Binding key 为 HelloEx22helloQ,而发送消息时指定的 Routing key 为 HelloEx22helloQ2,mandatory 为 true。此时消息无法路由到合适的队列,所以 RabbitMQ 会将消息返回给生产者,运行程序最终打印结果为:
Get return message from exchange[HelloEx2], routing key[HelloEx22helloQ2], content: Hello World]
二、备份交换器
备份交换器,英文名称为 Alternate Exchange,简称 AE。
如果既不想因为添加返回消息监听而复杂化生产者的编程逻辑,又不想消息丢失,那么可以使用备份交换器,这样可以将未被路由的消息存储在 RabbitMQ 中,在需要的时候去处理这些消息。
Go 语言客户端设置为某个交换器设置备份交换器的方法很简单,只需在调用 Channel.ExchangeDeclare() 方法声明交换器的时候,在最后一个参数加入 alternate-exchange 参数指定备份交换器的名称即可。
代码示例:
package main
import (
"fmt"
"time"
"github.com/streadway/amqp"
)
func main() {
// connect to rabbitmq
conn, err := amqp.Dial("amqp://root:shiajun666@192.168.10.4:5672")
if err != nil {
fmt.Println("Connect to RabbitMQ failed: ", err)
return
}
defer conn.Close()
// open a channel
ch, err := conn.Channel()
if err != nil {
fmt.Println("Open channel failed: ", err)
return
}
defer ch.Close()
// alternate exchange name
aeName := "myAe"
// declare an exchange with a alternate exchange
args := amqp.Table{"alternate-exchange": aeName}
err = ch.ExchangeDeclare("normalExchange", "direct", true, false, false, false, args)
if err != nil {
fmt.Println("Declare exchange failed: ", err)
return
}
// declare an queue
_, err = ch.QueueDeclare("normalQueue", true, false, false, false, nil)
if err != nil {
fmt.Println("Declare queue failed: ", err)
return
}
// bind the exchange with the queue
err = ch.QueueBind("normalQueue", "normalRoutingKey", "normalExchange", false, nil)
if err != nil {
fmt.Println("Bind the exchange with the queue failed: ", err)
return
}
// declare an alternate exchange
err = ch.ExchangeDeclare(aeName, "fanout", true, false, false, false, nil)
if err != nil {
fmt.Println("Declare exchange failed: ", err)
return
}
// declare an queue
_, err = ch.QueueDeclare("unrouteQueue", true, false, false, false, nil)
if err != nil {
fmt.Println("Declare queue failed: ", err)
return
}
// bind the alternate exchange with the queue
err = ch.QueueBind("unrouteQueue", "normalRoutingKey", aeName, false, nil)
if err != nil {
fmt.Println("Bind the exchange with the queue failed2: ", err)
return
}
// send a message
msg := amqp.Publishing{
ContentType: "text/plain", //消息内容类型
DeliveryMode: amqp.Persistent, //消息传输类型:1 amqp.Transient 不管队列是否持久化,消息都不会被持久
// 2 amqp.Persistent 只有队列是持久化的,消息才会持久化,否则消息同样不会持久化
Priority: 0, //消息优先级:0 - 9
ReplyTo: "", //address to to reply to (ex: RPC)
Expiration: "", //消息有效期
MessageId: "", //message identifier
Timestamp: time.Now(), //消息发送时间戳
Type: "", //消息类型
UserId: "", //creating user id - ex: "guest
AppId: "", //creating application id
Body: []byte("Hello World"), //消息内容
}
err = ch.Publish(
"normalExchange", //exchange:源交换器名称,如果设置为空字符串,则消息会被发送到RabbitMQ默认的交换器中。
"notExistRoutingKey", //key:路由键
false, //mandatory:
false, //immediate:
msg, //msg:发送消息,数据类型为 amqp.Publishing
)
if err != nil {
fmt.Println("Send message failed: ", err)
return
}
}
运行程序,从 RabbitMQ 管理后台可以看到,备份交换器 myAE 绑定的队列 unrouteQueue 上多了一条消息。
流转过程:
注:
如果设置的备份交换器不存在,或者备份交换器没有绑定任何队列,或者没有任何与 RoutineKey 匹配的队列,消息都会丢失。
为了防止消息丢失,备份交换器的类型最好设置为 fanout。
如果备份交换器和 mandatory 参数一起使用,那么 mandatory 参数无效。
三、过期时间(TTL)
TTL,Time to Live 的简称,即过期时间。RabbitMQ 可以对消息和队列设置 TTL。
1. 设置消息的 TTL
目前有两种方法可以设置消息的 TTL。第一种方法是通过队列属性设置,队列中所有消息都有相同的过期时间。第二种方法是对消息本身进行单独设置,每条消息的 TTL 可以不同。如果两种方法一起使用,则消息的 TTL 以两者之间较小的那个数值为准。
如果不设置 TTL,则表示此消息不会过期;如果将 TTL 设置为0,则表示除非此时可以直接将消息投递到消费者,否则该消息会被立即丢弃。
消息在队列中的生存时间一旦超过设置的 TTL 值时,就会变成“死信”(Dead Message),消费者将无法再收到该消息。
对于第一种设置队列 TTL 属性的方法,一旦消息过期,就会从队列中抹去,而在第二种方法中,即使消息过期,也不会马上从队列中抹去,因为每条消息是否过期是在即将投递到消费者之前判定的。
为什么这两种方法处理的方式不一样?因为第一种方法里,队列中已过期的消息肯定在队列头部,RabbitMQ 只要定期从队头开始扫描是否有过期的消息即可(先入列的消息肯定是先过期的)。而第二种方法里,每条消息的过期时间不同,如果要删除所有过期消息势必要扫描整个队列,所以不如等到此消息即将被消费时再判定是否过期,如果过期再进行删除即可。
代码示例:
(1)通过队列属性设置消息 TTL
调用 Channel.QueueDeclare() 方法声明队列时,在最后的扩展参数中设置 “x-message-ttl”。
qargs := amqp.Table{"x-message-ttl": 10000} //单位:毫秒
_, err := ch.QueueDeclare("normalQueue2", true, false, false, false, qargs)
if err != nil {
fmt.Println("Declare queue failed: ", err)
return
}
(2)对消息本身设置 TTL
通过 amqp.Publishing 结构体的 “Expiration” 字段进行设置。
msg := amqp.Publishing{
ContentType: "text/plain", //消息内容类型
DeliveryMode: amqp.Persistent, //消息传输类型:1 amqp.Transient 不管队列是否持久化,消息都不会被持久
// 2 amqp.Persistent 只有队列是持久化的,消息才会持久化,否则消息同样不会持久化
Priority: 0, //消息优先级:0 - 9
ReplyTo: "", //address to to reply to (ex: RPC)
Expiration: "5000", //消息有效期
MessageId: "", //message identifier
Timestamp: time.Now(), //消息发送时间戳
Type: "", //消息类型
UserId: "", //creating user id - ex: "guest
AppId: "", //creating application id
Body: []byte("Hello World"), //消息内容
}
2. 设置队列的 TTL
调用 Channel.QueueDeclare() 方法声明队列时,在最后的扩展参数中设置 “x-expires”。
qargs := amqp.Table{"x-expires": 10000} //单位:毫秒
_, err = ch.QueueDeclare("normalQueue3", true, false, false, false, qargs)
if err != nil {
fmt.Println("Declare queue failed: ", err)
return
}
四、死信队列
DLX,全称为 Dead-Letter-Exchange,可以称之为死信交换器,也有人称之为死信邮箱。当消息在一个队列中变成死信(dead message)之后,它能被重新被发送到另一个交换器中,这个交换器就是 DLX,绑定 DLX 的队列就称之为死信队列。
消息变成死信一般是由于以下几种情况:
(1)消息被接收方拒绝,并且没有重新入列的参数设置;
(2)消息过期;
(3)队列达到最大长度。
DLX 也是一个正常的交换器,和一般的交换器没有区别,它能在任何的队列上被指定,实际上就是设置某个队列的属性。当这个队列中存在死信时,RabbitMQ 就会自动地将这个消息重新发布到设置的 DLX 上去,进而被路由到另一个队列,即死信队列。
DLX 的设置也很简单,在 Channel.QueueDeclare() 扩展参数中设置“x-dead-letter-exchange”即可。
代码示例:
package main
import (
"fmt"
"time"
"github.com/streadway/amqp"
)
func main() {
// connect to rabbitmq
conn, err := amqp.Dial("amqp://root:shiajun666@192.168.10.4:5672")
if err != nil {
fmt.Println("Connect to RabbitMQ failed: ", err)
return
}
defer conn.Close()
// open a channel
ch, err := conn.Channel()
if err != nil {
fmt.Println("Open channel failed: ", err)
return
}
defer ch.Close()
// dead letter exchange name
dlxName := "exchange.dlx"
// declare an exchange
err = ch.ExchangeDeclare("exchange.normal", "fanout", true, false, false, false, nil)
if err != nil {
fmt.Println("Declare exchange failed: ", err)
return
}
// declare a queue with TTL and DLX
qargs := amqp.Table{
"x-message-ttl": 10000,
"x-dead-letter-exchange": dlxName,
}
_, err = ch.QueueDeclare("queue.normal", true, false, false, false, qargs)
if err != nil {
fmt.Println("Declare queue failed: ", err)
return
}
// bind the exchange with the queue
err = ch.QueueBind("queue.normal", "normalRoutingKey", "exchange.normal", false, nil)
if err != nil {
fmt.Println("Bind the exchange with the queue failed: ", err)
return
}
// declare a dead letter exchange
err = ch.ExchangeDeclare(dlxName, "direct", true, false, false, false, nil)
if err != nil {
fmt.Println("Declare exchange failed: ", err)
return
}
// declare a queue
_, err = ch.QueueDeclare("queue.dlx", true, false, false, false, nil)
if err != nil {
fmt.Println("Declare queue failed: ", err)
return
}
// bind the dead letter exchange with the queue
err = ch.QueueBind("queue.dlx", "normalRoutingKey", dlxName, false, nil)
if err != nil {
fmt.Println("Bind the exchange with the queue failed: ", err)
return
}
// send a message
msg := amqp.Publishing{
ContentType: "text/plain", //消息内容类型
DeliveryMode: amqp.Persistent, //消息传输类型:1 amqp.Transient 不管队列是否持久化,消息都不会被持久
// 2 amqp.Persistent 只有队列是持久化的,消息才会持久化,否则消息同样不会持久化
Priority: 0, //消息优先级:0 - 9
ReplyTo: "", //address to to reply to (ex: RPC)
Expiration: "", //消息有效期
MessageId: "", //message identifier
Timestamp: time.Now(), //消息发送时间戳
Type: "", //消息类型
UserId: "", //creating user id - ex: "guest
AppId: "", //creating application id
Body: []byte("Hello World"), //消息内容
}
err = ch.Publish(
"exchange.normal", //exchange:源交换器名称,如果设置为空字符串,则消息会被发送到RabbitMQ默认的交换器中。
"normalRoutingKey", //key:路由键
false, //mandatory:
false, //immediate:
msg, //msg:发送消息,数据类型为 amqp.Publishing
)
if err != nil {
fmt.Println("Send message failed: ", err)
return
}
}
以上代码中,交换器 exchange.normal 绑定队列 queue.normal,交换器 exchange.dlx 绑定队列 queue.dlx,同时设置队列 queue.normal 的消息过期时间为 10000 毫秒,且其 DLX 为 exchange.dlx。消息发送给交换器 exchange.normal,再路由到队列 queue.normal,10000 毫秒后,消息过期,RabbitMQ 自动将其发送到交换器 exchange.dlx,再路由到队列 queue.dlx。
流转过程:
RabbitMQ web 后台:
五、TTL+DLX 的拓展应用
1. 实现 immediate 参数效果
通过队列属性设置消息的 TTL 为 0,同时为该队列设置一个 DLX,生产者监听死信队列中的消息,进行相应的处理。这样,如果消息无法直接投递到消费者,则会进入死信队列,从而被生产者监听到,这与 RabbitMQ 3.0 版本之前的 immediate 参数为 true 时消息无法直接投递到消费者则返回给生产者有异曲同工之妙。
2. 实现延迟队列功能
例如,在“4. 死信队列”的代码示例中,若生产者直接将消息发送到交换器 exchange.normal,但消费者却监听的是死信队列 queue.dlx,则生产者发送的每条消息都会在队列 queue.nomal 中逗留 10000 毫秒之后才能被消费者接收到,从而实现了延迟队列的功能。
可通过使用多个队列设置多个 TTL 和 DLX,实现不同延迟等级的队列:
六、优先级队列
优先级高的消息具备优先被消费的特权。可在 Channel.QueueDeclare() 扩展参数中通过“x-max-priority”设置队列中消息的优先级。