RabbitMQ備忘錄

Mugetsukun發表於2024-09-11

介紹

RabbitMQ是一個開源的訊息代理軟體,支援多種訊息協議。它允許不同的應用程式透過訊息佇列進行通訊,促進了系統之間的解耦和非同步處理。

1. 解耦

解耦是指將系統中的不同元件分離,使它們可以獨立開發和部署。RabbitMQ透過訊息佇列實現瞭解耦,生產者和消費者不需要直接知道彼此的存在。

2. 提速

RabbitMQ可以透過非同步處理來提高系統的響應速度。生產者可以將訊息傳送到佇列中,而消費者可以在後臺處理這些訊息,從而提高整體效能。

3. 削峰

削峰是指在高負載時透過訊息佇列平衡請求。RabbitMQ可以將突發的請求分散到一段時間內處理,避免系統過載。

4. 分發

RabbitMQ支援多種訊息分發模式,包括點對點和釋出/訂閱。可以根據需求選擇合適的模式來實現訊息的分發。

RabbitMQ安裝

docker run -d --name rabbitmq -p 5672:5672 -p 15672:15672 rabbitmq:management

開啟瀏覽器,訪問:http://localhost:15672,預設使用者名稱和密碼為guest

示例

1. 環境準備

go get github.com/streadway/amqp

2. 生產者程式碼

package main

import (
    "log"
    "github.com/streadway/amqp"
)

func main() {
    conn, err := amqp.Dial("amqp://guest:guest@localhost:5672/")
    if err != nil {
        log.Fatalf("Failed to connect to RabbitMQ: %s", err)
    }
    defer conn.Close()

    ch, err := conn.Channel()
    if err != nil {
        log.Fatalf("Failed to open a channel: %s", err)
    }
    defer ch.Close()

    q, err := ch.QueueDeclare(
        "task_queue", // 佇列名稱
        true,         // 持久化
        false,        // 排他
        false,        // 自動刪除
        false,        // 阻塞
        nil,          // 額外屬性
    )
    if err != nil {
        log.Fatalf("Failed to declare a queue: %s", err)
    }

    for i := 0; i < 10; i++ {
        body := "Task " + strconv.Itoa(i)
        err = ch.Publish(
            "",     // 預設交換機
            q.Name, // 佇列名稱
            false,  // 強制傳送
            false,  // 立即傳送
            amqp.Publishing{
                ContentType: "text/plain",
                Body:        []byte(body),
            })
        if err != nil {
            log.Fatalf("Failed to publish a message: %s", err)
        }
        log.Printf("Sent %s", body)
    }
}

3. 消費者程式碼

package main

import (
    "log"
    "time"
    "github.com/streadway/amqp"
)

func main() {
    conn, err := amqp.Dial("amqp://guest:guest@localhost:5672/")
    if err != nil {
        log.Fatalf("Failed to connect to RabbitMQ: %s", err)
    }
    defer conn.Close()

    ch, err := conn.Channel()
    if err != nil {
        log.Fatalf("Failed to open a channel: %s", err)
    }
    defer ch.Close()

    q, err := ch.QueueDeclare(
        "task_queue", // 佇列名稱
        true,         // 持久化
        false,        // 排他
        false,        // 自動刪除
        false,        // 阻塞
        nil,          // 額外屬性
    )
    if err != nil {
        log.Fatalf("Failed to declare a queue: %s", err)
    }

    msgs, err := ch.Consume(
        q.Name, // 佇列名稱
        "",     // 消費者名稱
        false,  // 自動確認
        false,  // 排他
        false,  // 阻塞
        false,  // 優先
        nil,    // 額外屬性
    )
    if err != nil {
        log.Fatalf("Failed to register a consumer: %s", err)
    }

    go func() {
        for d := range msgs {
            log.Printf("Received a message: %s", d.Body)
            time.Sleep(2 * time.Second) // 模擬處理時間
            log.Printf("Done processing message: %s", d.Body)
            d.Ack(false) // 手動確認
        }
    }()

    log.Println("Waiting for messages. To exit press CTRL+C")
    select {}
}

執行示例

  1. 啟動RabbitMQ服務。
  2. 執行消費者程式。
  3. 執行生產者程式。

簡單模式下發布者釋出訊息,消費者消費訊息

環境準備

docker run -d --name rabbitmq -p 5672:5672 -p 15672:15672 rabbitmq:management
go get github.com/streadway/amqp

示例

1. 生產者程式碼

package main

import (
    "log"
    "os"
    "github.com/streadway/amqp"
)

func main() {
    // 連線到RabbitMQ
    conn, err := amqp.Dial("amqp://guest:guest@localhost:5672/")
    if err != nil {
        log.Fatalf("Failed to connect to RabbitMQ: %s", err)
    }
    defer conn.Close()

    // 建立通道
    ch, err := conn.Channel()
    if err != nil {
        log.Fatalf("Failed to open a channel: %s", err)
    }
    defer ch.Close()

    // 宣告佇列
    q, err := ch.QueueDeclare(
        "simple_queue", // 佇列名稱
        false,          // 是否持久化
        false,          // 是否排他
        false,          // 是否自動刪除
        false,          // 是否阻塞
        nil,            // 額外屬性
    )
    if err != nil {
        log.Fatalf("Failed to declare a queue: %s", err)
    }

    // 傳送訊息
    body := "Hello, RabbitMQ!"
    err = ch.Publish(
        "",     // 預設交換機
        q.Name, // 佇列名稱
        false,  // 強制傳送
        false,  // 立即傳送
        amqp.Publishing{
            ContentType: "text/plain",
            Body:        []byte(body),
        })
    if err != nil {
        log.Fatalf("Failed to publish a message: %s", err)
    }

    log.Printf("Sent %s", body)
}

2. 消費者程式碼

package main

import (
    "log"
    "github.com/streadway/amqp"
)

func main() {
    // 連線到RabbitMQ
    conn, err := amqp.Dial("amqp://guest:guest@localhost:5672/")
    if err != nil {
        log.Fatalf("Failed to connect to RabbitMQ: %s", err)
    }
    defer conn.Close()

    // 建立通道
    ch, err := conn.Channel()
    if err != nil {
        log.Fatalf("Failed to open a channel: %s", err)
    }
    defer ch.Close()

    // 宣告佇列
    q, err := ch.QueueDeclare(
        "simple_queue", // 佇列名稱
        false,          // 是否持久化
        false,          // 是否排他
        false,          // 是否自動刪除
        false,          // 是否阻塞
        nil,            // 額外屬性
    )
    if err != nil {
        log.Fatalf("Failed to declare a queue: %s", err)
    }

    // 消費訊息
    msgs, err := ch.Consume(
        q.Name, // 佇列名稱
        "",     // 消費者名稱
        true,   // 自動確認
        false,  // 排他
        false,  // 阻塞
        false,  // 優先
        nil,    // 額外屬性
    )
    if err != nil {
        log.Fatalf("Failed to register a consumer: %s", err)
    }

    // 處理訊息
    go func() {
        for d := range msgs {
            log.Printf("Received a message: %s", d.Body)
        }
    }()

    log.Println("Waiting for messages. To exit press CTRL+C")
    select {}
}

執行示例

  1. 啟動RabbitMQ服務。
  2. 先執行消費者程式,確保它在等待訊息。
  3. 然後執行生產者程式,傳送訊息。

工作模式下傳送消費訊息手動確認資訊

環境準備

確保你已經安裝了RabbitMQ並且可以訪問管理介面。如果你還沒有安裝,可以使用以下Docker命令:

docker run -d --name rabbitmq -p 5672:5672 -p 15672:15672 rabbitmq:management
go get github.com/streadway/amqp

程式碼示例

1. 生產者程式碼

package main

import (
    "log"
    "strconv"
    "github.com/streadway/amqp"
)

func main() {
    // 連線到RabbitMQ
    conn, err := amqp.Dial("amqp://guest:guest@localhost:5672/")
    if err != nil {
        log.Fatalf("Failed to connect to RabbitMQ: %s", err)
    }
    defer conn.Close()

    // 建立通道
    ch, err := conn.Channel()
    if err != nil {
        log.Fatalf("Failed to open a channel: %s", err)
    }
    defer ch.Close()

    // 宣告佇列
    q, err := ch.QueueDeclare(
        "work_queue", // 佇列名稱
        true,         // 是否持久化
        false,        // 是否排他
        false,        // 是否自動刪除
        false,        // 是否阻塞
        nil,          // 額外屬性
    )
    if err != nil {
        log.Fatalf("Failed to declare a queue: %s", err)
    }

    // 傳送訊息
    for i := 0; i < 10; i++ {
        body := "Task " + strconv.Itoa(i)
        err = ch.Publish(
            "",     // 預設交換機
            q.Name, // 佇列名稱
            false,  // 強制傳送
            false,  // 立即傳送
            amqp.Publishing{
                ContentType: "text/plain",
                Body:        []byte(body),
            })
        if err != nil {
            log.Fatalf("Failed to publish a message: %s", err)
        }
        log.Printf("Sent %s", body)
    }
}

2. 消費者程式碼

package main

import (
    "log"
    "time"
    "github.com/streadway/amqp"
)

func main() {
    // 連線到RabbitMQ
    conn, err := amqp.Dial("amqp://guest:guest@localhost:5672/")
    if err != nil {
        log.Fatalf("Failed to connect to RabbitMQ: %s", err)
    }
    defer conn.Close()

    // 建立通道
    ch, err := conn.Channel()
    if err != nil {
        log.Fatalf("Failed to open a channel: %s", err)
    }
    defer ch.Close()

    // 宣告佇列
    q, err := ch.QueueDeclare(
        "work_queue", // 佇列名稱
        true,         // 是否持久化
        false,        // 是否排他
        false,        // 是否自動刪除
        false,        // 是否阻塞
        nil,          // 額外屬性
    )
    if err != nil {
        log.Fatalf("Failed to declare a queue: %s", err)
    }

    // 消費訊息
    msgs, err := ch.Consume(
        q.Name, // 佇列名稱
        "",     // 消費者名稱
        false,  // 自動確認
        false,  // 排他
        false,  // 阻塞
        false,  // 優先
        nil,    // 額外屬性
    )
    if err != nil {
        log.Fatalf("Failed to register a consumer: %s", err)
    }

    // 處理訊息
    go func() {
        for d := range msgs {
            log.Printf("Received a message: %s", d.Body)
            // 模擬處理時間
            time.Sleep(2 * time.Second)
            log.Printf("Done processing message: %s", d.Body)
            // 手動確認訊息
            d.Ack(false)
        }
    }()

    log.Println("Waiting for messages. To exit press CTRL+C")
    select {}
}

執行示例

  1. 啟動RabbitMQ服務。
  2. 先執行消費者程式,確保它在等待訊息。
  3. 然後執行生產者程式,傳送訊息。

Publist、Subscribe釋出訂閱模式下傳送消費訊息獲取執行程式傳遞的引數args

環境準備

docker run -d --name rabbitmq -p 5672:5672 -p 15672:15672 rabbitmq:management
go get github.com/streadway/amqp

程式碼示例

1. 釋出者程式碼

package main

import (
    "log"
    "os"
    "github.com/streadway/amqp"
)

func main() {
    // 連線到RabbitMQ
    conn, err := amqp.Dial("amqp://guest:guest@localhost:5672/")
    if err != nil {
        log.Fatalf("Failed to connect to RabbitMQ: %s", err)
    }
    defer conn.Close()

    // 建立通道
    ch, err := conn.Channel()
    if err != nil {
        log.Fatalf("Failed to open a channel: %s", err)
    }
    defer ch.Close()

    // 宣告交換機
    err = ch.ExchangeDeclare(
        "logs",    // 交換機名稱
        "fanout",  // 交換機型別
        true,      // 是否持久化
        false,     // 是否排他
        false,     // 是否自動刪除
        false,     // 是否阻塞
        nil,       // 額外屬性
    )
    if err != nil {
        log.Fatalf("Failed to declare an exchange: %s", err)
    }

    // 從命令列獲取訊息內容
    body := "Hello, RabbitMQ!"
    if len(os.Args) > 1 {
        body = os.Args[1]
    }

    // 釋出訊息
    err = ch.Publish(
        "logs", // 交換機名稱
        "",     // 路由鍵
        false,  // 強制傳送
        false,  // 立即傳送
        amqp.Publishing{
            ContentType: "text/plain",
            Body:        []byte(body),
        })
    if err != nil {
        log.Fatalf("Failed to publish a message: %s", err)
    }

    log.Printf("Sent %s", body)
}

2. 消費者程式碼

package main

import (
    "log"
    "github.com/streadway/amqp"
)

func main() {
    // 連線到RabbitMQ
    conn, err := amqp.Dial("amqp://guest:guest@localhost:5672/")
    if err != nil {
        log.Fatalf("Failed to connect to RabbitMQ: %s", err)
    }
    defer conn.Close()

    // 建立通道
    ch, err := conn.Channel()
    if err != nil {
        log.Fatalf("Failed to open a channel: %s", err)
    }
    defer ch.Close()

    // 宣告交換機
    err = ch.ExchangeDeclare(
        "logs",    // 交換機名稱
        "fanout",  // 交換機型別
        true,      // 是否持久化
        false,     // 是否排他
        false,     // 是否自動刪除
        false,     // 是否阻塞
        nil,       // 額外屬性
    )
    if err != nil {
        log.Fatalf("Failed to declare an exchange: %s", err)
    }

    // 宣告佇列
    q, err := ch.QueueDeclare(
        "",    // 隨機佇列名稱
        false, // 是否持久化
        false, // 是否排他
        true,  // 是否自動刪除
        false, // 是否阻塞
        nil,   // 額外屬性
    )
    if err != nil {
        log.Fatalf("Failed to declare a queue: %s", err)
    }

    // 繫結佇列到交換機
    err = ch.QueueBind(
        q.Name, // 佇列名稱
        "",     // 路由鍵
        "logs", // 交換機名稱
        false,
        nil)
    if err != nil {
        log.Fatalf("Failed to bind a queue: %s", err)
    }

    // 消費訊息
    msgs, err := ch.Consume(
        q.Name, // 佇列名稱
        "",     // 消費者名稱
        true,   // 自動確認
        false,  // 排他
        false,  // 阻塞
        false,  // 優先
        nil,    // 額外屬性
    )
    if err != nil {
        log.Fatalf("Failed to register a consumer: %s", err)
    }

    // 處理訊息
    go func() {
        for d := range msgs {
            log.Printf("Received a message: %s", d.Body)
        }
    }()

    log.Println("Waiting for messages. To exit press CTRL+C")
    select {}
}

執行示例

  1. 啟動RabbitMQ服務。
  2. 先執行消費者程式,確保它在等待訊息。
  3. 然後執行釋出者程式,傳遞要傳送的訊息。例如:
go run publisher.go "Hello, World!"

路由模式下傳送消費訊息

環境準備

docker run -d --name rabbitmq -p 5672:5672 -p 15672:15672 rabbitmq:management
go get github.com/streadway/amqp

程式碼示例

1. 生產者程式碼

package main

import (
    "log"
    "os"
    "github.com/streadway/amqp"
)

func main() {
    // 連線到RabbitMQ
    conn, err := amqp.Dial("amqp://guest:guest@localhost:5672/")
    if err != nil {
        log.Fatalf("Failed to connect to RabbitMQ: %s", err)
    }
    defer conn.Close()

    // 建立通道
    ch, err := conn.Channel()
    if err != nil {
        log.Fatalf("Failed to open a channel: %s", err)
    }
    defer ch.Close()

    // 宣告交換機
    err = ch.ExchangeDeclare(
        "direct_logs", // 交換機名稱
        "direct",      // 交換機型別
        true,          // 是否持久化
        false,         // 是否排他
        false,         // 是否自動刪除
        false,         // 是否阻塞
        nil,           // 額外屬性
    )
    if err != nil {
        log.Fatalf("Failed to declare an exchange: %s", err)
    }

    // 從命令列獲取路由鍵和訊息內容
    if len(os.Args) < 3 {
        log.Fatalf("Usage: %s <severity> <message>", os.Args[0])
    }
    severity := os.Args[1]
    body := os.Args[2]

    // 釋出訊息
    err = ch.Publish(
        "direct_logs", // 交換機名稱
        severity,      // 路由鍵
        false,         // 強制傳送
        false,         // 立即傳送
        amqp.Publishing{
            ContentType: "text/plain",
            Body:        []byte(body),
        })
    if err != nil {
        log.Fatalf("Failed to publish a message: %s", err)
    }

    log.Printf("Sent [%s] %s", severity, body)
}

2. 消費者程式碼

package main

import (
    "log"
    "github.com/streadway/amqp"
)

func main() {
    // 連線到RabbitMQ
    conn, err := amqp.Dial("amqp://guest:guest@localhost:5672/")
    if err != nil {
        log.Fatalf("Failed to connect to RabbitMQ: %s", err)
    }
    defer conn.Close()

    // 建立通道
    ch, err := conn.Channel()
    if err != nil {
        log.Fatalf("Failed to open a channel: %s", err)
    }
    defer ch.Close()

    // 宣告交換機
    err = ch.ExchangeDeclare(
        "direct_logs", // 交換機名稱
        "direct",      // 交換機型別
        true,          // 是否持久化
        false,         // 是否排他
        false,         // 是否自動刪除
        false,         // 是否阻塞
        nil,           // 額外屬性
    )
    if err != nil {
        log.Fatalf("Failed to declare an exchange: %s", err)
    }

    // 宣告佇列
    q, err := ch.QueueDeclare(
        "",    // 隨機佇列名稱
        false, // 是否持久化
        false, // 是否排他
        true,  // 是否自動刪除
        false, // 是否阻塞
        nil,   // 額外屬性
    )
    if err != nil {
        log.Fatalf("Failed to declare a queue: %s", err)
    }

    // 繫結佇列到交換機,指定路由鍵
    severity := "info" // 你可以根據需要更改這個值
    err = ch.QueueBind(
        q.Name,   // 佇列名稱
        severity, // 路由鍵
        "direct_logs", // 交換機名稱
        false,
        nil)
    if err != nil {
        log.Fatalf("Failed to bind a queue: %s", err)
    }

    // 消費訊息
    msgs, err := ch.Consume(
        q.Name, // 佇列名稱
        "",     // 消費者名稱
        true,   // 自動確認
        false,  // 排他
        false,  // 阻塞
        false,  // 優先
        nil,    // 額外屬性
    )
    if err != nil {
        log.Fatalf("Failed to register a consumer: %s", err)
    }

    // 處理訊息
    go func() {
        for d := range msgs {
            log.Printf("Received [%s] %s", d.RoutingKey, d.Body)
        }
    }()

    log.Println("Waiting for messages. To exit press CTRL+C")
    select {}
}

執行示例

  1. 啟動RabbitMQ服務。
  2. 先執行消費者程式,確保它在等待訊息。
go run consumer.go
  1. 然後執行生產者程式,傳遞路由鍵和要傳送的訊息。
go run producer.go info "This is an info message"
go run producer.go error "This is an error message"

主題訂閱模式和RPC模式

1. 主題生產者程式碼

package main

import (
    "log"
    "os"
    "github.com/streadway/amqp"
)

func main() {
    // 連線到RabbitMQ
    conn, err := amqp.Dial("amqp://guest:guest@localhost:5672/")
    if err != nil {
        log.Fatalf("Failed to connect to RabbitMQ: %s", err)
    }
    defer conn.Close()

    // 建立通道
    ch, err := conn.Channel()
    if err != nil {
        log.Fatalf("Failed to open a channel: %s", err)
    }
    defer ch.Close()

    // 宣告主題交換機
    err = ch.ExchangeDeclare(
        "topic_logs", // 交換機名稱
        "topic",      // 交換機型別
        true,         // 是否持久化
        false,        // 是否排他
        false,        // 是否自動刪除
        false,        // 是否阻塞
        nil,          // 額外屬性
    )
    if err != nil {
        log.Fatalf("Failed to declare an exchange: %s", err)
    }

    // 從命令列獲取路由鍵和訊息內容
    if len(os.Args) < 3 {
        log.Fatalf("Usage: %s <routing_key> <message>", os.Args[0])
    }
    routingKey := os.Args[1]
    body := os.Args[2]

    // 釋出訊息
    err = ch.Publish(
        "topic_logs", // 交換機名稱
        routingKey,   // 路由鍵
        false,        // 強制傳送
        false,        // 立即傳送
        amqp.Publishing{
            ContentType: "text/plain",
            Body:        []byte(body),
        })
    if err != nil {
        log.Fatalf("Failed to publish a message: %s", err)
    }

    log.Printf("Sent [%s] %s", routingKey, body)
}

2. 主題消費者程式碼

package main

import (
    "log"
    "github.com/streadway/amqp"
)

func main() {
    // 連線到RabbitMQ
    conn, err := amqp.Dial("amqp://guest:guest@localhost:5672/")
    if err != nil {
        log.Fatalf("Failed to connect to RabbitMQ: %s", err)
    }
    defer conn.Close()

    // 建立通道
    ch, err := conn.Channel()
    if err != nil {
        log.Fatalf("Failed to open a channel: %s", err)
    }
    defer ch.Close()

    // 宣告主題交換機
    err = ch.ExchangeDeclare(
        "topic_logs", // 交換機名稱
        "topic",      // 交換機型別
        true,         // 是否持久化
        false,        // 是否排他
        false,        // 是否自動刪除
        false,        // 是否阻塞
        nil,          // 額外屬性
    )
    if err != nil {
        log.Fatalf("Failed to declare an exchange: %s", err)
    }

    // 宣告佇列
    q, err := ch.QueueDeclare(
        "",    // 隨機佇列名稱
        false, // 是否持久化
        false, // 是否排他
        true,  // 是否自動刪除
        false, // 是否阻塞
        nil,   // 額外屬性
    )
    if err != nil {
        log.Fatalf("Failed to declare a queue: %s", err)
    }

    // 繫結佇列到交換機,指定路由鍵模式
    bindingKey := "#.info" // 你可以根據需要更改這個值
    err = ch.QueueBind(
        q.Name,      // 佇列名稱
        bindingKey,  // 路由鍵模式
        "topic_logs", // 交換機名稱
        false,
        nil)
    if err != nil {
        log.Fatalf("Failed to bind a queue: %s", err)
    }

    // 消費訊息
    msgs, err := ch.Consume(
        q.Name, // 佇列名稱
        "",     // 消費者名稱
        true,   // 自動確認
        false,  // 排他
        false,  // 阻塞
        false,  // 優先
        nil,    // 額外屬性
    )
    if err != nil {
        log.Fatalf("Failed to register a consumer: %s", err)
    }

    // 處理訊息
    go func() {
        for d := range msgs {
            log.Printf("Received [%s] %s", d.RoutingKey, d.Body)
        }
    }()

    log.Println("Waiting for messages. To exit press CTRL+C")
    select {}
}

執行主題示例

  1. 啟動RabbitMQ服務。
  2. 先執行消費者程式,確保它在等待訊息。例如,執行以下命令以接收所有“info”級別的訊息:
go run consumer.go
  1. 然後執行生產者程式,傳遞路由鍵和要傳送的訊息。例如:
go run producer.go "quick.info" "This is an info message"
go run producer.go "lazy.error" "This is an error message"

你將看到消費者接收到與其繫結的路由鍵匹配的訊息。


二、RPC模式

1. RPC 服務端程式碼

package main

import (
    "log"
    "strconv"
    "github.com/streadway/amqp"
)

func fib(n int) int {
    if n <= 0 {
        return 0
    }
    if n == 1 {
        return 1
    }
    return fib(n-1) + fib(n-2)
}

func main() {
    // 連線到RabbitMQ
    conn, err := amqp.Dial("amqp://guest:guest@localhost:5672/")
    if err != nil {
        log.Fatalf("Failed to connect to RabbitMQ: %s", err)
    }
    defer conn.Close()

    // 建立通道
    ch, err := conn.Channel()
    if err != nil {
        log.Fatalf("Failed to open a channel: %s", err)
    }
    defer ch.Close()

    // 宣告佇列
    q, err := ch.QueueDeclare(
        "rpc_queue", // 佇列名稱
        false,       // 是否持久化
        false,       // 是否排他
        false,       // 是否自動刪除
        false,       // 是否阻塞
        nil,         // 額外屬性
    )
    if err != nil {
        log.Fatalf("Failed to declare a queue: %s", err)
    }

    // 處理請求
    msgs, err := ch.Consume(
        q.Name, // 佇列名稱
        "",     // 消費者名稱
        true,   // 自動確認
        false,  // 排他
        false,  // 阻塞
        false,  // 優先
        nil,    // 額外屬性
    )
    if err != nil {
        log.Fatalf("Failed to register a consumer: %s", err)
    }

    log.Println("Awaiting RPC requests")

    for d := range msgs {
        n, err := strconv.Atoi(string(d.Body))
        if err != nil {
            log.Printf("Failed to convert body to int: %s", err)
            continue
        }
        log.Printf("Calculating fib(%d)", n)
        response := fib(n)

        // 釋出響應
        ch.Publish(
            "",          // 交換機
            d.ReplyTo,   // 路由鍵
            false,       // 強制傳送
            false,       // 立即傳送
            amqp.Publishing{
                CorrelationId: d.CorrelationId,
                Body:          []byte(strconv.Itoa(response)),
            })
    }
}

2. RPC 客戶端程式碼

package main

import (
    "log"
    "os"
    "strconv"
    "github.com/streadway/amqp"
)

func main() {
    // 連線到RabbitMQ
    conn, err := amqp.Dial("amqp://guest:guest@localhost:5672/")
    if err != nil {
        log.Fatalf("Failed to connect to RabbitMQ: %s", err)
    }
    defer conn.Close()

    // 建立通道
    ch, err := conn.Channel()
    if err != nil {
        log.Fatalf("Failed to open a channel: %s", err)
    }
    defer ch.Close()

    // 宣告一個隨機的響應佇列
    replyQueue, err := ch.QueueDeclare(
        "",    // 隨機佇列名稱
        false, // 是否持久化
        false, // 是否排他
        true,  // 是否自動刪除
        false, // 是否阻塞
        nil,   // 額外屬性
    )
    if err != nil {
        log.Fatalf("Failed to declare a reply queue: %s", err)
    }

    // 消費響應訊息
    corrId := ""
    msgs, err := ch.Consume(
        replyQueue.Name, // 佇列名稱
        "",              // 消費者名稱
        true,           // 自動確認
        false,          // 排他
        false,          // 阻塞
        false,          // 優先
        nil,            // 額外屬性
    )
    if err != nil {
        log.Fatalf("Failed to register a consumer: %s", err)
    }

    if len(os.Args) < 2 {
        log.Fatalf("Usage: %s <n>", os.Args[0])
    }
    n, err := strconv.Atoi(os.Args[1])
    if err != nil {
        log.Fatalf("Invalid argument: %s", os.Args[1])
    }

    // 傳送請求
    corrId = randomString(32)
    err = ch.Publish(
        "",          // 交換機
        "rpc_queue", // 路由鍵
        false,       // 強制傳送
        false,       // 立即傳送
        amqp.Publishing{
            CorrelationId: corrId,
            ReplyTo:       replyQueue.Name,
            Body:         []byte(strconv.Itoa(n)),
        })
    if err != nil {
        log.Fatalf("Failed to publish a message: %s", err)
    }

    // 等待響應
    for d := range msgs {
        if d.CorrelationId == corrId {
            log.Printf("Got response: %s", d.Body)
            break
        }
    }
}

// randomString 生成一個隨機字串
func randomString(n int) string {
    letters := []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789")
    b := make([]rune, n)
    for i := range b {
        b[i] = letters[rand.Intn(len(letters))]
    }
    return string(b)
}

執行RPC示例

  1. 啟動RabbitMQ服務。
  2. 啟動RPC服務端:
go run rpc_server.go
  1. 啟動RPC客戶端,傳遞要計算的斐波那契數:
go run rpc_client.go 10

可靠性、資料持久化、消費端限流、消費者確認和訊息過期處理

在RabbitMQ中,確保訊息的可靠性和資料的永續性是非常重要的。我們可以透過以下幾個方面來實現這些目標:

  1. 訊息持久化:確保訊息在RabbitMQ重啟後仍然存在。
  2. 消費端限流:控制消費者的訊息處理速率。
  3. 消費者確認:確保訊息被成功處理後再從佇列中移除。
  4. 訊息過期處理:設定訊息的過期時間,超時後自動刪除。

1. 訊息持久化

生產者程式碼(持久化訊息)

package main

import (
    "log"
    "os"
    "github.com/streadway/amqp"
)

func main() {
    // 連線到RabbitMQ
    conn, err := amqp.Dial("amqp://guest:guest@localhost:5672/")
    if err != nil {
        log.Fatalf("Failed to connect to RabbitMQ: %s", err)
    }
    defer conn.Close()

    // 建立通道
    ch, err := conn.Channel()
    if err != nil {
        log.Fatalf("Failed to open a channel: %s", err)
    }
    defer ch.Close()

    // 宣告持久化佇列
    q, err := ch.QueueDeclare(
        "durable_queue", // 佇列名稱
        true,            // 是否持久化
        false,           // 是否排他
        false,           // 是否自動刪除
        false,           // 是否阻塞
        nil,             // 額外屬性
    )
    if err != nil {
        log.Fatalf("Failed to declare a queue: %s", err)
    }

    // 從命令列獲取訊息內容
    if len(os.Args) < 2 {
        log.Fatalf("Usage: %s <message>", os.Args[0])
    }
    body := os.Args[1]

    // 釋出持久化訊息
    err = ch.Publish(
        "",              // 交換機
        q.Name,         // 路由鍵
        false,          // 強制傳送
        false,          // 立即傳送
        amqp.Publishing{
            ContentType: "text/plain",
            Body:        []byte(body),
            DeliveryMode: amqp.Persistent, // 設定訊息持久化
        })
    if err != nil {
        log.Fatalf("Failed to publish a message: %s", err)
    }

    log.Printf("Sent %s", body)
}

2. 消費端限流

可以透過設定prefetch來控制消費者在確認之前可以處理的訊息數量。

消費者程式碼(限流)

package main

import (
    "log"
    "github.com/streadway/amqp"
)

func main() {
    // 連線到RabbitMQ
    conn, err := amqp.Dial("amqp://guest:guest@localhost:5672/")
    if err != nil {
        log.Fatalf("Failed to connect to RabbitMQ: %s", err)
    }
    defer conn.Close()

    // 建立通道
    ch, err := conn.Channel()
    if err != nil {
        log.Fatalf("Failed to open a channel: %s", err)
    }
    defer ch.Close()

    // 宣告持久化佇列
    _, err = ch.QueueDeclare(
        "durable_queue", // 佇列名稱
        true,            // 是否持久化
        false,           // 是否排他
        false,           // 是否自動刪除
        false,           // 是否阻塞
        nil,             // 額外屬性
    )
    if err != nil {
        log.Fatalf("Failed to declare a queue: %s", err)
    }

    // 設定限流
    err = ch.Qos(
        1,    // 每次只處理一條訊息
        0,    // 不限制訊息大小
        false, // 不阻塞
    )
    if err != nil {
        log.Fatalf("Failed to set QoS: %s", err)
    }

    // 消費訊息
    msgs, err := ch.Consume(
        "durable_queue", // 佇列名稱
        "",              // 消費者名稱
        false,          // 自動確認
        false,          // 排他
        false,          // 阻塞
        false,          // 優先
        nil,            // 額外屬性
    )
    if err != nil {
        log.Fatalf("Failed to register a consumer: %s", err)
    }

    // 處理訊息
    go func() {
        for d := range msgs {
            log.Printf("Received %s", d.Body)
            // 模擬處理時間
            // time.Sleep(2 * time.Second)
            d.Ack(false) // 手動確認
        }
    }()

    log.Println("Waiting for messages. To exit press CTRL+C")
    select {}
}

3. 消費者確認

在消費者處理完訊息後,可以透過手動確認來確保訊息被成功處理。
在上面的消費者程式碼中,d.Ack(false)就是手動確認的實現。

4. 訊息過期處理

可以透過設定佇列的x-message-ttl屬性來設定訊息的過期時間。

修改消費者程式碼以支援訊息過期

package main

import (
    "log"
    "github.com/streadway/amqp"
)

func main() {
    // 連線到RabbitMQ
    conn, err := amqp.Dial("amqp://guest:guest@localhost:5672/")
    if err != nil {
        log.Fatalf("Failed to connect to RabbitMQ: %s", err)
    }
    defer conn.Close()

    // 建立通道
    ch, err := conn.Channel()
    if err != nil {
        log.Fatalf("Failed to open a channel: %s", err)
    }
    defer ch.Close()

    // 宣告持久化佇列並設定訊息過期時間
    _, err = ch.QueueDeclare(
        "durable_queue", // 佇列名稱
        true,            // 是否持久化
        false,           // 是否排他
        false,           // 是否自動刪除
        false,           // 是否阻塞
        amqp.Table{
            "x-message-ttl": 10000, // 訊息過期時間(毫秒)
        },
    )
    if err != nil {
        log.Fatalf("Failed to declare a queue: %s", err)
    }

    // 消費訊息
    msgs, err := ch.Consume(
        "durable_queue", // 佇列名稱
        "",              // 消費者名稱
        false,          // 自動確認
        false,          // 排他
        false,          // 阻塞
        false,          // 優先
        nil,            // 額外屬性
    )
    if err != nil {
        log.Fatalf("Failed to register a consumer: %s", err)
    }

    // 處理訊息
    go func() {
        for d := range msgs {
            log.Printf("Received %s", d.Body)
            d.Ack(false) // 手動確認
        }
    }()

    log.Println("Waiting for messages. To exit press CTRL+C")
    select {}
}

執行示例

  1. 啟動RabbitMQ服務。
  2. 啟動生產者,傳送持久化訊息:
go run producer.go "Hello World!"
  1. 啟動消費者,處理訊息並設定限流和過期時間:
go run consumer.go

實現高併發秒殺、搶購、預約、訂票系統

系統設計

  1. 商品庫存管理:使用資料庫或記憶體來管理商品庫存。
  2. RabbitMQ作為訊息佇列:將使用者請求放入RabbitMQ佇列中,由消費者處理實際的庫存扣減操作。
  3. 併發控制:透過訊息佇列來控制併發,確保在高併發情況下不會超賣。
  4. 消費者確認:確保每個請求被成功處理後再從佇列中移除。

程式碼實現

1. 商品庫存管理

package main

import (
    "sync"
)

type Product struct {
    ID       string
    Quantity int
}

var inventory = map[string]*Product{
    "product_1": {ID: "product_1", Quantity: 10},
}

var mu sync.Mutex

func reduceStock(productID string) bool {
    mu.Lock()
    defer mu.Unlock()

    product, exists := inventory[productID]
    if !exists || product.Quantity <= 0 {
        return false
    }

    product.Quantity--
    return true
}

2. 生產者程式碼

package main

import (
    "log"
    "os"
    "github.com/streadway/amqp"
)

func sendRequest(productID string) {
    conn, err := amqp.Dial("amqp://guest:guest@localhost:5672/")
    if err != nil {
        log.Fatalf("Failed to connect to RabbitMQ: %s", err)
    }
    defer conn.Close()

    ch, err := conn.Channel()
    if err != nil {
        log.Fatalf("Failed to open a channel: %s", err)
    }
    defer ch.Close()

    // 宣告佇列
    _, err = ch.QueueDeclare(
        "order_queue", // 佇列名稱
        true,          // 是否持久化
        false,         // 是否排他
        false,         // 是否自動刪除
        false,         // 是否阻塞
        nil,           // 額外屬性
    )
    if err != nil {
        log.Fatalf("Failed to declare a queue: %s", err)
    }

    // 釋出訊息
    err = ch.Publish(
        "",              // 交換機
        "order_queue",   // 佇列名稱
        false,          // 強制傳送
        false,          // 立即傳送
        amqp.Publishing{
            ContentType: "text/plain",
            Body:        []byte(productID),
        })
    if err != nil {
        log.Fatalf("Failed to publish a message: %s", err)
    }

    log.Printf("Sent request for %s", productID)
}

func main() {
    if len(os.Args) < 2 {
        log.Fatalf("Usage: %s <product_id>", os.Args[0])
    }
    productID := os.Args[1]
    sendRequest(productID)
}

3. 消費者程式碼

package main

import (
    "log"
    "github.com/streadway/amqp"
)

func main() {
    conn, err := amqp.Dial("amqp://guest:guest@localhost:5672/")
    if err != nil {
        log.Fatalf("Failed to connect to RabbitMQ: %s", err)
    }
    defer conn.Close()

    ch, err := conn.Channel()
    if err != nil {
        log.Fatalf("Failed to open a channel: %s", err)
    }
    defer ch.Close()

    // 宣告佇列
    _, err = ch.QueueDeclare(
        "order_queue", // 佇列名稱
        true,          // 是否持久化
        false,         // 是否排他
        false,         // 是否自動刪除
        false,         // 是否阻塞
        nil,           // 額外屬性
    )
    if err != nil {
        log.Fatalf("Failed to declare a queue: %s", err)
    }

    msgs, err := ch.Consume(
        "order_queue", // 佇列名稱
        "",            // 消費者名稱
        false,         // 自動確認
        false,         // 排他
        false,         // 阻塞
        false,         // 優先
        nil,           // 額外屬性
    )
    if err != nil {
        log.Fatalf("Failed to register a consumer: %s", err)
    }

    go func() {
        for d := range msgs {
            productID := string(d.Body)
            log.Printf("Received request for %s", productID)

            if reduceStock(productID) {
                log.Printf("Successfully purchased %s", productID)
                d.Ack(false) // 手動確認
            } else {
                log.Printf("Failed to purchase %s: out of stock", productID)
                d.Nack(false, false) // 拒絕訊息,不重新入隊
            }
        }
    }()

    log.Println("Waiting for messages. To exit press CTRL+C")
    select {}
}

執行示例

  1. 啟動RabbitMQ服務。
  2. 啟動消費者:
go run consumer.go
  1. 啟動多個生產者模擬使用者請求:
go run producer.go product_1

使用Gin+PostgreSQL解決高併發增加資料問題、以及使用RabbitMQ結合PostgreSQL最佳化。

環境準備

  1. 安裝Go:確保你已經安裝了Go語言。
  2. 安裝Gin:使用以下命令安裝Gin框架:
    go get -u github.com/gin-gonic/gin
    
  3. 安裝PostgreSQL:確保你已經安裝並執行PostgreSQL。
  4. 安裝RabbitMQ:確保你已經安裝並執行RabbitMQ。
  5. 安裝PostgreSQL驅動
    go get -u github.com/lib/pq
    
  6. 安裝RabbitMQ驅動
    go get -u github.com/streadway/amqp
    

資料庫設定

CREATE DATABASE testdb;

\c testdb

CREATE TABLE records (
    id SERIAL PRIMARY KEY,
    data TEXT NOT NULL
);

程式碼實現

1. 資料庫連線

package db

import (
    "database/sql"
    "log"

    _ "github.com/lib/pq"
)

var DB *sql.DB

func InitDB() {
    var err error
    connStr := "user=yourusername dbname=testdb sslmode=disable"
    DB, err = sql.Open("postgres", connStr)
    if err != nil {
        log.Fatal(err)
    }

    if err = DB.Ping(); err != nil {
        log.Fatal(err)
    }
}

2. RabbitMQ連線

package rabbitmq

import (
    "log"

    "github.com/streadway/amqp"
)

var Channel *amqp.Channel

func InitRabbitMQ() {
    conn, err := amqp.Dial("amqp://guest:guest@localhost:5672/")
    if err != nil {
        log.Fatalf("Failed to connect to RabbitMQ: %s", err)
    }

    Channel, err = conn.Channel()
    if err != nil {
        log.Fatalf("Failed to open a channel: %s", err)
    }

    _, err = Channel.QueueDeclare(
        "task_queue", // 佇列名稱
        true,         // 是否持久化
        false,        // 是否排他
        false,        // 是否自動刪除
        false,        // 是否阻塞
        nil,          // 額外屬性
    )
    if err != nil {
        log.Fatalf("Failed to declare a queue: %s", err)
    }
}

3. 處理請求

package main

import (
    "bytes"
    "encoding/json"
    "net/http"

    "github.com/gin-gonic/gin"
    "yourmodule/db"
    "yourmodule/rabbitmq"
)

type Record struct {
    Data string `json:"data"`
}

func main() {
    db.InitDB()
    rabbitmq.InitRabbitMQ()

    r := gin.Default()

    r.POST("/records", func(c *gin.Context) {
        var record Record
        if err := c.ShouldBindJSON(&record); err != nil {
            c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
            return
        }

        // 將記錄傳送到RabbitMQ
        body, _ := json.Marshal(record)
        err := rabbitmq.Channel.Publish(
            "",              // 交換機
            "task_queue",    // 路由鍵
            false,           // 強制傳送
            false,           // 立即傳送
            amqp.Publishing{
                ContentType: "application/json",
                Body:        body,
                DeliveryMode: amqp.Persistent, // 設定訊息持久化
            })
        if err != nil {
            c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to publish message"})
            return
        }

        c.JSON(http.StatusAccepted, gin.H{"status": "Request accepted"})
    })

    r.Run(":8080")
}

4. 消費者程式碼

package main

import (
    "encoding/json"
    "log"

    "github.com/streadway/amqp"
    "yourmodule/db"
)

type Record struct {
    Data string `json:"data"`
}

func main() {
    db.InitDB()

    conn, err := amqp.Dial("amqp://guest:guest@localhost:5672/")
    if err != nil {
        log.Fatalf("Failed to connect to RabbitMQ: %s", err)
    }
    defer conn.Close()

    ch, err := conn.Channel()
    if err != nil {
        log.Fatalf("Failed to open a channel: %s", err)
    }
    defer ch.Close()

    msgs, err := ch.Consume(
        "task_queue", // 佇列名稱
        "",           // 消費者名稱
        false,        // 自動確認
        false,        // 排他
        false,        // 阻塞
        false,        // 優先
        nil,          // 額外屬性
    )
    if err != nil {
        log.Fatalf("Failed to register a consumer: %s", err)
    }

    log.Println("Waiting for messages. To exit press CTRL+C")

    for d := range msgs {
        var record Record
        if err := json.Unmarshal(d.Body, &record); err != nil {
            log.Printf("Failed to unmarshal message: %s", err)
            d.Nack(false, false) // 拒絕訊息,不重新入隊
            continue
        }

        // 將資料插入到PostgreSQL
        _, err := db.DB.Exec("INSERT INTO records(data) VALUES($1)", record.Data)
        if err != nil {
            log.Printf("Failed to insert record: %s", err)
            d.Nack(false, false) // 拒絕訊息,不重新入隊
            continue
        }

        log.Printf("Inserted record: %s", record.Data)
        d.Ack(false) // 手動確認
    }
}

執行示例

  1. 啟動RabbitMQ服務。
  2. 啟動PostgreSQL服務並建立資料庫和表。
  3. 啟動消費者:
go run consumer.go
  1. 啟動Gin服務:
go run main.go
  1. 使用curl或Postman傳送請求:
curl -X POST http://localhost:8080/records -H "Content-Type: application/json" -d '{"data": "test data"}'

百萬、千萬併發的秒殺預約系統,負載均衡、Redis叢集限流和RabbitMQ消峰

系統架構

  1. 負載均衡:使用Nginx或HAProxy等負載均衡器,將請求分發到多個後端服務例項。
  2. Redis叢集限流:使用Redis來實現限流,確保在高併發情況下不會超賣。
  3. RabbitMQ消峰:使用RabbitMQ將請求非同步處理,減輕後端服務的壓力。
  4. 資料庫:使用關係型資料庫(如PostgreSQL)來持久化資料。

環境準備

  1. 安裝Go:確保你已經安裝了Go語言。
  2. 安裝Gin:使用以下命令安裝Gin框架:
    go get -u github.com/gin-gonic/gin
    
  3. 安裝Redis:確保你已經安裝並執行Redis。
  4. 安裝RabbitMQ:確保你已經安裝並執行RabbitMQ。
  5. 安裝PostgreSQL:確保你已經安裝並執行PostgreSQL。
  6. 安裝依賴
    go get -u github.com/go-redis/redis/v8
    go get -u github.com/streadway/amqp
    go get -u github.com/lib/pq
    

資料庫設定

CREATE DATABASE testdb;

\c testdb

CREATE TABLE records (
    id SERIAL PRIMARY KEY,
    data TEXT NOT NULL
);

Redis 限流實現

1. Redis連線

package redisdb

import (
    "context"
    "github.com/go-redis/redis/v8"
)

var ctx = context.Background()
var Rdb *redis.Client

func InitRedis() {
    Rdb = redis.NewClient(&redis.Options{
        Addr: "localhost:6379", // Redis地址
    })
}

2. 限流函式

func RateLimit(key string, limit int) bool {
    // 使用Redis的INCR命令增加請求計數
    count, err := Rdb.Incr(ctx, key).Result()
    if err != nil {
        return false
    }

    // 設定過期時間
    if count == 1 {
        Rdb.Expire(ctx, key, 1) // 1秒的過期時間
    }

    return count <= int64(limit)
}

RabbitMQ 消費者

package main

import (
    "context"
    "encoding/json"
    "log"

    "github.com/go-redis/redis/v8"
    "github.com/streadway/amqp"
    "yourmodule/db"
    "yourmodule/redisdb"
)

type Record struct {
    Data string `json:"data"`
}

func main() {
    db.InitDB()
    redisdb.InitRedis()

    conn, err := amqp.Dial("amqp://guest:guest@localhost:5672/")
    if err != nil {
        log.Fatalf("Failed to connect to RabbitMQ: %s", err)
    }
    defer conn.Close()

    ch, err := conn.Channel()
    if err != nil {
        log.Fatalf("Failed to open a channel: %s", err)
    }
    defer ch.Close()

    msgs, err := ch.Consume(
        "task_queue", // 佇列名稱
        "",           // 消費者名稱
        false,        // 自動確認
        false,        // 排他
        false,        // 阻塞
        false,        // 優先
        nil,          // 額外屬性
    )
    if err != nil {
        log.Fatalf("Failed to register a consumer: %s", err)
    }

    log.Println("Waiting for messages. To exit press CTRL+C")

    for d := range msgs {
        var record Record
        if err := json.Unmarshal(d.Body, &record); err != nil {
            log.Printf("Failed to unmarshal message: %s", err)
            d.Nack(false, false) // 拒絕訊息,不重新入隊
            continue
        }

        // 將資料插入到PostgreSQL
        _, err := db.DB.Exec("INSERT INTO records(data) VALUES($1)", record.Data)
        if err != nil {
            log.Printf("Failed to insert record: %s", err)
            d.Nack(false, false) // 拒絕訊息,不重新入隊
            continue
        }

        log.Printf("Inserted record: %s", record.Data)
        d.Ack(false) // 手動確認
    }
}

Gin HTTP 服務

package main

import (
    "encoding/json"
    "net/http"

    "github.com/gin-gonic/gin"
    "yourmodule/db"
    "yourmodule/rabbitmq"
    "yourmodule/redisdb"
)

type Record struct {
    Data string `json:"data"`
}

func main() {
    db.InitDB()
    redisdb.InitRedis()
    rabbitmq.InitRabbitMQ()

    r := gin.Default()

    r.POST("/records", func(c *gin.Context) {
        var record Record
        if err := c.ShouldBindJSON(&record); err != nil {
            c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
            return
        }

        // 限流
        if !redisdb.RateLimit("rate_limit_key", 100) { // 每秒100個請求
            c.JSON(http.StatusTooManyRequests, gin.H{"error": "Too many requests"})
            return
        }

        // 將記錄傳送到RabbitMQ
        body, _ := json.Marshal(record)
        err := rabbitmq.Channel.Publish(
            "",              // 交換機
            "task_queue",    // 佇列名稱
            false,           // 強制傳送
            false,           // 立即傳送
            amqp.Publishing{
                ContentType: "application/json",
                Body:        body,
                DeliveryMode: amqp.Persistent, // 設定訊息持久化
            })
        if err != nil {
            c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to publish message"})
            return
        }

        c.JSON(http.StatusAccepted, gin.H{"status": "Request accepted"})
    })

    r.Run(":8080")
}

啟動服務

  1. 啟動Redis服務。
  2. 啟動RabbitMQ服務。
  3. 啟動PostgreSQL服務並建立資料庫和表。
  4. 啟動消費者:
go run consumer.go
  1. 啟動Gin服務:
go run main.go
  1. 使用curl或Postman傳送請求:
curl -X POST http://localhost:8080/records -H "Content-Type: application/json" -d '{"data": "test data"}'

負載均衡

http {
    upstream myapp {
        server localhost:8080;
        server localhost:8081;
        server localhost:8082;
    }

    server {
        listen 80;

        location / {
            proxy_pass http://myapp;
        }
    }
}

相關文章