這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。
寫在前面
在我們的生產環境中搭了兩台rabbitmq, 前面架設了一台HAProxy做負載平衡,當我們的用戶端串連到HAProxy,然後由HAProxy負責將連結分配給其中一台rabbitmq,用戶端需要需要負責斷線重連,需要將擷取的資料,分配訊息給相應的處理方法,然後還需要回複給rabbitmq ACK,這其中用戶端需要負責斷線重連的邏輯是很重要的,因為有可能用戶端和HAProxy的串連是正常的,但是HAProxy和rabbitmq的連結因為網路波動斷開了,那麼這個時候用戶端其實是沒有工作的,並且會在rabbitmq中不斷積累訊息。
下面的內容給出了一個比較完善的處理邏輯,以供參考。
實戰
定義介面
從之前的說明來看,這是一個典型的觀察者模式,由RabbitMQ對象負責維護串連,擷取訊息,然後定義若干個接收者註冊到RabbitMQ對象中,這時候RabbitMQ對象一旦收到了由RabbitMQ發來的資料,就可以將該訊息分發到相應的接收者去處理,當接收者處理完成後告訴RabbitMQ對象訊息消費成功,然後由RabbitMQ對象回複RabbitMQ ACK,當然可以在其中加上重試機制,接收者有可能因為某種情況處理失敗,那麼每隔一定的時間RabbitMQ對象需要重新調用一次接收者重新處理,直至成功,然後再返回ACK。
先來看看基本的介面約定
// Receiver 觀察者模式需要的介面// 觀察者用於接收指定的queue到來的資料type Receiver interface { QueueName() string // 擷取接收者需要監聽的隊列 RouterKey() string // 這個隊資料行繫結的路由 OnError(error) // 處理遇到的錯誤,當RabbitMQ對象發生了錯誤,他需要告訴接收者處理錯誤 OnReceive([]byte) bool // 處理收到的訊息, 這裡需要告知RabbitMQ對象訊息是否處理成功}
這樣就將接收者和RabbitMQ對象之間就解耦了,這樣後期如果需要添加新的接收者那就很容易了。
下面來看一看RabbitMQ對象的定義:
這裡用到的RabbitMQ client是RabbitMQ官方的 Github
// RabbitMQ 用於管理和維護rabbitmq的對象type RabbitMQ struct { wg sync.WaitGroup channel *amqp.Channel exchangeName string // exchange的名稱 exchangeType string // exchange的類型 receivers []Receiver}// New 建立一個新的操作RabbitMQ的對象func New() *RabbitMQ { // 這裡可以根據自己的需要去定義 return &RabbitMQ{ exchangeName: ExchangeName, exchangeType: ExchangeType, }}
RabbitMQ對象的初始化操作
這裡RabbitMQ對象需要初始化交換器,註冊接收者並初始化接收者監聽的Queue,以及斷線重連的機制
// prepareExchange 準備rabbitmq的Exchangefunc (mq *RabbitMQ) prepareExchange() error { // 申明Exchange err := mq.channel.ExchangeDeclare( mq.exchangeName, // exchange mq.exchangeType, // type true, // durable false, // autoDelete false, // internal false, // noWait nil, // args ) if nil != err { return err } return nil}// run 開始擷取串連並初始化相關操作func (mq *RabbitMQ) run() { if !config.Global.RabbitMQ.Refresh() { log.Errorf("rabbit重新整理串連失敗,將要重連: %s", config.Global.RabbitMQ.URL) return } // 擷取新的channel對象 mq.channel = config.Global.RabbitMQ.Channel() // 初始化Exchange mq.prepareExchange() for _, receiver := range mq.receivers { mq.wg.Add(1) go mq.listen(receiver) // 每個接收者單獨啟動一個goroutine用來初始化queue並接收訊息 } mq.wg.Wait() log.Errorf("所有處理queue的任務都意外退出了") // 理論上mq.run()在程式的執行過程中是不會結束的 // 一旦結束就說明所有的接收者都退出了,那麼意味著程式與rabbitmq的串連斷開 // 那麼則需要重新串連,這裡嘗試銷毀當前串連 config.Global.RabbitMQ.Distory()}// Start 啟動Rabbitmq的用戶端func (mq *RabbitMQ) Start() { for { mq.run() // 一旦串連斷開,那麼需要隔一段時間去重連 // 這裡最好有一個時間間隔 time.Sleep(3 * time.Second) }}
註冊接收者
// RegisterReceiver 註冊一個用於接收指定隊列指定路由的資料接收者func (mq *RabbitMQ) RegisterReceiver(receiver Receiver) { mq.receivers = append(mq.receivers, receiver)}// Listen 監聽指定路由發來的訊息// 這裡需要針對每一個接收者啟動一個goroutine來執行listen// 該方法負責從每一個接收者監聽的隊列中擷取資料,並負責重試func (mq *RabbitMQ) listen(receiver Receiver) { defer mq.wg.Done() // 這裡擷取每個接收者需要監聽的隊列和路由 queueName := receiver.QueueName() routerKey := receiver.RouterKey() // 申明Queue _, err := mq.channel.QueueDeclare( queueName, // name true, // durable false, // delete when usused false, // exclusive(排他性隊列) false, // no-wait nil, // arguments ) if nil != err { // 當隊列初始化失敗的時候,需要告訴這個接收者相應的錯誤 receiver.OnError(fmt.Errorf("初始化隊列 %s 失敗: %s", queueName, err.Error())) } // 將Queue綁定到Exchange上去 err = mq.channel.QueueBind( queueName, // queue name routerKey, // routing key mq.exchangeName, // exchange false, // no-wait nil, ) if nil != err { receiver.OnError(fmt.Errorf("綁定隊列 [%s - %s] 到交換器失敗: %s", queueName, routerKey, err.Error())) } // 擷取消費通道 mq.channel.Qos(1, 0, true) // 確保rabbitmq會一個一個發訊息 msgs, err := mq.channel.Consume( queueName, // queue "", // consumer false, // auto-ack false, // exclusive false, // no-local false, // no-wait nil, // args ) if nil != err { receiver.OnError(fmt.Errorf("擷取隊列 %s 的消費通道失敗: %s", queueName, err.Error())) } // 使用callback消費資料 for msg := range msgs { // 當接收者訊息處理失敗的時候, // 比如網路問題導致的資料庫連接失敗,redis串連失敗等等這種 // 通過重試可以成功的操作,那麼這個時候是需要重試的 // 直到資料處理成功後再返回,然後才會回複rabbitmq ack for !receiver.OnReceive(msg.Body) { log.Warnf("receiver 資料處理失敗,將要重試") time.Sleep(1 * time.Second) } // 確認收到本條訊息, multiple必須為false msg.Ack(false) }}
整合到一起
接收者的邏輯這裡就不寫的,只要根據實際的商務邏輯並實現了介面就可以了,這個比較容易。
擷取RabbitMQ的串連
rabbitmqConn, err = amqp.Dial(url)if err != nil { panic("RabbitMQ 初始化失敗: " + err.Error())}rabbitmqChannel, err = rabbitmqConn.Channel()if err != nil { panic("開啟Channel失敗: " + err.Error())}
// 啟動並開始處理資料 func main() { // 假設這裡有一個AReceiver和BReceiver aReceiver := NewAReceiver() bReceiver := NewBReceiver() mq := rabbitmq.New() // 將這個接收者註冊到 mq.RegisterReceiver(aReceiver) mq.RegisterReceiver(bReceiver) mq.Start()}
應用情境
舉一個我們自己用於生產環境的例子:
我們主要是用於接收Mysql的變更,並累加式更新Elasticsearch的索引,負責資料庫變更監聽的服務用的是Canel,它偽裝成一個mysql slave,用於接收mysql binlog的變更通知,然後將變更的資料格式化後寫入RabbitMQ,然後由go實現的消費者去訂閱資料庫的變更通知。
由於用戶端並不關心表中哪些欄位發生了變化,只需要知道資料庫指定的表有變更,那麼就將此次變更寫入Elasticsearch,這個邏輯對於每一張監聽的表都是一樣的,那麼這樣我們就可以將需要監聽表變更的操作完全配置化,我只要再設定檔中指定一個接收者並指定待消費的隊列,然後就可以由程式自動產生若干的接收者並且依次註冊進RabbitMQ對象中,這樣我們只需要針對一些特殊的操作寫相應地代碼即可,這樣大大簡化了我們地工作量,來看一看設定檔:
[[autoReceivers]] receiverName = "article_receiver" database = "blog" tableName = "articles" primaryKey = "articleId" queueName = "articles_queue" routerKey = "blog.articles.*" esIndex = "articles_idx"[[autoReceivers]] receiverName = "comment_receiver" database = "blog" tableName = "comments" primaryKey = "commentId" queueName = "comments_queue" routerKey = "blog.comments.*" esIndex = "comments_idx"
這個時候就需要調整一下接收者地註冊函數了:
// WalkReceivers 使用callback遍曆處理所有的接收者// 這裡地callback就是上面提到地 mq.RegisterReceiverfunc WalkReceivers(callback func(rabbitmq.Receiver)) { successCount := 0 // 遍曆每一個配置項,依次產生需要自動建立接收者 // 這裡的congfig是統一擷取配置地對象,大家根據實際情況處理就可以了 for _, receiverCfg := range config.Global.AutoReceivers { // 驗證每一個接收者的合法性 err := receiverCfg.Validate() if err != nil { log.Criticalf("產生 %s 失敗: %s, 使用該配置: %+v", receiverCfg.ReceiverName, err.Error(), receiverCfg) continue } // 將接收者註冊到監聽rabbitmq的對象中 callback(NewAutoReceiver(receiverCfg)) log.Infof("產生 %s 成功使用該配置: %+v", receiverCfg.ReceiverName, receiverCfg) successCount++ } if successCount != len(config.Global.AutoReceivers) || successCount == 0 { panic("無法啟動所有的接收者,請檢查配置") } // 如有必要,這裡可以繼續添加需要手工建立的接收者}
啟動地流程也需要進行微調一下:
func registeAndStart() { mq := rabbitmq.New() // 遍曆所有的receiver,將他們註冊到rabbitmq中去 WalkReceivers(mq.RegisterReceiver) log.Info("初始化所有的Receiver成功") mq.Start()}
這樣就定義好了兩個receiver,啟動程式後,就可以接收到資料庫地變更並更新elasticsearch中地索引了,非常地方便。
寫在最後
這個是對平時工作地一點總結,希望可以給大家帶來協助,如果文中有紕漏之處,還望指正,這裡完整地代碼就不貼了,文章裡已經搭起了一個完整地架構了,剩下地就是商務邏輯了,如果有必要地化,我會整理成一個完整地項目放到github上。