介紹
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 {}
}
執行示例
- 啟動RabbitMQ服務。
- 執行消費者程式。
- 執行生產者程式。
簡單模式下發布者釋出訊息,消費者消費訊息
環境準備
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 {}
}
執行示例
- 啟動RabbitMQ服務。
- 先執行消費者程式,確保它在等待訊息。
- 然後執行生產者程式,傳送訊息。
工作模式下傳送消費訊息手動確認資訊
環境準備
確保你已經安裝了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 {}
}
執行示例
- 啟動RabbitMQ服務。
- 先執行消費者程式,確保它在等待訊息。
- 然後執行生產者程式,傳送訊息。
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 {}
}
執行示例
- 啟動RabbitMQ服務。
- 先執行消費者程式,確保它在等待訊息。
- 然後執行釋出者程式,傳遞要傳送的訊息。例如:
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 {}
}
執行示例
- 啟動RabbitMQ服務。
- 先執行消費者程式,確保它在等待訊息。
go run consumer.go
- 然後執行生產者程式,傳遞路由鍵和要傳送的訊息。
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 {}
}
執行主題示例
- 啟動RabbitMQ服務。
- 先執行消費者程式,確保它在等待訊息。例如,執行以下命令以接收所有“info”級別的訊息:
go run consumer.go
- 然後執行生產者程式,傳遞路由鍵和要傳送的訊息。例如:
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示例
- 啟動RabbitMQ服務。
- 啟動RPC服務端:
go run rpc_server.go
- 啟動RPC客戶端,傳遞要計算的斐波那契數:
go run rpc_client.go 10
可靠性、資料持久化、消費端限流、消費者確認和訊息過期處理
在RabbitMQ中,確保訊息的可靠性和資料的永續性是非常重要的。我們可以透過以下幾個方面來實現這些目標:
- 訊息持久化:確保訊息在RabbitMQ重啟後仍然存在。
- 消費端限流:控制消費者的訊息處理速率。
- 消費者確認:確保訊息被成功處理後再從佇列中移除。
- 訊息過期處理:設定訊息的過期時間,超時後自動刪除。
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 {}
}
執行示例
- 啟動RabbitMQ服務。
- 啟動生產者,傳送持久化訊息:
go run producer.go "Hello World!"
- 啟動消費者,處理訊息並設定限流和過期時間:
go run consumer.go
實現高併發秒殺、搶購、預約、訂票系統
系統設計
- 商品庫存管理:使用資料庫或記憶體來管理商品庫存。
- RabbitMQ作為訊息佇列:將使用者請求放入RabbitMQ佇列中,由消費者處理實際的庫存扣減操作。
- 併發控制:透過訊息佇列來控制併發,確保在高併發情況下不會超賣。
- 消費者確認:確保每個請求被成功處理後再從佇列中移除。
程式碼實現
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 {}
}
執行示例
- 啟動RabbitMQ服務。
- 啟動消費者:
go run consumer.go
- 啟動多個生產者模擬使用者請求:
go run producer.go product_1
使用Gin+PostgreSQL解決高併發增加資料問題、以及使用RabbitMQ結合PostgreSQL最佳化。
環境準備
- 安裝Go:確保你已經安裝了Go語言。
- 安裝Gin:使用以下命令安裝Gin框架:
go get -u github.com/gin-gonic/gin
- 安裝PostgreSQL:確保你已經安裝並執行PostgreSQL。
- 安裝RabbitMQ:確保你已經安裝並執行RabbitMQ。
- 安裝PostgreSQL驅動:
go get -u github.com/lib/pq
- 安裝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) // 手動確認
}
}
執行示例
- 啟動RabbitMQ服務。
- 啟動PostgreSQL服務並建立資料庫和表。
- 啟動消費者:
go run consumer.go
- 啟動Gin服務:
go run main.go
- 使用
curl
或Postman傳送請求:
curl -X POST http://localhost:8080/records -H "Content-Type: application/json" -d '{"data": "test data"}'
百萬、千萬併發的秒殺預約系統,負載均衡、Redis叢集限流和RabbitMQ消峰
系統架構
- 負載均衡:使用Nginx或HAProxy等負載均衡器,將請求分發到多個後端服務例項。
- Redis叢集限流:使用Redis來實現限流,確保在高併發情況下不會超賣。
- RabbitMQ消峰:使用RabbitMQ將請求非同步處理,減輕後端服務的壓力。
- 資料庫:使用關係型資料庫(如PostgreSQL)來持久化資料。
環境準備
- 安裝Go:確保你已經安裝了Go語言。
- 安裝Gin:使用以下命令安裝Gin框架:
go get -u github.com/gin-gonic/gin
- 安裝Redis:確保你已經安裝並執行Redis。
- 安裝RabbitMQ:確保你已經安裝並執行RabbitMQ。
- 安裝PostgreSQL:確保你已經安裝並執行PostgreSQL。
- 安裝依賴:
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")
}
啟動服務
- 啟動Redis服務。
- 啟動RabbitMQ服務。
- 啟動PostgreSQL服務並建立資料庫和表。
- 啟動消費者:
go run consumer.go
- 啟動Gin服務:
go run main.go
- 使用
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;
}
}
}