訊息佇列之RocketMQ

ice_moss發表於2022-11-13
[TOC]

文章介紹

本文來簡單介紹一下訊息佇列 ,這裡將什麼是MQ, 介紹RocketMQ的安裝,RocketMQ的基本概念,訊息型別,並使用go做各類訊息的收發

什麼是MQ

1.什麼是mq

訊息佇列是一種“先進先出”的資料結構

queue1.png

2.應用場景

其應用場景主要包含以下3個方面

  • 應用解耦

系統的耦合性越高,容錯性就越低。以電商應用為例,使用者建立訂單後,如果耦合呼叫庫存系統、物流系統、支付系統,任何一個子系統出了故障或者因為升級等原因暫時不可用,都會造成下單操作異常,影響使用者使用體驗。

解耦1.png
使用訊息佇列解耦合,系統的耦合性就會提高了。比如物流系統發生故障,需要幾分鐘才能來修復,在這段時間內,物流系統要處理的資料被快取到訊息佇列中,使用者的下單操作正常完成。當物流系統回覆後,補充處理存在訊息佇列中的訂單訊息即可,終端系統感知不到物流系統發生過幾分鐘故障。

解耦2.png

  • 流量削峰

mq-5.png
應用系統如果遇到系統請求流量的瞬間猛增,有可能會將系統壓垮。有了訊息佇列可以將大量請求快取起來,分散到很長一段時間處理,這樣可以大大提到系統的穩定性和使用者體驗。

mq-6.png

一般情況,為了保證系統的穩定性,如果系統負載超過閾值,就會阻止使用者請求,這會影響使用者體驗,而如果使用訊息佇列將請求快取起來,等待系統處理完畢後通知使用者下單完畢,這樣總不能下單體驗要好。

處於經濟考量目的:

業務系統正常時段的QPS如果是1000,流量最高峰是10000,為了應對流量高峰配置高效能的伺服器顯然不划算,這時可以使用訊息佇列對峰值流量削峰

  • 資料分發

mq-1.png

透過訊息佇列可以讓資料在多個系統更加之間進行流通。資料的產生方不需要關心誰來使用資料,只需要將資料傳送到訊息佇列,資料使用方直接在訊息佇列中直接獲取資料即可。

MQ的優點和缺點

優點:解耦、削峰、資料分發mq-2.png

缺點包含以下幾點:

  • 系統可用性降低
    系統引入的外部依賴越多,系統穩定性越差。一旦MQ當機,就會對業務造成影響。
    如何保證MQ的高可用?
  • 系統複雜度提高
    MQ的加入大大增加了系統的複雜度,以前系統間是同步的遠端呼叫,現在是透過MQ進行非同步呼叫。
    如何保證訊息沒有被重複消費?怎麼處理訊息丟失情況?那麼保證訊息傳遞的順序性?
  • 一致性問題
    A系統處理完業務,透過MQ給B、C、D三個系統發訊息資料,如果B系統、C系統處理成功,D系統處理失敗。
    如何保證訊息資料處理的一致性?

RocketMQ的安裝

使用docker安裝

docker安裝RocketMQ

RocketMQ的基本概念

  • Producer:訊息的傳送者;例如:發信人
  • Consumer:訊息接收者;例如:收信人
  • Broker:暫存和傳輸訊息;例如:郵局、中轉站
  • NameServer:管理Broker;例如:各個郵局的管理機構
  • Topic:區分訊息的種類;一個傳送者可以傳送訊息給一個或者多個Topic;一個訊息的接收者可以訂閱一個或者多個Topic訊息
  • Message Queue:相當於是Topic的分割槽;用於並行傳送和接收訊息

訊息型別

go實戰

需要拉取

go get github.com/apache/rocketmq-client-go/v2
go get github.com/apache/rocketmq-client-go/v2/primitive
go get github.com/apache/rocketmq-client-go/v2/producer

這裡我以實戰的角度來介紹rocketMQ的訊息型別:

1. 普通訊息

只是訊息的收發,傳送成功後接收者就直接可以收到訊息

package main

import (
    "context"
    "fmt"
    "time"

    "github.com/apache/rocketmq-client-go/v2"
    "github.com/apache/rocketmq-client-go/v2/primitive"
    "github.com/apache/rocketmq-client-go/v2/producer"
)

func main() {
    //初始化生產者
    q, err := rocketmq.NewProducer(producer.WithNameServer([]string{"101.1.12.202:9876"}))
    if err != nil {
        panic("生成q生產者失敗")
    }
    if err := q.Start(); err != nil {
        panic("啟動q生產者失敗")
    }

    msg := []byte("您好呀, 我是ice_moss")
    mq := primitive.NewMessage("msg_test_hello", msg)  //msg_test_hello是為Topic

    res, err := q.SendSync(context.Background(), mq)
    if err != nil {
        fmt.Printf("傳送失敗%s", err)
    }
    fmt.Println("訊息傳送成功")
    fmt.Println(res.String())

    err = q.Shutdown()
    if err != nil {
        panic("shutdown fail err")
    }
}

這裡需要注意的是如果我們需要在一個程式中啟動多個rocketmq.NewProducer()就必須將他的第二個引數配置上:producer.WithGroupName("sendMsg")

q, err := rocketmq.NewProducer(producer.WithNameServer([]string{"101.1.12.202:9876"}), producer.WithGroupName("sendMsg"))

不然就會報:生產者組已經被建立

原因:我們沒有不設定WithGroupName在呼叫時,會自動為我們建立一個預設名稱的WithGroupName,當第二次rocketmq.NewProducer仍然是預設名,這時整個GroupName就衝突了

好了已經將”普通訊息”傳送到佇列中了,現在我們來接收

2. 消費訊息

注意:兩端的Topic必須保持一直

package main

import (
    "context"
    "fmt"
    "os"
    "time"

    "github.com/apache/rocketmq-client-go/v2"
    "github.com/apache/rocketmq-client-go/v2/consumer"
    "github.com/apache/rocketmq-client-go/v2/primitive"
)

func main() {
    c, _ := rocketmq.NewPushConsumer(
        //接收者組
        consumer.WithGroupName("msg_test"),
        consumer.WithNsResolver(primitive.NewPassthroughResolver([]string{"101.1.12.202:9876"})),
    )
    //訂閱訊息
    err := c.Subscribe("msg_test_hello", consumer.MessageSelector{}, func(ctx context.Context,
        msgs ...*primitive.MessageExt) (consumer.ConsumeResult, error) {
        for i := range msgs {
            fmt.Printf("subscribe callback: %v \n", msgs[i])
        }

        return consumer.ConsumeSuccess, nil
    })
    if err != nil {
        fmt.Println(err.Error())
    }

    // Note: start after subscribe
    err = c.Start()
    if err != nil {
        fmt.Println(err.Error())
        os.Exit(-1)
    }

  //程式執行2分鐘
    time.Sleep(time.Second * 120)

    err = c.Shutdown()
    if err != nil {
        fmt.Printf("shutdown Consumer error: %s", err.Error())
    }
}

輸出:

subscribe callback: [Message=[topic=msg_test_hello, body=您好呀, 我是ice_moss, Flag=0, properties=map[CONSUME_START_TIME:1668255347270 MAX_OFFSET:2 MIN_OFFSET:0 UNIQ_K251664A6E000000003cf040100001], TransactionId=], MsgId=0A0251664A6E000000003cf040100001, OffsetMsgId=010EB4CA00002A9F000000000004BC14,QueueId=1, StoreSize=174, QueueOffset=0, SysFlag=0, BornTimestamp=1668254378888, BornHost=112.21.20.248:43010, StoreTimestamp=1668254379066, StoreHost=101.1.12.202:10911, CommitLogOffset=310292, BodyCRC=1573027761, ReconsumeTimes=0, PreparedTransactionOffset=0] 

3. 延時訊息

延時訊息,指我們將我們需要傳送的傳送訊息延遲多少時間後接收方才能收到,其中一個應用場景就是分散式電商系統的下單——>支付, 例如:12306官網買車票,當我們購買一張車票後,後臺會做車票庫存扣減,但是如果我們只下單,不支付這就很要命,該買票的人買不到票,該賣出去的票沒有賣出去;其實仔細一點就會發現,12306購買下單後,在規定時間沒有完成支付,就會取消相應的訂單, 然後做庫存歸還。

現在來看一下延遲訊息怎麼傳送:

package main

import (
    "context"
    "fmt"

    "github.com/apache/rocketmq-client-go/v2"
    "github.com/apache/rocketmq-client-go/v2/primitive"
    "github.com/apache/rocketmq-client-go/v2/producer"
)

//SendMessage 生成訊息,延遲訊息
func SendMessage(q rocketmq.Producer) {
    if err := q.Start(); err != nil {
        panic("啟動q生產者失敗")
    }

    msg := primitive.NewMessage("msg_test_hello", []byte("這是一個延遲訊息"))

    //延遲時間級別
    //messageDelayLevel=1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h
    msg.WithDelayTimeLevel(3)  //10s

    res, err := q.SendSync(context.Background(), msg)
    if err != nil {
        fmt.Printf("傳送失敗%s", err)
    }
    err = q.Shutdown()
    if err != nil {
        fmt.Printf("shutdown Consumer error: %s", err.Error())
    }
    fmt.Println(res.String())
}

func main() {
    //初始化生產者
    q, err := rocketmq.NewProducer(producer.WithNameServer([]string{"101.1.12.202:9876"}))
    if err != nil {
        panic("生成q生產者失敗")
    }
    SendMessage(q)
}

10秒後:

subscribe callback: [Message=[topic=msg_test_hello, body=這是一個延遲訊息, Flag=0, properties=map[CONSUME_START_TIME:1668256662984 DELAY:3 MAX_OFFSET:5 MIN_OFFSET:0 REAREAL_TOPIC:msg_test_hello UNIQ_KEY:0A0251664BE9000000003d12f2e00001]………

4.事務訊息

什麼是事務

事務是指是程式中一系列嚴密的邏輯操作,而且所有操作必須全部成功完成,否則在每個操作中所作的所有更改都會被撤消。可以通俗理解為:就是把多件事情當做一件事情來處理,好比大家同在一條船上,要活一起活,要完一起完

事物的四個特性(ACID)

● 原子性(Atomicity)操作這些指令時,要麼全部執行成功,要麼全部不執行。只要其中一個指令執行失敗,所有的指令都執行失敗,資料進行回滾,回到執行指令前的資料狀態。

eg:拿轉賬來說,假設使用者A和使用者B兩者的錢加起來一共是20000,那麼不管A和B之間如何轉賬,轉幾次賬,事務結束後兩個使用者的錢相加起來應該還得是20000,這就是事務的一致性。

● 一致性(Consistency)事務的執行使資料從一個狀態轉換為另一個狀態,但是對於整個資料的完整性保持穩定。

● 隔離性(Isolation)隔離性是當多個使用者併發訪問資料庫時,比如操作同一張表時,資料庫為每一個使用者開啟的事務,不能被其他事務的操作所干擾,多個併發事務之間要相互隔離,可以使用鎖機制來實現隔離,其實就是將併發場景下對資料操作的部分對併發請求進行序列化。

● 永續性(Durability)當事務正確完成後,它對於資料的改變是永久性的。

eg: 例如我們在使用JDBC運算元據庫時,在提交事務方法後,提示使用者事務操作完成,當我們程式執行完成直到看到提示後,就可以認定事務以及正確提交,即使這時候資料庫出現了問題,也必須要將我們的事務完全執行完成,否則就會造成我們看到提示事務處理完畢,但是資料庫因為故障而沒有執行事務的重大錯誤。

MQ的事務訊息

這裡的事務訊息實現介面:

type TransactionListener interface {
    //  When send transactional prepare(half) message succeed, this method will be invoked to execute local transaction.
    ExecuteLocalTransaction(*Message) LocalTransactionState

    // When no response to prepare(half) message. broker will send check message to check the transaction status, and this
    // method will be invoked to get local transaction status.
    CheckLocalTransaction(*MessageExt) LocalTransactionState
}

我們的業務程式碼需要放在ExecuteLocalTransaction(*Message) LocalTransactionState方法中執行,對應返回相應的狀態

const (
    CommitMessageState LocalTransactionState = iota + 1   //返回狀態:事務執行成功發現訊息
    RollbackMessageState                                                          //返回狀態:進行事務回查
    UnknowState                                                                                        //仍然會回查
)

我們回查機制業務需要在CheckLocalTransaction(*MessageExt) LocalTransactionState方法中完成

下面我們來實現該介面(建立訂單場景下):

package main

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

    "github.com/apache/rocketmq-client-go/v2"
    "github.com/apache/rocketmq-client-go/v2/primitive"
    "github.com/apache/rocketmq-client-go/v2/producer"
    "google.golang.org/grpc/codes"
)

//Order 模擬訂單
type Order struct {
    OrderSrvID string
    UserID     int32
    GoodsID    int32
    TotalPrice float32
    Post       string
    Address    string
    Mobile     string
}

//OrderLister 介面實現者,事務可以將一下配置\資訊寫入該結構體中
type OrderLister struct {
    Code codes.Code      //返回狀態碼
    Ctx  context.Context //上下文資料
    ID   int32           //訂單id
}

//ExecuteLocalTransaction  When send transactional prepare(half) message succeed, this method will be invoked to execute local transaction.
func (o *OrderLister) ExecuteLocalTransaction(msg *primitive.Message) primitive.LocalTransactionState {

    //執行本地業務邏輯
    fmt.Println("開始執行本地邏輯")
    time.Sleep(time.Second * 3)

    orderInfo := Order{}
    err := json.Unmarshal(msg.Body, &orderInfo)
    if err != nil {
        o.Code = codes.Unavailable
        log.Fatal("解析失敗:", err)

        //呼叫回查邏輯
        return primitive.RollbackMessageState
    }

    fmt.Println("訂單資訊:", orderInfo)

    fmt.Println("本地邏輯執行成功")

    //CommitMessageState 提交資訊至mq
    //CommitMessageState/RollbackMessageState都不會回查
    return primitive.CommitMessageState
}

//CheckLocalTransaction When no response to prepare(half) message. broker will send check message to check the transaction status, and this method will be invoked to get local transaction status.
func (o *OrderLister) CheckLocalTransaction(*primitive.MessageExt) primitive.LocalTransactionState {
    //回查
    fmt.Println("事務未透過,開始回查")
    return primitive.RollbackMessageState
}

func (o *Order) CreateOrder(q rocketmq.TransactionProducer) {
    order, err := json.Marshal(o)
    if err != nil {
        panic("marshal fail")
    }

    msg := primitive.NewMessage("msg_test_order", order)
    res, err := q.SendMessageInTransaction(context.Background(), msg)
    if err != nil {
        fmt.Printf("傳送失敗%s", err)
    } else {
        fmt.Println("傳送成功", res.String())
    }

    time.Sleep(time.Hour)

    err = q.Shutdown()
    if err != nil {
        panic("shutdown fail err")
    }
}

func main() {

    //初始化事務物件
    orderLister := &OrderLister{}
    q, err := rocketmq.NewTransactionProducer(orderLister,
        producer.WithNameServer([]string{"101.1.12.202:9876"}), producer.WithGroupName("msg_test"))
    if err != nil {
        panic("生成q生產者失敗")
    }

    if err = q.Start(); err != nil {
        panic("啟動q生產者失敗")
    }
    orderInfo := &Order{
        OrderSrvID: "343435",
        UserID:     21,
        GoodsID:    214,
        TotalPrice: 150.5,
        Post:       "請儘快發貨",
        Address:    "無錫市",
        Mobile:     "18389202834",
    }

    orderInfo.CreateOrder(q)
}

執行輸出:

開始執行本地邏輯
訂單資訊: {343435 21 214 150.5 請儘快發貨 無錫市 18389202834}
本地邏輯執行成功
傳送成功 SendResult [sendStatus=0, msgIds=0A0266DB4E24000000003da94f100001, offsetMsgId=010EB4CA00002A9F000000000004C28F, queueOffset=364, messageQueue=MessageQueue [tomsg_test_order, brokerName=broker-a, queueId=1]]

接收者接收到:

subscribe callback: [Message=[topic=msg_test_order, body={"OrderSrvID":"343435","UserID":21,"GoodsID":214,"TotalPrice":150.5,"Post":"請儘快發貨","Address":"無錫市","Mobile":"18389202834"}, Flag=0, properties=map[CONSUME_START_TIME:1668266524665 MAX_OFFSET:1 MIN_OFFSET:0 PGROUP:msg_test REAL_QID:1 REAL_TOPIC:msg_test_order TRAN_MSG:true UNIQ_KEY:0A0266DB4E24000000003da94f100001], TransactionId=0A0266DB4E24000000003da94f100001], MsgId=0A0266DB4E24000000003da94f100001…………
本作品採用《CC 協議》,轉載必須註明作者和本文連結

相關文章