什麼是訊息佇列?
就是生產者生產一條訊息,傳送到這個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,每次新增節點,網路和磁碟負載都會增加,相對於單機來說,效能不但不會提升,反而可能下降。
但是由於交換器只是一張查詢表,並非實際的路由器,因此將交換器在整個叢集進行復制也不會損耗太多的效能,所以交換器在每個節點都會儲存一份,以便於查詢。