我們需要在Hello World的基礎上進行修改,結合上一章節未做解釋的參數,一一處理,我們先介紹以下幾個關鍵詞,最後再進行總結和整合。
循環調度
使用工作隊列的一個好處就是它能夠並行的處理隊列。如果堆積了很多任務,我們只需要添加更多的工作者(workers)就可以,然後擴展也很簡單。
默認來說,RabbitMQ會按順序得把消息發送給每個消費者(consumer)。平均每個消費者都會收到同等數量得消息。這種發送消息得方式叫做——輪詢(round-robin)。
消息確認
簡單來說,當RabbitMQ將任務丟給worker(也就是消費者)處理時,只有worker給RabbitMQ返回確認消息時,RabbitMQ才認爲這條消息被正確處理完成,繼而將消息移除。否則RabbitMQ認爲消息沒有處理還會繼續保留,當worker掛掉時,RabbitMQ會將這個任務交給另一個worker來處理,直到處理完成。
基於此,RabbitMQ提供了消息響應(acknowledgments)。消費者會通過一個ack(響應),告訴RabbitMQ已經收到並處理了某條消息,然後RabbitMQ就會釋放並刪除這條消息。
消息是沒有超時這個概念的;當工作者與它斷開連的時候,RabbitMQ會重新發送消息。這樣在處理一個耗時非常長的消息任務的時候就不會出問題了。
消息響應默認是開啓的。之前的例子中我們可以使用no_ack=True標識把它關閉。我們下面需要移除這個標識,並在工作者(worker)完成任務後,就發送一個響應。
代碼如下:
def callback(ch, method, properties, body):
"""
@ch: channel 通道,是由外部調用時上送
out body
讀取隊列內容做不同的操作
"""
print " [x] Recived %r" % (body, )
print " [x] ch {0}".format(ch)
print " [x] method {0}".format(method)
print " [x] properties {0}".format(properties)
time.sleep(body.count('.'))
print " [x] Done %r" % (body, )
ch.basic_ack(delivery_tag=method.delivery_tag)
使用basic_ack來發送確認消息。
注意:
如果忘記確認也是一件非常危險的事情。從技術上來講,這條消息即使被處理也不會從內存移除,這就造成可用內存會越來越小,最終造成內存溢出。從業務上來來講,同一條消息被處理兩次,涉及到業務模型可能會造成重複處理,造成嚴重後果。
對於這種問題也可以通過rabbitmqctl輸出messages_unacknowledged字段:
[tRabbitMQ@iZ250x18mnzZ src]$ sudo rabbitmqctl list_queues name messages_ready messages_unacknowledged
Listing queues ...
task_queue 0 0
hello 0 0
[tRabbitMQ@iZ250x18mnzZ src]$
持久化
如果沒有特意告訴RabbitMQ,那麼在它退出或者崩潰的時候,將會丟失所有隊列和消息。爲了確保信息不會丟失,有兩個事情是需要注意的:我們必須把“隊列”和“消息”設爲持久化。
隊列持久化,需要在聲明隊列時,通過參數durable=True來聲明隊列是持久化
channel.queue_declare(queue='hello', durable=True)
儘管這行代碼本身是正確的,但是仍然不會正確運行。因爲我們已經定義過一個叫hello的非持久化隊列。RabbitMq不允許你使用不同的參數重新定義一個隊列,它會返回一個錯誤。但我們現在使用一個快捷的解決方法——用不同的名字,例如task_queue。
channel.queue_declare(queue='task_queue', durable=True)
後面我們在發送消息和接收消息中,再對相應聲明隊列的地方再做修改
消息持久化,我們需要將delivery_mode的屬性設爲2。
channel.basic_publish(exchange='',
routing_key="task_queue",
body=message,
properties=pika.BasicProperties(
delivery_mode = 2, # make message persistent
))
注意:
將消息設爲持久化並不能完全保證不會丟失。以上代碼只是告訴了RabbitMq要把消息存到硬盤,但從RabbitMq收到消息到保存之間還是有一個很小的間隔時間。因爲RabbitMq並不是所有的消息都使用fsync(2)——它有可能只是保存到緩存中,並不一定會寫到硬盤中。並不能保證真正的持久化,但已經足夠應付我們的簡單工作隊列。如果你一定要保證持久化,你需要改寫你的代碼來支持事務(transaction)。
公平調度
默認情況下,如果沒有特殊說明,RabbitMQ只管將消息分配給woker,而rabbitMQ並不知道woker是否能夠應付得了相應的消息,也不會關心有多少worker沒有作出響應。它盲目的把第n-th條消息發給第n-th個消費者。
可以使用basic.qos方法,並設置prefetch_count=1。這樣是告訴RabbitMQ,再同一時刻,不要發送超過1條消息給一個工作者(worker),直到它已經處理了上一條消息並且作出了響應。這樣,RabbitMQ就會把消息分發給下一個空閒的工作者(worker)。
channel.basic_qos(prefetch_count=1)
注意:
關於工作隊列的大小
如果所有的worker都處理繁忙狀態,你的隊列就會被填滿。需要注意這個問題,要麼添加更多的工作者(workers),要麼使用其他策略。
總結
在第一章的基礎上,我們在這裏重點增加了以下幾個概念
工作隊列:實際應用過程中,隊列是可以並行處理的,我們可以通過增加worker的方式提升工作效率
持久化:爲了安全性,我們需要保證隊列不能丟失,那麼隊列以及消息都必須要保持持久化
隊列持久化:聲明時,指定durable=True
消息持久化:指定消息屬性,delivery_mode=2
消息確認:保持完整性,在worker處理完成後需要通知RabbitMQ已經處理,這樣整個流程才能保持完整,避免風險。
worker處理時,通過basic_ack來進行確認消息
公平調度:RabbitMQ只負責分發,而不關注worker是否處理完成,所以worker中需要指定,我最多同時可以處理多少任務,處理多大的任務,否則rabbitMQ會無限制的發送,導致崩潰。
worker處理時,通過basic_qos(prefetch_count=1)來控制
整合代碼
發送程序
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# @Date : 2016-02-28 21:28:17
# @Author : mx ([email protected])
# @Link : http://www.shujutiyu.com.cn/
# @Version : $Id$
import os
import pika
import sys
conn = None
message = ' '.join(sys.argv[1:]) or "Hello World!"
try:
# 獲取連接
conn = pika.BlockingConnection(pika.ConnectionParameters('localhost'))
# 獲取通道
channel = conn.channel()
# 在發送隊列前,需要確定隊列是否存在,如果不存在RabbitMQ將會丟棄,先創建隊列
# @param: durable 隊列持久化, 默認False
channel.queue_declare('task_queue', durable=True)
# 在RabbitMQ中發送消息,不是直接發送隊列,而是發送交換機(exchange),此處不多做研究
ret = channel.basic_publish(exchange='',
routing_key='task_queue',
body=message,
properties=pika.BasicProperties(
delivery_mode=2, # 消息持久化
) )
print " [x] Sent '{0}'".format(message)
print ret
except Exception, e:
raise e
finally:
if conn:
conn.close()
接收程序
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# @Date : 2016-02-29 16:30:21
# @Author : mx ([email protected])
# @Link : http://www.shujutiyu.com/
# @Version : $Id$
import os
import pika
import time
conn = None
def callback(ch, method, properties, body):
"""
@ch: channel 通道,是由外部調用時上送
out body
讀取隊列內容做不同的操作
"""
print " [x] Recived %r" % (body, )
print " [x] ch {0}".format(ch)
print " [x] method {0}".format(method)
print " [x] properties {0}".format(properties)
time.sleep(body.count('.'))
print " [x] Done %r" % (body, )
ch.basic_ack(delivery_tag=method.delivery_tag)
try:
# get connection
conn = pika.BlockingConnection(pika.ConnectionParameters(
'localhost')
)
# get channel
channel = conn.channel()
# declare queue, 重複聲明不會報錯,但是沒有隊列的話直接取用會報錯
# @param: durable 隊列持久化
channel.queue_declare('task_queue', durable=True)
# set channel 在同一時刻處理一條消息
channel.basic_qos(prefetch_count=1)
# get message
# @no_ack,不需要返回確認消息, 默認是False,設置的話,有可能會丟包,不設置的話,
# 當不返回確認消息時,有可能重複處理或者是內存溢出。
channel.basic_consume(callback, queue='task_queue')
print ' [*] Waiting for messages. To exit press CTRL+C'
channel.start_consuming()
except Exception, e:
raise e
finally:
if conn:
conn.close()
本章就介紹到這裏,其實還有好多可用的東西,可以到下面提供的參考資料查看,也可以查看pika的源碼,自取所需。