rabbitmq原理和應用

slowquery發表於2022-09-22

0.1、索引

blog.waterflow.link/articles/16637...

RabbitMQ 是一個輕量級且易於部署的訊息佇列。它支援開箱即用的多種訊息傳遞協議。我們將使用 AMQP(高階訊息佇列協議)

1、概念

https://cdn.learnku.com/uploads/images/202209/22/25530/bTDFgu6uxJ.png!large

既然是訊息佇列,顧名思義,肯定會有生產者生產訊息,消費者消費訊息,還會有佇列用來儲存訊息,等等。

我們先來看下這些概念:

  • Producer: 將訊息推送到rabbitmq交換機的應用
  • Consumer: 從佇列讀取訊息並處理他們的應用
  • Exchange: 交換機負責在Binding和Routing key的幫助下,將訊息路由到不同的佇列。從上圖可以看出rabbitmq有多種型別的交換機
  • Binding: Binding是佇列和交換機之間的連結
  • Routing key: 交換機用來決定如何將訊息路由到佇列的鍵。可以看做是訊息的地址
  • Queue: 儲存訊息的緩衝區
  • Connection:生產者到Broker(rabbitmq服務),消費者到Broker的連線
  • Channel:為了複用一個連線,一個connection下可以有多個channel,可以把connection理解成電線,channel就是電線裡面的銅絲。

訊息傳遞的完整流程是這樣的:

  1. 生產者初始化一個到rabbitmq服務的連線
  2. 獲取連線的管道,透過管道宣告一個交換機
  3. 透過管道宣告一個佇列,透過繫結的路由鍵將佇列和交換機繫結(傳送訊息的時候宣告一個佇列並繫結交換機,訊息會進到佇列裡。如果不宣告也可以放到消費者去宣告佇列和繫結交換機。需要注意的是生產者沒有宣告佇列的話,此時已經生產多條訊息,然後去開啟消費者消費,是不會消費到之前的訊息的)
  4. 透過管道傳送訊息到指定的交換機
  5. 消費者初始化一個到rabbitmq服務的連線
  6. 獲取連線的管道,透過管道宣告一個佇列
  7. 透過繫結的路由鍵將佇列和交換機繫結
  8. 從佇列中消費訊息

交換機型別:

  1. direct:直接指定到某個佇列
  2. topic:釋出訂閱模式,一個交換機可以對應多個佇列,透過路由規則匹配
  3. fanout:顧名思義,無腦廣播模式

2、示例

生產者:

package main

import (
    "fmt"
    "time"

    "github.com/streadway/amqp"
)

var (
    conn    *amqp.Connection
    channel *amqp.Channel
    queue   amqp.Queue
    mymsg   = "Hello HaiCoder"
    err     error

    confirms chan amqp.Confirmation
)

func main() {
    // 建立連線
    conn, err = amqp.Dial("amqp://guest:guest@127.0.0.1:5672/")
    if err != nil {
        fmt.Println(err)
        return
    }

    defer conn.Close()

    // 建立channel
    if channel, err = conn.Channel(); err != nil {
        fmt.Println(err)
        return
    }

  // 宣告交換機
    err = channel.ExchangeDeclare("liutest", amqp.ExchangeDirect, false, false, false, false, nil)
    if err != nil {
        fmt.Println("ExchangeDeclare Err =", err)
        return
    }

    // 建立佇列
    if queue, err = channel.QueueDeclare("liutest", false, false, false, false, nil); err != nil {
        fmt.Println("QueueDeclare Err =", err)
        return
    }

  // 佇列和交換機繫結
    err = channel.QueueBind(queue.Name, "queueroutekey", "liutest", false, nil)
    if err != nil {
        fmt.Println("QueueBind Err =", err)
        return
    }

    channel.Confirm(false)
    confirms = channel.NotifyPublish(make(chan amqp.Confirmation, 1))
    //傳送資料
    go func() {
        for {
            if err = channel.Publish("liutest", "queueroutekey", false, false, amqp.Publishing{
                ContentType: "text/plain",
                Body:        []byte(mymsg),
            }); err != nil {
                fmt.Println("Publish Err =", err)
                return
            }
            fmt.Println("Send msg ok, msg =", mymsg)
            time.Sleep(time.Second * 5)
        }
    }()

    go func() {
        for confirm := range confirms {
            if confirm.Ack {
                fmt.Printf("confirmed delivery with delivery tag: %d \n", confirm.DeliveryTag)
            } else {
                fmt.Printf("confirmed delivery of delivery tag: %d \n", confirm.DeliveryTag)
            }
        }
    }()

    select {}

}

消費者:

package main

import (
    "fmt"

    "github.com/streadway/amqp"
)

var (
    conn    *amqp.Connection
    channel *amqp.Channel
    queue   amqp.Queue
    err     error
    msgs    <-chan amqp.Delivery
)

func main() {
    // 建立連線
    conn, err = amqp.Dial("amqp://guest:guest@127.0.0.1:5672/")
    if err != nil {
        fmt.Println(err)
        return
    }

    defer conn.Close()

    // 建立channel
    if channel, err = conn.Channel(); err != nil {
        fmt.Println(err)
        return
    }

    // 建立佇列
    if queue, err = channel.QueueDeclare("liutest", false, false, false, false, nil); err != nil {
        fmt.Println("QueueDeclare Err =", err)
        return
    }

    err = channel.QueueBind("liutest", "queueroutekey", "liutest", false, nil)
    if err != nil {
        fmt.Println("QueueBind Err =", err)
        return
    }
    //讀取資料
    if msgs, err = channel.Consume(queue.Name, "", false, false, false, false, nil); err != nil {
        fmt.Println("Consume Err =", err)
        return
    }
    go func() {
        for msg := range msgs {
            fmt.Println("Receive Msg =", string(msg.Body))
            msg.Ack(false)
        }
    }()

    select {}

}

3、訊息可靠性

rabbitmqack

生產者可靠性

// 將通道設定為確認模式
func (ch *Channel) Confirm(noWait bool) error {
    if err := ch.call(
        &confirmSelect{Nowait: noWait},
        &confirmSelectOk{},
    ); err != nil {
        return err
    }

    ch.confirmM.Lock()
    ch.confirming = true
    ch.confirmM.Unlock()

    return nil
}
// 用於接受服務端的確認響應
func (ch *Channel) NotifyPublish(confirm chan Confirmation) chan Confirmation {
    ch.notifyM.Lock()
    defer ch.notifyM.Unlock()

    if ch.noNotify {
        close(confirm)
    } else {
        ch.confirms.Listen(confirm)
    }

    return confirm

}

Confirm 將此通道置為確認模式,以便生產者可以確保服務端已成功接收所有訊息。進入該模式後,服務端將傳送一個basic.ack或basic.nack訊息,其中deliver tag設定為一個基於1的增量索引(用來標識訊息的唯一性),對應於該方法返回後收到的每次ack。

在 Channel.NotifyPublish上監聽以響應ack。如果未呼叫 Channel.NotifyPublish,則ack將被忽略。

ack的順序不受投遞訊息順序的約束。

Ack 和 Nack 確認將在未來的某個時間到達。

在通知任何 Channel.NotifyReturn 偵聽器後,立即確認不可路由的mandatory或immediate訊息。當所有應該將訊息路由到它們的佇列都已收到傳遞確認或已將訊息加入佇列時,其他訊息將被確認,必要時將訊息持久化。

注:當mandatory標誌位設定為true時,如果exchange根據自身型別和訊息routingKey無法找到一個合適的queue儲存訊息,那麼broker會呼叫basic.return方法將訊息返還給生產者;當mandatory設定為false時,出現上述情況broker會直接將訊息丟棄;通俗的講,mandatory標誌告訴broker代理伺服器至少將訊息route到一個佇列中,否則就將訊息return給傳送者;

當 noWait 為真時,客戶端不會等待響應。如果服務端不支援此方法,則可能會發生通道異常。

具體程式碼實現如下:

...

// 設定訊息確認
channel.Confirm(false)
confirms = channel.NotifyPublish(make(chan amqp.Confirmation, 1))

...

go func() {
        for confirm := range confirms {
            if confirm.Ack { // 訊息已確認
                fmt.Printf("confirmed delivery with delivery tag: %d \n", confirm.DeliveryTag)
            } else { // 未確認的訊息可以重新傳送
                fmt.Printf("failed confirmed delivery of delivery tag: %d \n", confirm.DeliveryTag)
            }
        }
    }()

...

消費者可靠性

// 將autoAck設定為false
func (ch *Channel) Consume(queue, consumer string, autoAck, exclusive, noLocal, noWait bool, args Table) (<-chan Delivery, error) {
    // When we return from ch.call, there may be a delivery already for the
    // consumer that hasn't been added to the consumer hash yet.  Because of
    // this, we never rely on the server picking a consumer tag for us.

    if err := args.Validate(); err != nil {
        return nil, err
    }

    if consumer == "" {
        consumer = uniqueConsumerTag()
    }

    req := &basicConsume{
        Queue:       queue,
        ConsumerTag: consumer,
        NoLocal:     noLocal,
        NoAck:       autoAck,
        Exclusive:   exclusive,
        NoWait:      noWait,
        Arguments:   args,
    }
    res := &basicConsumeOk{}

    deliveries := make(chan Delivery)

    ch.consumers.add(consumer, deliveries)

    if err := ch.call(req, res); err != nil {
        ch.consumers.cancel(consumer)
        return nil, err
    }

    return (<-chan Delivery)(deliveries), nil
}

立即開始消費排隊的訊息。

在 Connection 或 Channel 上的任何其他操作之前開始接收返回的 chan Delivery。

訊息會繼續往返回的 chan Delivery 傳遞,直到發生 Channel.Cancel、Connection.Close、Channel.Close 或 AMQP 異常。消費者必須在 chan 範圍內確保收到所有訊息。未收到的訊息將阻塞同一連線上的所有方法。

AMQP 中的所有訊息都必須得到確認。消費者在成功處理訊息後最好手動呼叫 Delivery.Ack。如果消費者被取消或通道或連線被關閉,任何未確認的訊息將在同一佇列的末尾重新入隊

消費者由一個字串標識,該字串是唯一的,適用於該channal上的所有消費者。如果希望最終取消消費者,請在 Channel.Cancel 中使用相同的非空識別符號。空字串將導致重新成唯一標識。消費者身份將包含在 ConsumerTag 欄位中的每個訊息中

當 autoAck(也稱為 noAck)為真時,伺服器將在將訊息寫入網路之前向該消費者確認確認。當 autoAck 為真時,消費者不應呼叫 Delivery.Ack。自動確認訊息意味著如果伺服器投遞訊息後消費者無法處理某些訊息,則可能會丟失某些訊息。

當exclusive 為true 時,伺服器將確保這是該佇列中的唯一消費者。當exclusive 為false 時,伺服器將在多個消費者之間公平地分發訊息。 RabbitMQ 不支援 noLocal 標誌。建議對 Channel.Publish 和 Channel.Consume 使用單獨的連線,以免在釋出時 TCP 回推影響消費訊息的能力,因此這裡主要是為了完整性。當 noWait 為 true 時,不要等待伺服器確認請求並立即開始消費。如果無法消費,則會引發通道異常並關閉通道。

消費訊息時,將autoAck設定為false

func (d Delivery) Ack(multiple bool) error {
    if d.Acknowledger == nil {
        return errDeliveryNotInitialized
    }
    return d.Acknowledger.Ack(d.DeliveryTag, multiple)
}

客戶端消費到訊息後,需要呼叫ack確認接收到訊息

AMQP 中的所有訊息的投遞都必須得到確認。如果使用 autoAck true 呼叫 Channel.Consume,那麼服務端將自動確認每條訊息,但是不應該呼叫此方法,因為這個不能保證消費端業務處理成功。所以,必須在成功處理訊息後呼叫 Delivery.Ack。當multiple 為真時,此訊息和同一通道上所有先前未確認的訊息將被確認,這對於訊息的批處理很有用(但是有個弊端就是,如果有一個出錯了,所有批處理的資料都需要重發)。對於每個未自動確認的訊息,都必須呼叫 Delivery.Ack、Delivery.Reject 或 Delivery.Nack

消費端的確認機制的實現:

...

//讀取資料
    if msgs, err = channel.Consume(queue.Name, "", false, false, false, false, nil); err != nil {
        fmt.Println("Consume Err =", err)
        return
    }
    go func() {
        for msg := range msgs {
            fmt.Println("Receive Msg =", string(msg.Body))
      // 確認訊息
            msg.Ack(false)
        }
    }()
...
本作品採用《CC 協議》,轉載必須註明作者和本文連結

相關文章