RabbitMQ是實現了高級消息隊列協議(AMQP)的開源消息代理軟件。RabbitMQ服務器是用Erlang語言編寫的,它可以爲你的應用提供一個通用的消息發送和接收平臺,並且保證消息在傳輸過程中的安全,RabbitMQ官網,RabbitMQ中文文檔。
安裝RabbitMQ
安裝EPEL源
[root@root ~]# yum -y install epel-release
安裝erlang
[root@root ~]# yum -y install erlang
安裝RabbitMQ
[root@root ~]# yum -y install rabbitmq-server
啓動並設置開機器啓動
在啓動RabbitMQ之前需要hostname的解析,要不然啓動不起來
[root@root ~]# cat /etc/hosts 127.0.0.1 localhost localhost.localdomain localhost4 localhost4.localdomain4 anshengme ::1 localhost localhost.localdomain localhost6 localhost6.localdomain6
[root@root ~]# systemctl start rabbitmq-server [root@root ~]# systemctl enable rabbitmq-server Created symlink from /etc/systemd/system/multi-user.target.wants/rabbitmq-server.service to /usr/lib/systemd/system/rabbitmq-server.service.
查看啓動狀態
[root@root ~]# netstat -tulnp |grep 5672 tcp 0 0 0.0.0.0:25672 0.0.0.0:* LISTEN 37507/beam.smp tcp6 0 0 :::5672 :::* LISTEN 37507/beam.smp
pika
pika模塊是官方認可的操作RabbitMQ的API接口。
安裝pika
pip3 install pika
pika:https://pypi.python.org/pypi/pika
測試
>>> import pika
Work Queues
如果你啓動了多個消費者,那麼生產者生產的任務會根據順序的依次讓消費者來執行,這就是Work Queues模式
rabbitmq-work-queues
生產者代碼
# _*_ codin:utf-8 _*_ import pika # 連接到RabbitMQ 這是一個阻塞的連接 connection = pika.BlockingConnection(pika.ConnectionParameters('192.168.56.100')) # 生成一個管道 channel = connection.channel() # 通過管道創建一個隊列 channel.queue_declare(queue='hello') # 在隊列內發送數據,body內容,routing_key隊列,exchange交換器,通過交換器往hello隊列內發送Hello World!數據 channel.basic_publish(exchange='', routing_key='hello', body='Hello World!') # 關閉連接 connection.close() 消費者代碼 # _*_ codin:utf-8 _*_ import pika # 連接到RabbitMQ 這是一個阻塞的連接 connection = pika.BlockingConnection(pika.ConnectionParameters('192.168.56.100')) # 生成一個管道 channel = connection.channel() # 如果消費者連接到這個隊列的時候,隊列沒有生成,那麼消費者就生成這個隊列,如果這個隊列已經生成了,那麼就忽略它 channel.queue_declare(queue='hello') # 回調函數 def callback(ch, method, properties, body): print(" [x] Received %r" % body) # 消費,當收到hello隊列的消息的時候就,就調用callback函數,no_ack消費者在處理任務的時候要不需要確認任務已經處理完成,改爲False則要確認 channel.basic_consume(callback, queue='hello', no_ack=True) # 開始接受任務,阻塞 channel.start_consuming() 持久化
隊列持久化
試想,如果我們的消費者在執行任務執行到一半時,突然down掉了,我們可以更改no_ack=False來讓消費者每次執行完成完成之後確認執行完畢了再把這個任務在隊列中移除移除掉,但是如果RabbitMQ的服務器停止我們的任務仍然會丟失。
首先,我們需要確保的RabbitMQ永遠不會在我們的隊列中失去,爲了做到這一點,我們需要把durable=True,聲明一個新名稱的隊列,爲task_queue:
channel.queue_declare(queue='task_queue', durable=True)
durable需要在生產者和消費者上面都需要寫上,且durable只會讓我們的隊列持久化,並不能夠讓消息持久化。
消息持久化
消息持久化只需要在添加消息的時候添加一個delivery_mode=2
channel.basic_publish(exchange='', routing_key='world', body='Hello World!', properties=pika.BasicProperties( # 2=消息持久化 delivery_mode=2, ))
在消費者的callback函數內添加以下代碼:
ch.basic_ack(delivery_tag = method.delivery_tag)
消息公平分發
每一個消費者同時只處理一個任務,比如說現在有三個消費者,剛開始來了三個任務,平均分配給了三個消費者,那麼這三個消費者目前都在同時執行任務,當第四個任務到來的時候依舊會分配給第一個消費者,第五個任務到來的時候會分配給第二個消費者,以此類推。
那麼以上的狀況有什麼不妥呢?譬如說不同的消費者執行任務的時間不同,我們現在需要的時候,當三個消費者都在執行任務的時候,比如說第二個消費者任務執行完了,其他消費者都還在執行任務,當第四個任務到來的時候希望交給第二個消費者,若要實現此功能,只需要在消費者加上一下代碼即可:
channel.basic_qos(prefetch_count=1)
完整的代碼如下
消費者代碼
#!/usr/bin/env python import pika import time connection = pika.BlockingConnection(pika.ConnectionParameters( host='192.168.56.100')) channel = connection.channel() 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(10) 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()
生產者代碼
#!/usr/bin/env python import pika import sys connection = pika.BlockingConnection(pika.ConnectionParameters( host='192.168.56.100')) channel = connection.channel() channel.queue_declare(queue='task_queue', durable=True) for n in range(10): message = "Hello World! %s" % (n + 1) channel.basic_publish(exchange='', routing_key='task_queue', body=message, properties=pika.BasicProperties( delivery_mode=2, # make message persistent )) print(" [x] Sent %r" % message) connection.close()
消息傳輸類型
之前的例子都基本都是1對1的消息發送和接收,即消息只能發送到指定的queue裏,但有些時候你想讓你的消息被所有的Queue收到,類似廣播的效果,這時候就要用到exchange了,
Exchange在定義的時候是有類型的,以決定到底是哪些Queue符合條件,可以接收消息
屬性 描述 fanout 所有bind到此exchange的queue都可以接收消息 direct 通過routingKey和exchange決定的那個唯一的queue可以接收消息 topic 所有符合routingKey(此時可以是一個表達式)的routingKey所bind的queue可以接收消息
fanout(發佈訂閱)
只要有消費者,那麼我生產者發佈一條消息的時候所有的消費者都會被收到
rabbitmq-fanout
# 消費者 import pika connection = pika.BlockingConnection(pika.ConnectionParameters(host='192.168.56.100')) channel = connection.channel() channel.exchange_declare(exchange='logs', type='fanout') # 不指定queue名字,rabbit會隨機分配一個名字,exclusive=True會在使用此queue的消費者斷開後,自動將queue刪除 result = channel.queue_declare(exclusive=True) # 獲取queue的name queue_name = result.method.queue # 把queue綁定到exchange channel.queue_bind(exchange='logs', queue=queue_name) def callback(ch, method, properties, body): print(" [x] %r" % body) channel.basic_consume(callback,queue=queue_name,no_ack=True) channel.start_consuming()
# 生產者 import pika connection = pika.BlockingConnection(pika.ConnectionParameters(host='192.168.56.100')) channel = connection.channel() # fanout發送給所有人 channel.exchange_declare(exchange='logs', type='fanout') channel.basic_publish(exchange='logs', routing_key='', body="Hello World!") connection.close()
直接(關鍵字)
RabbitMQ還支持根據關鍵字發送,即:隊列綁定關鍵字,發送者將數據根據關鍵字發送到消息exchange,exchange根據 關鍵字 判定應該將數據發送至指定隊列。
rabbitmq-direct
生產者代碼 #!/usr/bin/env python import pika import sys connection = pika.BlockingConnection(pika.ConnectionParameters( host='192.168.56.100')) 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()
消費者代碼 #!/usr/bin/env python import pika import sys connection = pika.BlockingConnection(pika.ConnectionParameters( host='192.168.56.100')) 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: sys.stderr.write("Usage: %s [info] [warning] [error]\n" % 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()
topic(模糊匹配)
在topic類型下,可以讓隊列綁定幾個模糊的關鍵字,之後發送者將數據發送到exchange,exchange將傳入”路由值“和 ”關鍵字“進行匹配,匹配成功,則將數據發送到指定隊列。
表達式符號說明:
符號 描述 # 表示可以匹配0個或多個單詞 * 表示只能匹配一個單詞
發送者路由值 隊列中 是否匹配 yangwen yangwen.* 不匹配 yangwen yangwen.# 匹配
消費者代碼
#!/usr/bin/env python import pika import sys connection = pika.BlockingConnection(pika.ConnectionParameters( host='192.168.56.100')) channel = connection.channel() channel.exchange_declare(exchange='topic_logs', type='topic') result = channel.queue_declare(exclusive=True) queue_name = result.method.queue binding_keys = sys.argv[1:] if not binding_keys: sys.stderr.write("Usage: %s [binding_key]...\n" % sys.argv[0]) sys.exit(1) for binding_key in binding_keys: channel.queue_bind(exchange='topic_logs', queue=queue_name, routing_key=binding_key) 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()
生產者代碼
#!/usr/bin/env python import pika import sys connection = pika.BlockingConnection(pika.ConnectionParameters( host='192.168.56.100')) 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()
RPC(遠程過程調用)
客戶端發送一個任務到服務端,服務端把任務的執行結果再返回給客戶端
RPC Server
# _*_coding:utf-8_*_ import pika connection = pika.BlockingConnection(pika.ConnectionParameters( host='192.168.56.100')) channel = connection.channel() # 聲明一個RPC QUEUE channel.queue_declare(queue='rpc_queue') def fib(n): if n == 0: return 0 elif n == 1: return 1 else: return fib(n - 1) + fib(n - 2) def on_request(ch, method, props, body): # 接受傳過來的值 n = int(body) print(" [.] fib(%s)" % n) # 交給fib函數進行斐波那契處理 response = fib(n) # 把結果發回去,此時消費者變成生產者 ch.basic_publish(exchange='', routing_key=props.reply_to, # 客戶端傳過來的UUID順便發回去 properties=pika.BasicProperties(correlation_id=props.correlation_id), body=str(response)) # 持久化 ch.basic_ack(delivery_tag=method.delivery_tag) # 同時只處理一個任務 channel.basic_qos(prefetch_count=1) channel.basic_consume(on_request, queue='rpc_queue') print(" [x] Awaiting RPC requests") channel.start_consuming()
RPC Client
# _*_coding:utf-8_*_ import pika import uuid class FibonacciRpcClient(object): def __init__(self): self.connection = pika.BlockingConnection(pika.ConnectionParameters( host='192.168.56.100')) self.channel = self.connection.channel() result = self.channel.queue_declare(exclusive=True) # 服務端返回處理完畢的數據新Queue名稱 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): # corr_id等於剛剛發送過去的ID,就代表這條消息是我的 if self.corr_id == props.correlation_id: self.response = body def call(self, n): self.response = None # 生成一個唯一ID,相當於每個任務的ID self.corr_id = str(uuid.uuid4()) self.channel.basic_publish(exchange='', routing_key='rpc_queue', properties=pika.BasicProperties( # 讓服務端處理完成之後把數據放到這個Queue裏面 reply_to=self.callback_queue, # 加上一個任務ID correlation_id=self.corr_id, ), body=str(n)) while self.response is None: # 不斷地去Queue接受消息,但不是阻塞的,而是一直循環的去取 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)