Golang 實現 RabbitMQ 的死信佇列

Mr-houzi發表於2022-01-30

讀本文之前,你應該已經瞭解 RabbitMQ 的一些概念,如佇列、交換機之類。

死信概念

通俗來講,無法被正常消費的訊息,我們可以稱之為死信。我們將其放入死信佇列,單獨處理這部分“異常”訊息。

當訊息符合以下的一個條件時,將會稱為死信。

  • 訊息被拒絕,不重新放回佇列(使用 basic.reject / basic.nack 方法拒絕訊息,並且這兩個方法的引數 requeue = false)
  • 訊息TTL過期
  • 佇列達到最大長度

應用

應用場景:當消費者無法正常消費訊息、訊息發生異常時,為了保證資料不丟失,將異常的訊息置為死信,放入死信佇列。在死信佇列中的訊息,將啟動單獨的消費程式特殊處理。

架構圖:

下面跟著架構圖來實現程式碼。

生產者

一個生產者一般來說只需要做兩件事,一是建立連結,二是傳送訊息。

RabbitMQ 中涉及的佇列、交換機、routing-key,這些都需要在程式碼中實現建立。這些操作既可以由生產者建立,也可以由消費者建立。關於誰來建立的探討,見RabbitMq:誰來建立佇列和交換機?此文。

本文中佇列、交換機、routing-key 放到生產者一方來實現。所以生產者一共需要做這幾件事。

  1. 建立連線
  2. 設定佇列(佇列、交換機、繫結)
  3. 設定死信佇列(佇列、交換機、繫結)
  4. 釋出訊息

建立連線

利用streadway/amqp包,與RabbitMQ 建立連線。

func main() {
    mq := util.NewRabbitMQ()
    defer mq.Close()
    mqCh := mq.Channel

    ……
}

……

// util.NewRabbitMQ()

func NewRabbitMQ() *RabbitMQ {
    conn, err := amqp.Dial(constant.MqUrl)
    FailOnError(err, "Failed to connect to RabbitMQ")

    ch, err := conn.Channel()
    FailOnError(err, "Failed to open a channel")

    return &RabbitMQ{
        Conn: conn,
        Channel: ch,
    }
}

設定佇列(佇列、交換機、繫結)

核心操作就是設定佇列階段。

宣告普通佇列,並指定死信交換機、指定死信routing-key。後續死信佇列建立後會與死信交換機、指定死信routing-key進行繫結

var err error
_, err = mqCh.QueueDeclare(constant.NormalQueue, true, false, false, false, amqp.Table{
    "x-message-ttl": 5000, // 訊息過期時間,毫秒
    "x-dead-letter-exchange": constant.DeadExchange, // 指定死信交換機
    "x-dead-letter-routing-key": constant.DeadRoutingKey, // 指定死信routing-key
})
util.FailOnError(err, "建立normal佇列失敗")

宣告交換機

err = mqCh.ExchangeDeclare(constant.NormalExchange, amqp.ExchangeDirect, true, false, false, false, nil)
util.FailOnError(err, "建立normal交換機失敗")

目前,普通佇列和交換機都已經建立,但它們都是獨立存在,沒有關聯。

通過 QueueBind 將佇列、routing-key、交換機三者繫結到一起。

err = mqCh.QueueBind(constant.NormalQueue, constant.NormalRoutingKey, constant.NormalExchange, false, nil)
util.FailOnError(err, "normal:佇列、交換機、routing-key 繫結失敗")

設定死信佇列(佇列、交換機、繫結)

同樣死信佇列,也需要建立佇列、建立交換機和繫結。

// 宣告死信佇列
// args 為 nil。切記不要給死信佇列設定訊息過期時間,否則失效的訊息進入死信佇列後會再次過期。
_, err = mqCh.QueueDeclare(constant.DeadQueue, true, false, false, false, nil)
util.FailOnError(err, "建立dead佇列失敗")

// 宣告交換機
err = mqCh.ExchangeDeclare(constant.DeadExchange, amqp.ExchangeDirect, true, false, false, false, nil)
util.FailOnError(err, "建立dead佇列失敗")

// 佇列繫結(將佇列、routing-key、交換機三者繫結到一起)
err = mqCh.QueueBind(constant.DeadQueue, constant.DeadRoutingKey, constant.DeadExchange, false, nil)
util.FailOnError(err, "dead:佇列、交換機、routing-key 繫結失敗")

當死信佇列建立完畢,普通佇列通過 x-dead-letter-exchangex-dead-letter-routing-key 引數的指定,便可生效,死信佇列便與普通佇列連通。

釋出訊息

message := "msg" + strconv.Itoa(int(time.Now().Unix()))
fmt.Println(message)

// 釋出訊息
err = mqCh.Publish(constant.NormalExchange, constant.NormalRoutingKey, false, false, amqp.Publishing{
    ContentType: "text/plain",
    Body:        []byte(message),
})
util.FailOnError(err, "訊息釋出失敗")

生產者完整程式碼

package main

import (
    "fmt"
    "github.com/streadway/amqp"
    "learn_gin/go/rabbitmq/deadletter/constant"
    "learn_gin/go/rabbitmq/deadletter/util"
    "strconv"
    "time"
)

func main() {
    // # ========== 1.建立連線 ==========
    mq := util.NewRabbitMQ()
    defer mq.Close()
    mqCh := mq.Channel

    // # ========== 2.設定佇列(佇列、交換機、繫結) ==========
    // 宣告佇列
    var err error
    _, err = mqCh.QueueDeclare(constant.NormalQueue, true, false, false, false, amqp.Table{
        "x-message-ttl": 5000, // 訊息過期時間,毫秒
        "x-dead-letter-exchange": constant.DeadExchange, // 指定死信交換機
        "x-dead-letter-routing-key": constant.DeadRoutingKey, // 指定死信routing-key
    })
    util.FailOnError(err, "建立normal佇列失敗")

    // 宣告交換機
    err = mqCh.ExchangeDeclare(constant.NormalExchange, amqp.ExchangeDirect, true, false, false, false, nil)
    util.FailOnError(err, "建立normal交換機失敗")

    // 佇列繫結(將佇列、routing-key、交換機三者繫結到一起)
    err = mqCh.QueueBind(constant.NormalQueue, constant.NormalRoutingKey, constant.NormalExchange, false, nil)
    util.FailOnError(err, "normal:佇列、交換機、routing-key 繫結失敗")

    // # ========== 3.設定死信佇列(佇列、交換機、繫結) ==========
    // 宣告死信佇列
    // args 為 nil。切記不要給死信佇列設定訊息過期時間,否則失效的訊息進入死信佇列後會再次過期。
    _, err = mqCh.QueueDeclare(constant.DeadQueue, true, false, false, false, nil)
    util.FailOnError(err, "建立dead佇列失敗")

    // 宣告交換機
    err = mqCh.ExchangeDeclare(constant.DeadExchange, amqp.ExchangeDirect, true, false, false, false, nil)
    util.FailOnError(err, "建立dead佇列失敗")

    // 佇列繫結(將佇列、routing-key、交換機三者繫結到一起)
    err = mqCh.QueueBind(constant.DeadQueue, constant.DeadRoutingKey, constant.DeadExchange, false, nil)
    util.FailOnError(err, "dead:佇列、交換機、routing-key 繫結失敗")

    // # ========== 4.釋出訊息 ==========
    message := "msg" + strconv.Itoa(int(time.Now().Unix()))
    fmt.Println(message)

    // 釋出訊息
    err = mqCh.Publish(constant.NormalExchange, constant.NormalRoutingKey, false, false, amqp.Publishing{
        ContentType: "text/plain",
        Body:        []byte(message),
    })
    util.FailOnError(err, "訊息釋出失敗")
}

消費者

由於佇列、交換機都交由生產者來建立,消費者只需做兩件,一是建立連線、二是消費訊息。

也由於這個原因,消費者要晚於生產者啟動,可以保證消費的時候,佇列是存在的。

package main

import (
    "learn_gin/go/rabbitmq/deadletter/constant"
    "learn_gin/go/rabbitmq/deadletter/util"
    "log"
)

func main() {
    // # ========== 1.建立連線 ==========
    mq := util.NewRabbitMQ()
    defer mq.Close()
    mqCh := mq.Channel

    // # ========== 2.消費訊息 ==========
    msgsCh, err := mqCh.Consume(constant.NormalQueue, "", false, false, false, false, nil)
    util.FailOnError(err, "消費normal佇列失敗")

    forever := make(chan bool)
    go func() {
        for d := range msgsCh {
            // 要實現的邏輯
            log.Printf("接收的訊息: %s", d.Body)

            // 手動應答
            d.Ack(false)
            //d.Reject(true)
        }
    }()
    log.Printf("[*] Waiting for message, To exit press CTRL+C")
    <-forever
}

死信消費者

死信佇列、交換機都也交由生產者來建立了,死信消費者也只需做兩件,建立連線和消費訊息。

package main

import (
    "learn_gin/go/rabbitmq/deadletter/constant"
    "learn_gin/go/rabbitmq/deadletter/util"
    "log"
)

func main() {
    // # ========== 1.建立連線 ==========
    mq := util.NewRabbitMQ()
    defer mq.Close()
    mqCh := mq.Channel

    // # ========== 2.消費死信訊息 ==========
    msgsCh, err := mqCh.Consume(constant.DeadQueue, "", false, false, false, false, nil)
    util.FailOnError(err, "消費dead佇列失敗")

    forever := make(chan bool)
    go func() {
        for d := range msgsCh {
            // 要實現的邏輯
            log.Printf("接收的訊息: %s", d.Body)

            // 手動應答
            d.Ack(false)
            //d.Reject(true)
        }
    }()
    log.Printf("[*] Waiting for message, To exit press CTRL+C")
    <-forever
}

原始碼Mr-houzi/go-demo

end!

個人部落格同步文章 Golang 實現 RabbitMQ 的死信佇列

本作品採用《CC 協議》,轉載必須註明作者和本文連結

相關文章