文章介紹
本文來簡單介紹一下訊息佇列 ,這裡將什麼是MQ, 介紹RocketMQ的安裝,RocketMQ的基本概念,訊息型別,並使用go做各類訊息的收發
什麼是MQ
1.什麼是mq
訊息佇列是一種“先進先出”的資料結構
2.應用場景
其應用場景主要包含以下3個方面
- 應用解耦
系統的耦合性越高,容錯性就越低。以電商應用為例,使用者建立訂單後,如果耦合呼叫庫存系統、物流系統、支付系統,任何一個子系統出了故障或者因為升級等原因暫時不可用,都會造成下單操作異常,影響使用者使用體驗。
使用訊息佇列解耦合,系統的耦合性就會提高了。比如物流系統發生故障,需要幾分鐘才能來修復,在這段時間內,物流系統要處理的資料被快取到訊息佇列中,使用者的下單操作正常完成。當物流系統回覆後,補充處理存在訊息佇列中的訂單訊息即可,終端系統感知不到物流系統發生過幾分鐘故障。
- 流量削峰
應用系統如果遇到系統請求流量的瞬間猛增,有可能會將系統壓垮。有了訊息佇列可以將大量請求快取起來,分散到很長一段時間處理,這樣可以大大提到系統的穩定性和使用者體驗。
一般情況,為了保證系統的穩定性,如果系統負載超過閾值,就會阻止使用者請求,這會影響使用者體驗,而如果使用訊息佇列將請求快取起來,等待系統處理完畢後通知使用者下單完畢,這樣總不能下單體驗要好。
處於經濟考量目的:
業務系統正常時段的QPS如果是1000,流量最高峰是10000,為了應對流量高峰配置高效能的伺服器顯然不划算,這時可以使用訊息佇列對峰值流量削峰
- 資料分發
透過訊息佇列可以讓資料在多個系統更加之間進行流通。資料的產生方不需要關心誰來使用資料,只需要將資料傳送到訊息佇列,資料使用方直接在訊息佇列中直接獲取資料即可。
MQ的優點和缺點
優點:解耦、削峰、資料分發
缺點包含以下幾點:
- 系統可用性降低
系統引入的外部依賴越多,系統穩定性越差。一旦MQ當機,就會對業務造成影響。
如何保證MQ的高可用? - 系統複雜度提高
MQ的加入大大增加了系統的複雜度,以前系統間是同步的遠端呼叫,現在是透過MQ進行非同步呼叫。
如何保證訊息沒有被重複消費?怎麼處理訊息丟失情況?那麼保證訊息傳遞的順序性? - 一致性問題
A系統處理完業務,透過MQ給B、C、D三個系統發訊息資料,如果B系統、C系統處理成功,D系統處理失敗。
如何保證訊息資料處理的一致性?
RocketMQ的安裝
使用docker安裝
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 協議》,轉載必須註明作者和本文連結