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”设置队列中消息的优先级。

 

posted @ 2022-09-30 11:01  疯一样的狼人  阅读(101)  评论(0编辑  收藏  举报