Golang 實現 RabbitMQ 的延遲佇列

Mr_houzi發表於2022-02-08
讀本文之前,你應該已經瞭解 RabbitMQ 的一些概念,如佇列、交換機之類。

延遲佇列簡介

一個佇列中的訊息在延遲一段時間後才被消費者消費,這樣的佇列可以稱之為延遲佇列。

延遲佇列的應用場景十分廣泛,如:下單後30分鐘內未付款則取消訂單;在某個時間下發一條通知等。

通過死信實現延遲佇列

通過Golang 實現 RabbitMQ 的死信佇列的介紹,我們可以很容易的實現一個延遲佇列。

  1. 將正常佇列的消費者取消;
  2. 發訊息時設定TTL;

通過上面兩點,正常佇列的訊息始終不會被消費,而是等待訊息TTL到期,進入死信佇列,讓死信消費者進行消費,從而達到延遲佇列的效果。

上面看上去似乎沒什麼問題,實測一下就會發現訊息不會“如期死亡”

當先生產一個TTL為60s的訊息,再生產一個TTL為5s的訊息,第二個訊息並不會再5s後過期進入死信佇列,而是需要等到第一個訊息TTL到期後,與第一個訊息一同進入死信佇列。這是因為RabbitMQ 只會判斷佇列中的第一個訊息是否過期。

通過外掛實現延遲佇列

架構

對於上文的問題,自然有解決方法,那就是通過 RabbitMQ 的 rabbitmq_delayed_message_exchange 外掛來解決。本文不贅述 RabbitMQ和外掛的安裝,你可以參考此文安裝或使用Docker來安裝。

此外掛的原理是將訊息在交換機處暫儲存在mnesia(一個分散式資料系統)表中,延遲投遞到佇列中,等到訊息到期再投遞到佇列當中。

簡單瞭解了外掛的原理,我們便可以如此設計延遲佇列。

實現

生產者實現的關鍵點:

1.在宣告交換機時不在是direct型別,而是x-delayed-message型別,這是由外掛提供的型別;

2.交換機要增加"x-delayed-type": "direct"引數設定;

3.釋出訊息時,要在 Headers 中設定x-delay引數,來控制訊息從交換機過期時間;

err = mqCh.Publish(constant.Exchange1, constant.RoutingKey1, false, false, amqp.Publishing{
    ContentType: "text/plain",
    Body:        []byte(message),
    //Expiration: "10000", // 訊息過期時間(訊息級別),毫秒
    Headers: map[string]interface{}{
        "x-delay": "5000", // 訊息從交換機過期時間,毫秒(x-dead-message外掛提供)
    },
})

生產者完整程式碼:

// producter.go
package main

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

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

    // # ========== 2.設定佇列(佇列、交換機、繫結) ==========
    // 宣告佇列
    var err error
    _, err = mqCh.QueueDeclare(constant.Queue1, true, false, false, false, amqp.Table{
        // "x-message-ttl": 60000, // 訊息過期時間(佇列級別),毫秒
    })
    util.FailOnError(err, "建立佇列失敗")

    // 宣告交換機
    //err = mqCh.ExchangeDeclare(Exchange1, amqp.ExchangeDirect, true, false, false, false, nil)
    err = mqCh.ExchangeDeclare(constant.Exchange1, "x-delayed-message", true, false, false, false, amqp.Table{
        "x-delayed-type": "direct", 
    })
    util.FailOnError(err, "建立交換機失敗")

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

    // # ========== 4.釋出訊息 ==========
    message := "msg" + strconv.Itoa(int(time.Now().Unix()))
    fmt.Println(message)
    // 釋出訊息
    err = mqCh.Publish(constant.Exchange1, constant.RoutingKey1, false, false, amqp.Publishing{
        ContentType: "text/plain",
        Body:        []byte(message),
        //Expiration: "10000", // 訊息過期時間(訊息級別),毫秒
        Headers: map[string]interface{}{
            "x-delay": "5000", // 訊息從交換機過期時間,毫秒(x-dead-message外掛提供)
        },
    })
    util.FailOnError(err, "訊息釋出失敗")
}

由於在生產者端建立佇列和交換機,所以消費者並不需要特殊的設定,直接附程式碼。

消費者完整程式碼:

// consumer.go
package main

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

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

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

    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
}

end!

原始碼Mr-houzi/go-demo

相關文章