使用go作為RabbitMQ消費者的正確姿勢

來源:互聯網
上載者:User
這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。

寫在前面

在我們的生產環境中搭了兩台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上。

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.