讀本文之前,你應該已經瞭解 RabbitMQ 的一些概念,如佇列、交換機之類。
延遲佇列簡介
一個佇列中的訊息在延遲一段時間後才被消費者消費,這樣的佇列可以稱之為延遲佇列。
延遲佇列的應用場景十分廣泛,如:下單後30分鐘內未付款則取消訂單;在某個時間下發一條通知等。
通過死信實現延遲佇列
通過Golang 實現 RabbitMQ 的死信佇列的介紹,我們可以很容易的實現一個延遲佇列。
- 將正常佇列的消費者取消;
- 發訊息時設定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!