go+rabbitmq實現一個簡單的tcc

  • 之前的文章解析了rocketmq的事務(關於producer的), 由於自己使用的是rabbit(界面友好,雖然不是分佈式的 但也基本ok), 所以簡單實現了一個。

思路

  • 核心的思想參考了rocketmq, 主要分爲兩個階段, 事務執行階段和消息發送階段。
    *1. 如果事務執行出問題,那麼消息不會發送,可以利用報警或者日誌去處理
    *2. 如果事務執行成功,消息發送失敗, 則事務進行回滾操作

  • 保證消息能夠正常發送到rabbitmq, 主要使用了rabbit的特性,基於硬盤和發送者事務, 硬盤可以保證數據的不丟失(如果物理損壞可以利用鏡像集羣, 當然如果恰好幾個物理機都壞了那就尷尬了), 發送者事務則可以保證消息到達rabbit並被持久化後返回成功。

  • 而對於消費者則啓用消費者事務,在消費後手動ack(需要消費者自行寫相關代碼) 從而保證整體消息的不丟失。 至於消費的冪等性、順序性還是需要共同去做一些方案。

  • 我個人還是喜歡在發送消息前/後,或者消息出現異常、超時、確認之後做一些其他操作,比如持久化到數據庫中,以確保之後做統計或者找問題。 這麼做的理由是雖然中間件會將消息存儲在磁盤, 首先不會存儲很久,例如kafka默認只存一週(甚至不足), 或者當消息確認後進行物理刪除(消息量大的時候很容易打滿硬盤導致服務崩掉)。 當然寫庫會產生新的問題,例如寫併發瓶頸、執行的延遲等等, 具體使用請酌情選擇。

流程圖

自己的流程圖

  • 流程圖和rocketmq比起來簡直就是小巫見大巫了, 主要因爲rabbit內部的原理沒有畫(erlang讀不懂 囧。。)
  • 類比三個階段
  • try消息 — 發送一個約定的不含邏輯的消息, 消費方只需要確保接受即可,不需要做邏輯操作。 這一步還在考慮。
  • 本地事務階段 — 通過listener的execute執行,進行容錯處理(go 沒有try-catch,所以使用error作爲返回值)。 出現問題即回滾, 不會發送後續消息
  • 消息階段 — 此時確認本地事務執行成功, 只需要確保消息發送即可。 此時如果rabbitmq掛掉接受不了消息或者拒絕 會嘗試重試幾次, 超過重試次數,則說明確定有問題,執行事後方案並且通知報警

show my code (附錄2有使用demo)

1. 接口類
// 事務監聽接口
type TransactionListener interface {
   Execute() error  // 執行事務方法
   Check() int      // 查詢方法 主要用於 消息隊列無法通信時作爲回調查詢
   RollBack() error // 回滾方法
}
  • 定義了一個接口, 主要是本地事務的實現接口。 Execute()方法是自己實現, 具體的參數還在考慮能否抽象出來。 Rollback()則是在出現問題時即時回滾,(如果可以的話我希望永遠不會調用這個方法)。 Check()則是作爲消費者的檢查,例如prodcuer和rabbit出現鏈接問題導致收不到對應的ack(mq頻頻重試等問題)。

  • 由於go的interface不能定義屬性(可能我沒找到方法,確實沒找到,希望知道的大佬告訴我下)。 如果可以的話, 我個人比較希望有個int的變量來存儲 這個接口的結果狀態(狀態碼自己定義), 或者使用map[string]interface{}來存儲更多的變量。(好懷念concurrentHashMap和HashMap)

2. struct
  • 定義了事務類和消息類。
  • 消息類主要是保持各個端通信的約定,因爲rabbitmq的真正的消息體只有body([]byte類型)。 需要彼此之間約定好一些字段,才方便共同處理(rocketmq自己有Message和MessageExt類)
  • 事務類主要是處理事務消息,類似於producer-client, 做一些消息的調度處理操作。所以也沒有做序列化的處理。
  • 可能有人注意到了 transaction是小寫,而message是首字母大寫。 在go中,純小寫一般代表私有(protected/private), 首字母大寫代表公用(public)。 所以transaction是不能在包外直接使用的。 具體使用見下方
type transaction struct {
	ExchangeName string              // 專門處理事務的交換器
	RouteKey     string              // 專門處理事務的路由key
	Listener     TransactionListener // 對應的listener
	NeedTry      bool                // 是否發送try消息 確認存活 默認關閉
}
// 消息類 
type Message struct {
	Id       int                    `json:"id"`
	Action   string                 `json:"action"`   // 消息動作
	Content  map[string]interface{} `json:"content"`  // 具體的消息內容
	Callback string                 `json:"callback"` // 消費成功後 調用的回調函數
}
3. 一些方法
3.1 初始化
	/**
	rabbitMq 事務類初始化
	@param listener TransactionListener 事務監聽實現
	*/
	func NewTransaction(listener TransactionListener) *transaction {

		trans := new(transaction)
		trans.ExchangeName = "" ; // 交換器名
		//  viper.GetString(rabbitPrefix + "transaction.exchangeName")
		trans.RouteKey = ""; // 對應的routekey
		// viper.GetString(rabbitPrefix + "transaction.routeKey")
		trans.Listener = listener // 事務接口
		trans.NeedTry = true // 是否需要try

	    // 交換器類型
		// var exchangeType string = viper.GetString(rabbitPrefix + "transaction.exchangeType")
		// 隊列名
		// var queueName string = viper.GetString(rabbitPrefix + "transaction.queueName")
	
		// 創建exchange
		go exchangeInit(trans.ExchangeName, exchangeType)
	
		// 創建隊列
		go queueInit(queueName, "", "")
	
		// 隊列綁定
		go queueBind(queueName, trans.RouteKey, trans.ExchangeName)
	
		return trans
	}
  • 使用NewTransaction來 對外提供transaction類, 寫起來有點像傳統的singleton的方式。 具體自己的考慮請看文末的附錄1.
  • 當然這種方式有很多。 可以用命名返回值。 也可以直接乾脆return一個 &transaction{…}。 (個人僅僅想嘗試下新的寫法)
  • 註釋掉的部分 我個人使用了viper去讀取配置(一個讀取配置文件的包, 個人用的yml)。
  • 下面的go 初始化和bind的方法,只是自己簡單的封了一層rabbit的初始化和bind方法。 採用了協程, 不影響主流程。 可能會在機器第一次初始化的時候會報錯, 我個人會選擇在項目初始化腳本里寫一個隊列初始化。 在這裏再寫一次是由於個人 遇到過 隊列和交換器突然失效的情況(可能是rabbit突然宕機又迅速重啓), 導致消息傳遞不到。 所以在這再初始化一次, rabbit允許在 主要核心參數相同的情況下, 覆蓋初始化(核心參數變更就會直接報錯。)
3.2 流程方法
  • 核心方法
/**
發送事務處理的消息
@param trulyMessage Message 消息
@return error
*/
func (trans *transaction) MakeMessageTransaction(trulyMessage Message) error {
	var err error

	// 1. 如果需要  發送try 消息
	if trans.NeedTry {
		err = trans.Try()
		if err != nil {
			return err
		}
	}

	// 2. 執行事務 -- 本地事務執行
	err = trans.Listener.Execute()
	if err != nil {
		return err
	}

	// 3. 發送消息
	confirmation, err := SendMessage(trulyMessage, trans.ExchangeName, trans.RouteKey)

	// 4. 出現異常情況 ||  ack = false (拒絕消息)
	if err != nil || (confirmation != nil && confirmation.Ack == false) {
		log.Println(trulyMessage.Action, err.Error())
		var tryTime int = 0
		// 回滾操作
		for {
			// 出現異常則進行回滾 直到回滾成功
			err = trans.Listener.RollBack()
			if err == nil {
				break
			}
			if tryTime >= MaxTries {
				log.Println("transaction rollback err : ", err.Error())
				// todo 其他報警機制
			}
			tryTime++

		}
	}
	return err
}
  • 發送Try方法。 由於還在考慮Try這步驟是否真的有效,所以加了個選擇項
/**
發送一條ping消息
@return error
*/
func (trans *transaction) Try() error {
	// 走默認的
	return 	TryMessageTransactionWithExchangeAndRoute(trans.ExchangeName, trans.RouteKey)
}

/**
事務開啓 - 嘗試與隊列通信
@param exchange string 交換器名
@param routeKey string 路由key
@return error
*/
func TryMessageTransactionWithExchangeAndRoute(exchange string, routeKey string) error {
	// send prepare
	content := make(map[string]interface{})
	prepareMessage := &Message{
		Id:       -1,
		Action:   MessageActionPrepare,
		Content:  content,
		Callback: "", // 消費成功後 調用的回調函數
	}
	_, err := SendMessage(prepareMessage, exchange, routeKey)

	if err != nil {
		log.Println(prepareMessage.Action, err.Error())
		// panic(err)
	}
	return err
}
  • 本地事務執行 execute() 需要自行實現哈~
  • 發送消息
    做了個簡單的重試機制( 只有特定錯誤纔會重試), 當然上述的try消息也是調用的該方法
    	/**
    發送消息 (含重試處理)
    @param message &Message 消息
    @param exchange string 交換器
    @param routeKey string 路由key
    @return *amqp.Confimation, error
    */
    func SendMessage(message *Message, exchange string, routeKey string) (*amqp.Confirmation, error) {
         // 轉json
    	jsonMsg, jsonErr := json.Marshal(message)
    
    	if jsonErr != nil {
    		return nil, jsonErr
    	}
    
    	// 發送消息計數
    	var count int = 0
    	// 發送消息
    	confirmation, err := mqSend(jsonMsg, exchange, routeKey)
    
    	// 重試機制
    	// 只有當消息超時再重新發送 , 其他錯誤直接返回
    	for err != nil && err == MqErrorSendMessageTimeout {
    		// 超過了最大重試次數
    		if count >= MaxTries {
    			return confirmation, MqErrorSendMessageFail
    		}
    		confirmation, err = mqSend(jsonMsg, exchange, routeKey)
    		count++
    
    		// todo  拒絕接受消息處理 (confirmation.Ack == false)
    		if confirmation != nil && confirmation.Ack == false {
    
    		}
    	}
    
    	return confirmation, err
    }
    
  • 容錯處理
    一旦發消息出現非預期異常,或者重試幾次仍然有問題, 則進行事務的回滾, 並進行一些報警機制。
    rollback的外層加了一個for{}, 之前自身遇到過rollback時候服務器異常的。。所以會調用直到成功,或者出現更可怕的問題 需要人工處理等。
  • 其他部分代碼
    • 開啓消費者事務 (初始化信道Channel時)
    // 開啓發送者確認模式
    err = Channel.Confirm(false)
    ConfirmChan = make(chan amqp.Confirmation)
    Channel.NotifyPublish(ConfirmChan)
    
    • 配置死信隊列 (創建隊列時)
    args := amqp.Table{}
    // 綁定該隊列的 死信route和key
    args["x-dead-letter-exchange"] = deadExchange
    args["x-dead-letter-routing-key"] = deadRouteKey
    
    Queue, err = Channel.QueueDeclare(
    	name,  // name
    	true,  // durable -- 是否持久化
    	false, // delete when unused
    	false, // exclusive -- 是否獨佔
    	false, // no-wait -- 阻塞消息
    	args,  // arguments
    )
    
    • 消息結果處理
    
    // 結果處理
    select {
    
    // 確認處理
    case messageConfirmation := <-ConfirmChan: //如果有數據,下面打印。但是有可能ch一直沒數據
    	log.Printf("%s: %s: %s: %d ", "common.transactionWithRabbit#mqSend", MqMessageSuccess, message, messageConfirmation.DeliveryTag)
    	return &messageConfirmation, nil
    
    // 超時處理
    case <-time.After(time.Duration(MaxTime) * time.Millisecond): //上面的ch如果一直沒數據會阻塞,那麼select也會檢測其他case條件,檢測到後MaxTime毫秒超時
    	log.Printf("%s: %s: %s ", "common.transactionWithRabbit#mqSend", MqErrorSendMessageTimeout.Error(), message)
    	return nil, MqErrorSendMessageTimeout
    }
    
  • 整體流程大概就是這樣,只是放了部分代碼。
4. 個人覺得一些問題
  • 由於是自己寫的,所以許多初始化 直接使用的 讀取yml配置文件, 考慮要不要搞一個或者多個config類
  • 關於Try的作用, 目前還在考慮是否有泛用性,主要是我個人遇到過場景。 由於消費端和我們不是一個團隊, 有一次消費端出現問題導致消息堆積,rabbitmq最後已經拒絕處理消息(rabbitmq掛掉 或者 消費端出現問題改造下直接把消息reject)。 導致大量本地事務執行又回滾, 吃了很多資源也浪費了很多時間
  • 發送Try消息後, 消費者是否要做一些動作。 如果後續事務失敗導致直接回滾, 要做些什麼
  • 通過源碼看到 go-rabbit 發送消息的隊列實際是個容量爲1的chan, 包括確認隊列也是, 當阻塞時可能會出現問題?(目前自己不知道怎麼阻塞。。), 之前重試時會不斷的 從chan中取出並重新塞回去。
  • transaction 要不要直接開放成public
  • 關於事務的方法,目前還沒想好可抽象的execute參數, 比如interface{}
  • 事務是否考慮在抽出一個commit()方法, 在確定發送消息後 在調用commit()。 而不是在execute裏直接做提交。
結尾

由於是自己寫的,希望大佬們輕噴,或者有什麼更好的意見和建議 歡迎提給我。 微信在下面哈, 加我的話請備註一下。。
個人的項目在 github地址
具體的這個代碼在項目的 src/common/目錄下 rabbitMQ.go和transactionWithRabbit.go文件

在這裏插入圖片描述

附錄1:
  • 主要是參考了一些大神的寫法, 由於go的特性不包含類和方法, 所以對於對外提供的方式還在爭議。 java系可能更喜歡可以New的struct, php系可能更喜歡 package.方法名(方法名首字母大寫保證public)這種寫法。
    • 在go的源碼中,例如log、fmt等常用的包, 大神們都是提供了兩種方式, 以log包爲例, 默認有一個實現類Std, 允許直接調用方法(比如log.Println()), 也允許自己實現(Logger struct),自行調用裏面的方法。
    • 而實現一個struct一般都會提供一個New*的方法, 個人認爲可能是爲了更好的迴避一些參數的初始化。 go中沒有java級別的重載, 也沒有php那種魔術方法或者給參數賦默認值。 用New可以實現一些參數取默認值, 也可以規避一些不必要對外暴露的參數, 當然也可以用New*變相實現一些重載。 下圖是Echo框架(一個高性能的api路由框架, 當然gin我也很喜歡)的初始化, 對外調用只需要Echo.New(),但實際 那麼一大坨參數。(舉例就好了。。有點跑偏了。。)
    •   	// New creates an instance of Echo.
        func New() (e *Echo) {
        	e = &Echo{
        		Server:    new(http.Server),
        		TLSServer: new(http.Server),
        		AutoTLSManager: autocert.Manager{
        			Prompt: autocert.AcceptTOS,
        		},
        		Logger:   log.New("echo"),
        		colorer:  color.New(),
        		maxParam: new(int),
        	}
        	e.Server.Handler = e
        	e.TLSServer.Handler = e
        	e.HTTPErrorHandler = e.DefaultHTTPErrorHandler
        	e.Binder = &DefaultBinder{}
        	e.Logger.SetLevel(log.ERROR)
        	e.StdLogger = stdLog.New(e.Logger.Output(), e.Logger.Prefix()+": ", 0)
        	e.pool.New = func() interface{} {
        		return e.NewContext(nil, nil)
        	}
        	e.router = NewRouter(e)
        	return
        }
      
    • 在對比一下bufio.Writer的初始化。 如果想的話, 可以直接 &Writer{…} 傳遞4個參數 也可以調用 NewWriterSize(io.Writer, int) 也可以NewWriter(io.Writer)。 甚至還有更多初始化的方法, 就是看是否需要更多的自定義配置。 用這種方式變相實現了參數默認值和重載(個人認爲)
    type Writer struct {
    	err error
    	buf []byte
    	n   int
    	wr  io.Writer
    }
    
    func NewWriterSize(w io.Writer, size int) *Writer {
    	// Is it already a Writer?
    	b, ok := w.(*Writer)
    	if ok && len(b.buf) >= size {
    		return b
    	}
    	if size <= 0 {
    		size = defaultBufSize
    	}
    	return &Writer{
    		buf: make([]byte, size),
    		wr:  w,
    	}
    }
    
    // NewWriter returns a new Writer whose buffer has the default size.
    func NewWriter(w io.Writer) *Writer {
    	return NewWriterSize(w, defaultBufSize)
    }
    
附錄2: demo
  1. producer:
type Listener1 struct {
	Status int
}

func (listener *Listener1) Check() int {

	return listener.Status
}

func (listener *Listener1) RollBack() error {
	log.Println("listener rollback")
	listener.Status = 2
	return nil
}

func (listener *Listener1) Execute() error {
	log.Println("execute local transaction")
	listener.Status = 1
	return nil
}

main.go

func main() {
	listen := Listener1{
		Status:0,
	}
	trans := common.NewTransaction(&listen)

	message := &common.Message{
		Id:1,
		Action:"add",
		Content:map[string]interface{}{
			"message" : "message content test",
		},
		Callback:"callback_url",
	}

	err := trans.MakeMessageTransaction(message)

	fmt.Println(err)
}

producer執行結果: (打印的日誌)

2019/10/29 16:45:51 common.transactionWithRabbit#mqSend: send message success: {"id":-1,"action":"prepare_ping","content":{},"callback":""}: 1 
2019/10/29 16:45:51 execute local transaction
2019/10/29 16:45:51 common.transactionWithRabbit#mqSend: send message success: {"id":1,"action":"add","content":{"message":"message content test"},"callback":"callback_url"}: 2 
<nil>
  1. consumer日誌(代碼不貼了)
// --- try消息
2019/10/29 16:46:54 Received a DeliveryTag: %s 1
2019/10/29 16:46:54 Received a message: %s [123 34 105 100 34 58 45 49 44 34 97 99 116 105 111 110 34 58 34 112 114 101 112 97 114 101 95 112 105 110 103 34 44 34 99 111 110 116 101 110 116 34 58 123 125 44 34 99 97 108 108 98 97 99 107 34 58 34 34 125]
2019/10/29 16:46:54 -1 : prepare_ping : map[] : 
2019/10/29 16:46:54 -1 : prepare_ping :  : map[]
2019/10/29 16:46:54 map[action:prepare_ping callback: content:map[] id:-1]

// --- 真實的消息
2019/10/29 16:46:54 Received a DeliveryTag: %s 2
2019/10/29 16:46:54 Received a message: %s [123 34 105 100 34 58 49 44 34 97 99 116 105 111 110 34 58 34 97 100 100 34 44 34 99 111 110 116 101 110 116 34 58 123 34 109 101 115 115 97 103 101 34 58 34 109 101 115 115 97 103 101 32 99 111 110 116 101 110 116 32 116 101 115 116 34 125 44 34 99 97 108 108 98 97 99 107 34 58 34 99 97 108 108 98 97 99 107 95 117 114 108 34 125]
2019/10/29 16:46:54 1 : add : map[message:message content test] : callback_url
2019/10/29 16:46:54 1 : add : callback_url : map[message:message content test]
2019/10/29 16:46:54 map[action:add callback:callback_url content:map[message:message content test] id:1]
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章