RabbitMQ是幹什麼的呢?
解釋RabbitMQ,就不得不提到AMQP(Advanced Message Queuing Protocol)協議。AMQP協議是一種基於網絡的消息傳輸協議,它能夠在應用或組織之間提供可靠的消息傳輸。RabbitMQ是該AMQP協議的一種實現,利用它,可以將消息安全可靠的從發送方傳輸到接收方。簡單的說,就是消息發送方利用RabbitMQ將信息安全的傳遞給接收方。
可靠的消息傳輸爲什麼一定要用RabbitMQ呢?直接用TCP,HTTP不OK?
在回答這個問題時,我比較模糊。應該說這個應用的範圍不同吧,TCP協議支持在IP之間進行消息傳輸,而RabbitMQ是根據關鍵字進行消息的分配和傳輸。TCP可以將消息從192.168.1.2傳輸到192.168.1.3。但是它不能將消息根據關鍵字進行傳輸吧,比如,給定一個關鍵字’key‘,你知道要將消息傳輸到哪嗎?呵呵,RabbitMQ知道。
怎麼根據關鍵字發送消息呢?
這個嘛!理解比較簡單,但介紹起來有點長。要理解這個發送機制,首先要對RabbitMQ的幾個定義搞清楚:
1 Server,要利用RabbitMQ進行消息傳輸,那麼就得有一個運行的RabbitMQ服務,我們可以稱爲Server
2 Producer,既然是傳輸消息,那得有一個消息的發送者,這裏我們成爲生產者(Producer)
3 Consumer,消息的接收者,這裏稱爲消息的消費這(Consumer)
4 Exchange,在生產者將消息發出後,消息往哪走呢?這個得由Exchange來決定,可以將它看作一個交換機,它 根據消息自帶的特徵信息(這裏指的是routing_key),進行發送。發給誰呢?不是接收者,是下面的Queue。
5 Queue,一個Exchange可以對應多個Queue,每個Queue都會在定義時,聲明自己要接收消息的特徵信息(routing_key)。Exchange根據Queue和消息的routing_key的匹配情況進行發送。消息到達Queue後,Consumer就可以將消息從Queue取出了。
一個Server可以聲明n多Exchange,一個Exchange可以對應n多Queue。Exchange和Queue都是存在Server內部的。簡化點,可以理解爲一個Producer與一個Exchange綁定,Producer將消息交給Exchange,Exchange負責發送消息。一個Consumer與一個Queue綁定,當Queue中有消息時,直接取就行了,管它怎麼來的。而消息在Exchange在Queue之間的怎麼傳遞,是由RabbitMQ負責的。我們只需要將消息交給Exchange,然後在Queue中取就行了。(當然還要多點步驟:聲明消息和Queue的特徵信息,將Queue與Exchange進行關聯)
用一個來自官網的圖片說明下:
P: Producer, X: Exchange, amq: Queue, C: Consumer
在消息的傳輸機制上理解,較爲簡單。但在源碼級別上,進行使用要爲複雜。這個我個人要歸功於RabbitMQ客戶端的幾種封裝,讓我是分不清東南西北。下面是官網上列舉的六個應用實例,有易到難,比較容易理解。
1 直接發送模式
不聲明Exchange,即採用默認的Exchange。默認的Exchange可以根據routing_key將消息直接發送給特定Queue。
注意:
1) 這種匹配是消息的routing_key與Queue的名子直接進行匹配,而不是與Queue的routing_key。
2) 消息並不能直接發送給queue,這裏是經過一個名爲''的Exchange進行發送的。
Producer.py
#!/usr/bin/env python
import pika
connection = pika.BlockingConnection(pika.ConnectionParameters( host='localhost'))
channel = connection.channel()
#這裏要對queue進行聲明,已確定Queue存在,如果不存在則創建名爲‘hello'的queue。
#否則消息發送時,queue不存在,消息會被直接丟棄
channel.queue_declare(queue='hello')
channel.basic_publish(exchange='', routing_key='hello', body='Hello World!')
print " [x] Sent 'Hello World!'"
connection.close()
Consumer.py
#!/usr/bin/env python
import pika
connection = pika.BlockingConnection(pika.ConnectionParameters( host='localhost'))
channel = connection.channel()
channel.queue_declare(queue='hello')
print ' [*] Waiting for messages. To exit press CTRL+C'
def callback(ch, method, properties, body):
print " [x] Received %r" % (body,)
channel.basic_consume(callback, queue='hello', no_ack=True)
channel.start_consuming()
2 工作列隊模式
該模型適用於分發資源密集型的任務。假設一下你需要進行10次計算圓周率的操作,每次計算到小數點後n位,每次耗時1一個小時(這只是一個假設,一般沒有比要進行10重複操作的。現在就當作你真的有這個必要)。如果只用一臺機器計算,需要十個小時。但如果我們將這十個任務分發給十臺進行計算,那麼只需一個小時。下面的模型就是適用於這種分發任務的。
採用這種模式,列隊消息以輪詢(round_robin)的方式將消息平均的發給所有與Queue關聯的Consumer,一般情況下,每個Consumer都平均的分攤任務。
注意:
1) 在目前的情況下,消息一旦被Consumer取出,就立即從列隊中消除。這樣當woker執行到任務中途失敗時,該任務的信息也丟失了,不能重新開始。
2) RabbitMQ提供一種消息認證機制(message acknowledgments),只有當Consumer返回一個ack時,它纔會將消息從列隊中刪除。如果當Consumer斷開連接時,依然沒有收到ack,那麼它就會重新分發給消息。
3) RabbitMQ不允許以新的屬性來重新定義Queue,所以這裏我們需要給Queue換個新名子
Producer.py
#!/usr/bin/env python
import pika
import sys
connection = pika.BlockingConnection(pika.ConnectionParameters( host='localhost'))
channel = connection.channel()
#Queue聲明持久化
channel.queue_declare(queue='task_queue', durable=True)
message = ' '.join(sys.argv[1:]) or "Hello World!"
channel.basic_publish(exchange='', routing_key='task_queue', body=message,
properties=pika.BasicProperties( delivery_mode = 2, # 消息聲明持久化 ))
print " [x] Sent %r" % (message,)
connection.close()
Consumer.py
#!/usr/bin/env python
import pika
import time
connection = pika.BlockingConnection(pika.ConnectionParameters( host='localhost'))
channel = connection.channel()
#Queue聲明持久化
channel.queue_declare(queue='task_queue', durable=True)
print ' [*] Waiting for messages. To exit press CTRL+C'
def callback(ch, method, properties, body):
print " [x] Received %r" % (body,)
time.sleep( body.count('.') )
print " [x] Done"
#返回消息認證
ch.basic_ack(delivery_tag = method.delivery_tag)
channel.basic_qos(prefetch_count=1)
channel.basic_consume(callback, queue='task_queue')
channel.start_consuming()
3 廣播(fanout)模型
前面兩種模型,每條消息只能被一個Consumer獲取。原因在於:使用默認的Exchange,它只能將每條消息發給一個或零個Queue中。這裏我們將使用一種類型爲fanout的Exchange,它可以將消息發送給每一個於它關聯的Queue,這樣每個Consumer都可以獲取相同的消息。
Producer.py
#!/usr/bin/env python
import pika
import sys
connection = pika.BlockingConnection(pika.ConnectionParameters( host='localhost'))
channel = connection.channel()
#定義一個類型爲fanout的Exchange
channel.exchange_declare(exchange='logs', type='fanout')
message = ' '.join(sys.argv[1:]) or "info: Hello World!"
channel.basic_publish(exchange='logs', routing_key='', body=message)
print " [x] Sent %r" % (message,)
connection.close()
Consumer.py
#由於此時Exchange將消息發給所有的Queue,所以Queue無需命名, #此處沒有給queue指定名稱,MQ會給它產生一個隨機名子。
4 direct模型
到目前爲止,Consumer只能被動的隨機接收一部分消息,或者接收全部,不能自主選擇接收哪一部分消息。該消息路由模型,可以使得Consumer指定它想要接收到消息。
將Exchange聲明爲direct類型:
channel.exchange_declare(exchange='direct_logs', type='direct')
將Queue與Exchange綁定,注意此處queue與Exchange綁定時,指定了一個參數routing_key。如上面兩圖所示,一個queue可以以多個routing_key與Exchange進行綁定,多個不同的Queue可以以相同routing_key與同一個Exchange進行綁定。遺留一個問題,一個Queue可不可以與多個Exchange進行綁定呢?:
channel.queue_bind(exchange=exchange_name, queue=queue_name, routing_key='black')
發送消息:
channel.basic_publish(exchange='direct_logs', routing_key=severity, body=message)
在發送消息時,也會指定一個routing_key。當Exchange在決定將消息發給哪幾個Queue時,它會將該routing_key與Queue綁定時指定的routing_key進行匹配,相同的Queue則可接收消息。
Producer.py
#!/usr/bin/env python
import pika
import sys
connection = pika.BlockingConnection(pika.ConnectionParameters( host='localhost'))
channel = connection.channel()
channel.exchange_declare(exchange='direct_logs', type='direct')
severity = sys.argv[1] if len(sys.argv) > 1 else 'info'
message = ' '.join(sys.argv[2:]) or 'Hello World!'
channel.basic_publish(exchange='direct_logs', routing_key=severity, body=message)
print " [x] Sent %r:%r" % (severity, message)
connection.close()
Consumer.py
#!/usr/bin/env python
import pika
import sys
connection = pika.BlockingConnection(pika.ConnectionParameters( host='localhost'))
channel = connection.channel()
channel.exchange_declare(exchange='direct_logs', type='direct')
result = channel.queue_declare(exclusive=True)
queue_name = result.method.queue
severities = sys.argv[1:]
if not severities:
print >> sys.stderr, "Usage: %s [info] [warning] [error]" % \ (sys.argv[0],)
sys.exit(1)
for severity in severities:
channel.queue_bind(exchange='direct_logs', queue=queue_name, routing_key=severity)
print ' [*] Waiting for logs. To exit press CTRL+C'
def callback(ch, method, properties, body):
print " [x] %r:%r" % (method.routing_key, body,)
channel.basic_consume(callback, queue=queue_name, no_ack=True)
channel.start_consuming()
5 Topic模型
direct模型是不是已經很好用、很靈活了?不,它的靈活度還不夠。看它在routing_key進行匹配時,只能將兩個完全相同的routing_key進行匹配,這個不夠好,要是能用正則表達式進行匹配,那就完美了。
Topic模型,雖然沒有實現用正則表達式進行匹配,但是它進步了一小步。實現了對任意的單詞進行匹配:
*
(星號) 可以匹配任意一個單詞
#
(警號) 可以匹配任意零個或多個單詞
Producer.py
#!/usr/bin/env python
import pika
import sys
connection = pika.BlockingConnection(pika.ConnectionParameters( host='localhost'))
channel = connection.channel()
channel.exchange_declare(exchange='topic_logs', type='topic')
routing_key = sys.argv[1] if len(sys.argv) > 1 else 'anonymous.info'
message = ' '.join(sys.argv[2:]) or 'Hello World!'
channel.basic_publish(exchange='topic_logs', routing_key=routing_key, body=message)
print " [x] Sent %r:%r" % (routing_key, message)
connection.close()
Consumer.py
[object Object]
6 遠程調用模型(RPC)
這個是對RabbitMQ消息傳輸的實際應用,在Openstack中,各個組件間的調用就是通過RabbitMQ實現的,這樣是我爲什麼學習RabbitMQ的原因了。
個人猜想,Client端執行遠程調用(RPC CALL),通過一個Queue將函數名、傳參、已經用於傳輸返回結果的臨時聲明的Queue(這個Queue在聲明時,無需指定名子,由RabbitMQ自動分配,這樣還可以避免命名衝突),Server端接收到消息後,調用相應的函數進行處理,並將結果通過默認Exchange傳給臨時Queue,這樣就完成了一個遠程調用。
但是這個猜想有問題:
每進行一次RPC CALL就要聲明一個臨時Queue。這個有點浪費。有多浪費我也不知道,沒測試過。
我們可以爲每個Client指定一個用於返回結果的Queue,這樣就不用每次聲明瞭。每次RPC CALL時,綁定一個correlation_id,這樣使得返回的結果不會混亂。
注意:
在返回結果的Queue中可能存在髒數據,比如,Server在將結果傳輸到Queue後,還沒來得及返回消息確認就掛了。那麼先前發的調用消息就不會消除,在Server下次啓動時會再次執行,並再次返回結果。這就有了髒數據。所以,面對Queue裏的髒數據,我們只需忽略就行了。
Server.py
[object Object]
Client.py
#!/usr/bin/env python
import pika
import uuid
class FibonacciRpcClient(object):
def __init__(self):
self.connection = pika.BlockingConnection(pika.ConnectionParameters(host='localhost'))
self.channel = self.connection.channel()
result = self.channel.queue_declare(exclusive=True)
self.callback_queue = result.method.queue
self.channel.basic_consume(self.on_response, no_ack=True, queue=self.callback_queue)
def on_response(self, ch, method, props, body):
if self.corr_id == props.correlation_id:
self.response = body
def call(self, n):
self.response = None
self.corr_id = str(uuid.uuid4())
self.channel.basic_publish(exchange='', routing_key='rpc_queue',properties=pika.BasicProperties( reply_to = self.callback_queue, correlation_id =self.corr_id, ), body=str(n))
while self.response is None:
self.connection.process_data_events()
return int(self.response)
fibonacci_rpc = FibonacciRpcClient()
print " [x] Requesting fib(30)"
response = fibonacci_rpc.call(30)
print " [.] Got %r" % (response,)
原文:http://blog.sina.com.cn/s/blog_85998e380101amp6.html