訊息佇列-一篇讀懂rabbitmq(生命週期,confirm模式,延遲佇列,叢集)

莫弦然發表於2020-07-11

 

什麼是訊息佇列?

就是生產者生產一條訊息,傳送到這個rabbitmq,消費者連線rabbitmq並且進行消費,生產者和消費者並需要知道對方是如何工作的,從而實現程式之間的解耦,非同步和削峰,這也就是訊息佇列的作用。

使用的場景也有很多,比如使用者支付購買之後的傳送簡訊,增加使用者積分等等,只要能將業務邏輯抽象出來,就能很好得使用它。

 

下面進入正題:

先來介紹一下基本概念和參與生命週期的各個成員。

publisher:訊息生產者,負責建立訊息,併傳送到代理伺服器(rabbitmq)

message:傳送的訊息,由 有效負載(payload) 和 標籤 (label) 組成

exchange:交換器,負責接收訊息並路由給伺服器的佇列

queue:訊息佇列,就是訊息最後要去的地方。然後等待消費者取走並消費

consumer:訊息消費者,與生產者對應,程式的另外一方,負責消費資訊,並完成相應的業務邏輯

channel:通道,在tcp之上建立的通道,負責傳送訊息。佇列的傳輸都是基於通道來完成的。

broker:訊息佇列伺服器實體

下面來解析一下這張圖,這張圖是網上找的,雖然不夠詳細,但是勉強能用。

 

準備前提,開啟rabbitmq服務,生命佇列和交換器,並將兩者進行繫結。

 

生產邏輯:

publisher 通過 broker伺服器ip 和 埠 嘗試建立與 broker 的 tcp 連線,連線成功之後,會嘗試驗證驗證使用者名稱和密碼,如果錯誤,則拒絕訪問。

驗證成功之後,在tcp上建立一個 通道(channel) ,publisher 通過通道,傳送訊息到 broker,進入指定的虛擬主機virtual host。然後查詢到交換器繫結的佇列,並將訊息推送進入佇列。

 

消費邏輯:

consumer 建立連線和驗證和生產者一樣。接下來通過佇列名,直接找到佇列進行監聽並消費。

 

有幾個比較重要的知識點:

1.  rabbitmq的交換器並不是真正意義的交換器,它本質上其實就是一張表,裡面存放和交換器名稱和訊息佇列的對映關係。所以說佇列的傳輸都是通過通道來完成的。

2.  通道存在的意義在於 建立和銷燬tcp連線非常消耗資源

 

 

交換器的型別

當生產的訊息進入虛擬主機時,會去尋找一張表,就是交換器和佇列對映關係的一張表。而交換器的型別也分了好多種,可以根據不同的場景自由選擇。

目前總共分了4種,direct,fanout,topic,headers。其中headers因為效能問題幾乎不在使用,這裡就不做過多的討論。

1.direct

direct是直接,完全匹配,單播的模式。

php簡單程式碼實現:

//建立佇列
$q = new AMQPQueue($channel);
$q->setName($q_name);
$q->setFlags(AMQP_DURABLE); //持久化
$q->declare();

//建立交換機物件
$ex = new AMQPExchange($channel);
$ex->setName($e_name);
$ex->setType(AMQP_EX_TYPE_DIRECT); //direct型別
$ex->setFlags(AMQP_DURABLE); //持久化
$ex->declare();

//繫結交換機與佇列,並指定路由鍵
$q->bind($e_name, $k_route);

//生產訊息
$ex->publish($message, $k_route)

特性: 當生產訊息時,未指定交換器,則會預設使用 (AMQP default) 交換器,然後路由到 和路由鍵名稱相同的佇列中去

 

 

2.fanout

如果說 direct 是 單對單 的關係,那麼 fanout就是單對多的關係,即一個交換器對應多個佇列。

fanout交換器不通過路由鍵路由到佇列,而是通過將佇列繫結在交換器上,當訊息進來時,直接路由到該交換器繫結的佇列去。

 

 

3.topic

topic交換器通過路由鍵,將自動匹配允許匹配的佇列,相比fanout,更加靈活,不過對架構要求更高。如下圖所示

$e_name = 'logs-exchange'; //交換機名
$q_name = 'msg-inbox-errors'; //佇列名

//建立佇列
$q = new AMQPQueue($channel);
$q->setName($q_name);
$q->setFlags(AMQP_DURABLE); //持久化
$q->declare();

//建立交換機物件
$ex = new AMQPExchange($channel);
$ex->setName($e_name);
$ex->setType(AMQP_EX_TYPE_TOPIC); //direct型別
$ex->setFlags(AMQP_DURABLE); //持久化
$ex->declare();


$q->bind($e_name, '*.msg-inbox');

$ex->publish($message, 'wonima.msg-inbox');

 

高階特性:

為了確保訊息可靠性,有兩種處理方式.

1.rabbitmq事務

事務主要是對通道進行設定,示例程式碼如下

$channel->startTransaction(); //開始事務
for($i=0; $i<5; ++$i){
    $message = "TEST MESSAGE! 測試訊息!";
    $message = $message.$i."---";
    echo "Send Message:".$ex->publish($message, 'xxxx')."\n";

}
$channel->commitTransaction(); //提交事務

經測驗,使用事務之後,效能會造成相當大的影響,與不實用事務相比,效能可以相差百倍以上。

 

2.confirm 模式

當通道設定未 confirm 模式的時候,每一條訊息都會獲的唯一的id。當消費者接收到訊息的時候,自動傳送 或 手動傳送訊息  進行訊息確認。

//建立佇列
$q = new AMQPQueue($channel);
$q->setName($q_name);
$q->setFlags(AMQP_DURABLE); //持久化
echo "Message Total:".$q->declare()."\n";

//第一種:自動應答
//$q->consume('processMessage', AMQP_AUTOACK); //自動ACK應答

//第二種:手動應答 $q->consume('processMessage'); /** * 消費回撥函式 * 處理訊息 */ function processMessage($envelope, $queue) { $msg = $envelope->getBody(); sleep(2); $myfile = fopen("newfile2.txt", "a+") or die("Unable to open file!"); $txt = $msg.time()."\n"; fwrite($myfile, $txt); fclose($myfile); echo $msg.time()."\n"; //處理訊息 $q->ack($envelope->getDeliveryTag()); //手動傳送ACK應答 }

 

應答模式最大的好處是就是非同步,執行效率高。事務和應答模式相比,後者使用更加頻繁,前者幾乎沒有見到過。

 

延遲佇列

首先宣告rabbitmq是不支援延遲佇列的,但是我們可以利用死信佇列來完成。

 

 

實現延遲佇列也有多種方式:

第一種:設定死信佇列,並將 過期時間 加到佇列裡面

try {
    $conn = new AMQPConnection($connectConfig);
    $conn->connect();
    if (!$conn->isConnected()) {

        echo 'rabbit-mq 連線錯誤:', json_encode($connectConfig);
        exit();
    }
    $channel = new AMQPChannel($conn);
    if (!$channel->isConnected()) {
        echo 'rabbit-mq Connection through channel failed:', json_encode($connectConfig);
        exit();
    }
    $exchange = new AMQPExchange($channel);
    $exchange->setFlags(AMQP_DURABLE);//持久化
    $exchange->setName($params['exchangeName'] ?: '');
    $exchange->setType(AMQP_EX_TYPE_DIRECT); //direct型別
    $exchange->declareExchange();

    $queue = new AMQPQueue($channel);
    $queue->setName($params['queueName'] ?: '');
    $queue->setFlags(AMQP_DURABLE);
    $queue->setArguments(array(
        'x-dead-letter-exchange' => 'last_exchange',
        'x-dead-letter-routing-key' => 'last_route',
        'x-message-ttl' => 10000,
    ));
    $queue->declareQueue();

    //繫結
    $queue->bind($params['exchangeName'], $params['routeKey']);

    $exchange2 = new AMQPExchange($channel);
    $exchange2->setFlags(AMQP_DURABLE);//持久化
    $exchange2->setName('last_exchange');
    $exchange2->setType(AMQP_EX_TYPE_DIRECT); //direct型別
    $exchange2->declareExchange();

    $queue2 = new AMQPQueue($channel);
    $queue2->setName('last_queue');
    $queue2->setFlags(AMQP_DURABLE);
    $queue2->declareQueue();

    $queue2->bind('last_exchange', 'last_queue');

} catch (Exception $e) {

}

$time = time();
//生成訊息
$exchange->publish((string)$time, $params['routeKey'], AMQP_MANDATORY, [
    'delivery_mode' => 2,
]);

 

第二種:設定死信佇列,並將 過期時間 加到訊息裡面,這一種更加自由。

$msg = [
    'x-message-ttl' => 5,
    'ttl' => 5,
    'body' => time()
];
$msg = json_encode($msg);
$exchange->publish($msg, '', AMQP_MANDATORY, ['delivery_mode' => 2]);

 

第三種:使用延遲外掛

 

叢集

先來談談rabbitmq的叢集是如何執行的

 

當你開啟來兩個rabbitmq(節點)服務,並將其組成為一個叢集。每個節點並不會將所有的佇列進行拷貝,後設資料依舊儲存在單個節點當中,其他節點則是通過指標。

舉個例子:節點a和節點b組成了一個叢集,節點a儲存著一堆後設資料 c 和 後設資料d的指標,用來指向節點b,節點b儲存一堆後設資料d 和 後設資料 c的指標,用來指向節點a。

這樣做有兩個原因

1 儲存空間 :如果一個節點儲存了1gb的資料,再新增節點,只會帶來一摸一樣的1gb的資料,非常浪費磁碟空間

2 效能:對於持久化訊息來說,每一條訊息都會觸發磁碟io,每次新增節點,網路和磁碟負載都會增加,相對於單機來說,效能不但不會提升,反而可能下降。

但是由於交換器只是一張查詢表,並非實際的路由器,因此將交換器在整個叢集進行復制也不會損耗太多的效能,所以交換器在每個節點都會儲存一份,以便於查詢。

 

相關文章