Golang與RabbitMQ

RabbitMQ 概述

RabbitMQ是採用Erlang編程語言實現了高級消息隊列協議AMQP (Advanced Message Queuing Protocol)的開源消息代理軟件(消息隊列中間件

市面上流行的消息隊列中間件有很多種,而RabbitMQ只是其中比較流行的一種

我們簡單說說消息隊列中間件的作用

  • 解耦
  • 削峯
  • 異步處理
  • 緩存存儲
  • 消息通信
  • 提高系統拓展性

RabbitMQ 特點

  1. 可靠性

    通過一些機制例如,持久化,傳輸確認等來確保消息傳遞的可靠性

  2. 拓展性

    多個RabbitMQ節點可以組成集羣

  3. 高可用性

    隊列可以在RabbitMQ集羣中設置鏡像,如此一來即使部分節點掛掉了,但是隊列仍然可以使用

  4. 多種協議

    原生的支持AMQP,也能支持STOMP,MQTT等協議

  5. 豐富的客戶端

    我們常用的編程語言都支持RabbitMQ

  6. 管理界面

    自帶提供一個WEB管理界面

  7. 插件機制

    RabbitMQ 自己提供了很多插件,可以按需要進行拓展 Plugins

RabbitMQ基礎概念

總體上看RabbitMQ是一個生產者和消費者的模型, 接收,存儲 ,轉發

在這裏插入圖片描述

我們看看在RabbitMQ中的幾個主要概念

  1. Producer (生產者) : 消息的生產者,投遞方

  2. Consumer (消費者) : 消息的消費者

  3. RabbitMQ Broker (RabbitMQ 代理) : RabbitMQ 服務節點(單機情況中,就是代表RabbitMQ服務器)

  4. Queue (隊列) : 在RabbitMQ中Queue是存儲消息數據的唯一形式

  5. Binding (綁定) : RabbitMQ中綁定(Binding)是交換機(exchange)將消息(message)路由給隊列(queue)所需遵循的規則。如果要指示交換機“E”將消息路由給隊列“Q”,那麼“Q”就需要與“E”進行綁定。綁定操作需要定義一個可選的路由鍵(routing key)屬性給某些類型的交換機。路由鍵的意義在於從發送給交換機的衆多消息中選擇出某些消息,將其路由給綁定的隊列。

  6. RoutingKey (路由鍵) : 消息投遞給交換器,通常會指定一個 RoutingKey ,通過這個路由鍵來明確消息的路由規則

    RoutingKey 通常是生產者和消費者有協商一致的key策略,消費者就可以合法從生產者手中獲取數據。這個RoutingKey主要當Exchange交換機模式爲設定爲direct和topic模式的時候使用,fanout模式不使用RoutingKey

  7. Exchange (交換機) : 生產者將消息發送給交換器(交換機),再由交換器將消息路由導對應的隊列中

    交換機四種類型 : fanout,direct,topic,headers

    1. fanout (扇形交換機) :

      將發送到該類型交換機的消息(message)路由到所有的與該交換機綁定的隊列中,如同一個"扇"狀擴散給各個隊列

    在這裏插入圖片描述

    fanout類型的交換機會忽略RoutingKey的存在,將message直接"廣播"到綁定的所有隊列中

    1. direct (直連交換機) :

      根據消息攜帶的路由鍵(RoutingKey) 將消息投遞到對應的隊列中

在這裏插入圖片描述

direct類型的交換機(exchange)是RabbitMQ Broker的默認類型,它有一個特別的屬性對一些簡單的應用來說是非常有用的,在使用這個類型的Exchange時,可以不必指定routing key的名字,在此類型下創建的Queue有一個默認的routing key,這個routing key一般同Queue同名。

  1. Topic (主題交換機) :

    topic類型交換機在RoutingKeyBindKey 匹配規則上更加的靈活. 同樣是將消息路由到RoutingKeyBindingKey 相匹配的隊列中,但是匹配規則有如下的特點 :

    規則1: RoutingKey 是一個使用. 的字符串 例如: “go.log.info” , “java.log.error”

    規則2: BingingKey 也會一個使用 . 分割的字符串, 但是在 BindingKey 中可以使用兩種特殊字符 *# ,其中 “*” 用於匹配一個單詞,"#"用於匹配多規格單詞(零個或者多個單詞)

在這裏插入圖片描述

RoutingKey和BindingKey 是一種"模糊匹配" ,那麼一個消息Message可能 會被髮送到一個或者多個隊列中

無法匹配的消息將會被丟棄或者返回者生產者

  1. Headers (頭交換機):

    Headers類型的交換機使用不是很多

    關於Headers Exchange 摘取一段比較容易理解的解釋 :

    有時消息的路由操作會涉及到多個屬性,此時使用消息頭就比用路由鍵更容易表達,頭交換機(headers exchange)就是爲此而生的。頭交換機使用多個消息屬性來代替路由鍵建立路由規則。通過判斷消息頭的值能否與指定的綁定相匹配來確立路由規則。

    我們可以綁定一個隊列到頭交換機上,並給他們之間的綁定使用多個用於匹配的頭(header)。這個案例中,消息代理得從應用開發者那兒取到更多一段信息,換句話說,它需要考慮某條消息(message)是需要部分匹配還是全部匹配。上邊說的“更多一段消息”就是"x-match"參數。當"x-match"設置爲“any”時,消息頭的任意一個值被匹配就可以滿足條件,而當"x-match"設置爲“all”的時候,就需要消息頭的所有值都匹配成功。

    頭交換機可以視爲直連交換機的另一種表現形式。頭交換機能夠像直連交換機一樣工作,不同之處在於頭交換機的路由規則是建立在頭屬性值之上,而不是路由鍵。路由鍵必須是一個字符串,而頭屬性值則沒有這個約束,它們甚至可以是整數或者哈希值(字典)等。

RabbitMQ 工作流程

消息生產流程

  1. 消息生產者連與RabbitMQ Broker 建立一個連接,建立好了連接之後,開啓一個信道Channel
  2. 聲明一個交換機,並設置其相關的屬性(交換機類型,持久化等)
  3. 聲明一個隊列並設置其相關屬性(排他性,持久化自動刪除等)
  4. 通過路由鍵將交換機和隊列綁定起來
  5. 消息生產者發送消息給 RabbitMQ Broker , 消息中包含了路由鍵,交換機等信息,交換機根據接收的路由鍵查找匹配對應的隊列
  6. 查找匹配成功,則將消息存儲到隊列中
  7. 查找匹配失敗,根據生產者配置的屬性選擇丟棄或者回退給生產者
  8. 關閉信道Channel , 關閉連接

消息消費流程

  1. 消息消費者連與RabbitMQ Broker 建立一個連接,建立好了連接之後,開啓一個信道Channel
  2. 消費者向RabbitMQ Broker 請求消費者相應隊列中的消息
  3. 等待RabbitMQ Broker 迴應並投遞相應隊列中的消息,消費者接收消息
  4. 消費者確認(ack) 接收消息, RabbitMQ Broker 消除已經確認的消息
  5. 關閉信道Channel ,關閉連接

Golang 操作RabbitMQ

RabbitMQ 支持我們常見的編程語言,此處我們使用 Golang 來操作

Golang操作RabbitMQ的前提我們需要有個RabbitMQ的服務端,至於RabbitMQ的服務怎麼搭建我們此處就不詳細描述了.

Golang操作RabbitMQ的客戶端包,網上已經有一個很流行的了,而且也是RabbitMQ官網比較推薦的,不需要我們再從頭開始構建一個RabbitMQ的Go語言客戶端包. 詳情

go get github.com/streadway/amqp

項目目錄

___lib
______commonFunc.go
___producer.go
___comsumer.go

commonFunc.go

package lib

import (
	"github.com/streadway/amqp"
	"log"
)
// RabbitMQ連接函數
func RabbitMQConn() (conn *amqp.Connection,err error){
    // RabbitMQ分配的用戶名稱
	var user string = "admin"
    // RabbitMQ用戶的密碼
	var pwd string = "123456"
    // RabbitMQ Broker 的ip地址
	var host string = "192.168.230.132"
    // RabbitMQ Broker 監聽的端口
	var port string = "5672"
	url := "amqp://"+user+":"+pwd+"@"+host+":"+port+"/"
    // 新建一個連接
	conn,err =amqp.Dial(url)
    // 返回連接和錯誤
	return
}
// 錯誤處理函數
func ErrorHanding(err error, msg string){
	if err != nil{
		log.Fatalf("%s: %s", msg, err)
	}
}

基礎隊列使用

簡單隊列模式是RabbitMQ的常規用法,簡單理解就是消息生產者發送消息給一個隊列,然後消息的消息的消費者從隊列中讀取消息

當多個消費者訂閱同一個隊列的時候,隊列中的消息是平均分攤給多個消費者處理
定義一個消息的生產者

producer.go

package main

import (
	"encoding/json"
	"log"
	"myDemo/rabbitmq_demo/lib"

	"github.com/streadway/amqp"
)
type simpleDemo struct {
	Name string `json:"name"`
	Addr string `json:"addr"`
}
func main() {
	// 連接RabbitMQ服務器
	conn, err := lib.RabbitMQConn()
	lib.ErrorHanding(err, "Failed to connect to RabbitMQ")
	// 關閉連接
	defer conn.Close()
	// 新建一個通道
	ch, err := conn.Channel()
	lib.ErrorHanding(err, "Failed to open a channel")
	// 關閉通道
	defer ch.Close()
	// 聲明或者創建一個隊列用來保存消息
	q, err := ch.QueueDeclare(
		// 隊列名稱
		"simple:queue", // name
		false,   // durable
		false,   // delete when unused
		false,   // exclusive
		false,   // no-wait
		nil,     // arguments
	)
	lib.ErrorHanding(err, "Failed to declare a queue")
	data := simpleDemo{
		Name: "Tom",
		Addr: "Beijing",
	}
	dataBytes,err := json.Marshal(data)
	if err != nil{
		lib.ErrorHanding(err,"struct to json failed")
	}
	err = ch.Publish(
		"",     // exchange
		q.Name, // routing key
		false,  // mandatory
		false,  // immediate
		amqp.Publishing{
			ContentType: "text/plain",
			Body:        dataBytes,
		})
	log.Printf(" [x] Sent %s", dataBytes)
	lib.ErrorHanding(err, "Failed to publish a message")
}

定義一個消息的消費者

comsumer.go

package main

import (
	"log"
	"myDemo/rabbitmq_demo/lib"
)

func main() {
	conn, err := lib.RabbitMQConn()
	lib.ErrorHanding(err,"failed to connect to RabbitMQ")
	defer conn.Close()
	ch, err := conn.Channel()
	lib.ErrorHanding(err,"failed to open a channel")
	defer ch.Close()
	q, err := ch.QueueDeclare(
		"simple:queue", // name
		false,   // durable
		false,   // delete when unused
		false,   // exclusive
		false,   // no-wait
		nil,     // arguments
	)
	lib.ErrorHanding(err,"Failed to declare a queue")
	// 定義一個消費者
	msgs, err := ch.Consume(
		q.Name, // queue
		"",     // consumer
		true,   // auto-ack
		false,  // exclusive
		false,  // no-local
		false,  // no-wait
		nil,    // args
	)
	lib.ErrorHanding(err,"Failed to register a consume")
	go func() {
		for d := range msgs {
			log.Printf("Received a message: %s", d.Body)
		}
	}()

	log.Printf(" [*] Waiting for messages. To exit press CTRL+C")
	select {}
}

工作隊列

工作隊列也稱爲 任務隊列 任務隊列是爲了避免等待執行一些耗時的任務,而是將需要執行的任務封裝爲消息發送給工作隊列,後臺運行的工作進程將任務消息取出來並執行相關任務 , 多個後臺工作進程同時間進行,那麼任務在他們之間共享

在這裏插入圖片描述

我們定義一個任務的生產者,用於生產任務消息

task.go

package main

import (
	"github.com/streadway/amqp"
	"log"
	"myDemo/rabbitmq_demo/lib"
	"os"
	"strings"
)

func bodyFrom(args []string) string {
	var s string
	if (len(args) < 2) || os.Args[1] == "" {
		s = "no task"
	} else {
		s = strings.Join(args[1:], " ")
	}
	return s
}
func main() {
	// 連接RabbitMQ服務器
	conn, err := lib.RabbitMQConn()
	lib.ErrorHanding(err, "Failed to connect to RabbitMQ")
	// 關閉連接
	defer conn.Close()
	// 新建一個通道
	ch, err := conn.Channel()
	lib.ErrorHanding(err, "Failed to open a channel")
	// 關閉通道
	defer ch.Close()
	// 聲明或者創建一個隊列用來保存消息
	q, err := ch.QueueDeclare(
		// 隊列名稱
		"task:queue", // name
		false,          // durable
		false,          // delete when unused
		false,          // exclusive
		false,          // no-wait
		nil,            // arguments
	)
	lib.ErrorHanding(err, "Failed to declare a queue")
	body := bodyFrom(os.Args)
	err = ch.Publish(
		"",
		q.Name,
		false,
		false,
		amqp.Publishing{
			ContentType: "text/plain",
			// 將消息標記爲持久消息
			DeliveryMode: amqp.Persistent,
			Body:         []byte(body),
		})
	lib.ErrorHanding(err, "Failed to publish a message")
	log.Printf("sent %s", body)
}

定義一個工作者,用於消費掉任務消息

worker.go

package main

import (
	"log"
	"myDemo/rabbitmq_demo/lib"
)

func main() {
	conn, err := lib.RabbitMQConn()
	lib.ErrorHanding(err, "Failed to connect to RabbitMQ")
	defer conn.Close()

	ch, err := conn.Channel()
	lib.ErrorHanding(err, "Failed to open a channel")
	defer ch.Close()

	q, err := ch.QueueDeclare(
		"task:queue", // name
		false,         // durable
		false,        // delete when unused
		false,        // exclusive
		false,        // no-wait
		nil,          // arguments
	)
	lib.ErrorHanding(err, "Failed to declare a queue")
	// 將預取計數器設置爲1
	// 在並行處理中將消息分配給不同的工作進程
	err = ch.Qos(
		1,     // prefetch count
		0,     // prefetch size
		false, // global
	)
	lib.ErrorHanding(err, "Failed to set QoS")

	msgs, err := ch.Consume(
		q.Name, // queue
		"",     // consumer
		false,  // auto-ack
		false,  // exclusive
		false,  // no-local
		false,  // no-wait
		nil,    // args
	)
	lib.ErrorHanding(err, "Failed to register a consumer")

	forever := make(chan bool)

	go func() {
		for d := range msgs {
			log.Printf("Received a message: %s", d.Body)
			log.Printf("Done")
			d.Ack(false)
		}
	}()

	log.Printf(" [*] Waiting for messages. To exit press CTRL+C")
	<-forever
}

測試

#shell1
go run task.go
#shell2
go run worker.go
#shell3
go run worker.go

RabbitMQ 的用法很多,詳情參看官網文檔

參考資料

https://www.rabbitmq.com/getstarted.html
http://rabbitmq.mr-ping.com/
https://github.com/streadway/amqp
https://blog.csdn.net/u013256816/category_6532725.html

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章