官方稳定链接:
一、https://www.rabbitmq.com/tutorials/tutorial-two-python.html
工作队列:
每个任务只分发给一个工作者(worker)
1、new_task.py代码:
import pika
import sys
connection=pika.BlockingConnection(pika.ConnectionParameters('localhost',5672));
channel=connection.channel();
channel.queue_declare(queue='task_queue');
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();
worker.py代码:
import pika
import time
connection=pika.BlockingConnection(pika.ConnectionParameters('localhost'));
channel=connection.channel();
channel.queue_declare(queue='task_queue');
def callback(ch,method,properties,body):
print(ch,method,properties);
print("[x] Received %r "%body);
time.sleep(body.count(b'.'));
print("[x] done");
channel.basic_consume(callback,queue='task_queue');
print("[x] Waiting for message.To exit press CTRL+C");
channel.start_consuming();
2、运行结果:
疑问:为什么new_task生产者发送消息。并且有个消费者worker也消费了一个,但在队列中消息不标记为删除呐?依然显示有一个消息。
二、
当在C1和C2同时运行时,假如两者现在都处于在等待新消息的状态(已经接收过几条消息了。),突然终止某个消费者,发现,另一个消费者会重新接收终止消费者之前所接收的所有已处理的消息(还处理么?反正很快就显示done了。。目测,再处理一遍)。
当某C消费者没执行完某条消息就被终止时,另一个消费者会从头接收被终止的消费者之前接收的所有消息,直到未执行完的消息执行完了。
注意,此时,用rabbitmqctl list_queues发现消息队列里的消息就是曾经发过的消息条数。(为什么没减呐?)
三、
实验总结:
ch.basic_ack(delivery_tag = method.delivery_tag)
作用:
将上面worker.py中callback函数最后一行添加此语句。
实验结果:
看见某条消息是否执行完毕,看见done才可。所以上面没有处理完接收的消息。
注意:此时,在终止C1消费者程序时,C2消费者只接收C1没处理完的消息。以上处理完的消息将不再接收和处理。并且此时用命令rabbitmqctl list_queues查看消息队列中的消息条数时,显示为0条,即代表都已经处理完?
总结:以上一、二、三、在Rabbitmq重启后,消息队列均会消失。不具有持久性。
四、
RabbitMQ崩溃后不影响我们的消息队列,和里面的消息应该怎么做?(应该消息持久化)
代码如下:
new_task.py
import pika
import sys
connection=pika.BlockingConnection(pika.ConnectionParameters('localhost',5672));
channel=connection.channel();
channel.queue_declare(queue='task_queue2',durable=True);#never lose our queue
message=' '.join(sys.argv[1:]) or "Hello World!";
channel.basic_publish(exchange='',routing_key='task_queue2',body=message,properties=pika.BasicProperties(delivery_mode=2,))#mark our messages as persistent
print("[x] sent %r "%message);
connection.close();
worker.py:
import pika
import time
connection=pika.BlockingConnection(pika.ConnectionParameters('localhost'));
channel=connection.channel();
channel.queue_declare(queue='task_queue2',durable=True);
def callback(ch,method,properties,body):
print(ch,method,properties);
print("[x] Received %r "%body);
time.sleep(body.count(b'.'));
print("[x] done");
ch.basic_ack(delivery_tag=method.delivery_tag);
channel.basic_consume(callback,queue='task_queue2');
print("[x] Waiting for message.To exit press CTRL+C");
channel.start_consuming();
测试:
在服务器重启后,队列task_queue2具有持久性。而此时队列task_queue已经消失了。
注意:Marking messages as persistent doesn't fully guarantee that a message won't be lost. If you need a stronger guarantee then you can use publisher confirms.
五、
new_task1.py:
import pika
import sys
connection=pika.BlockingConnection(pika.ConnectionParameters(host='localhost'));
channel=connection.channel();
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();
work1.py:
import pika
import time
connection=pika.BlockingConnection(pika.ConnectionParameters(host='localhost'));
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(body.count(b'.'));
print('[x] done');
ch.basic_ack(delivery_tag=method.delivery_tag);
channel.basic_qos(prfetch_count=1);#控制每个消费者一次接收的消息条数,只有返回ack,才会接收发送者的另一条消息
channel.basic_consume(callback,queue='task_queue');
channel.start_consuming();
六、知识点的补充:
1、循环调度:
new_task.py:
#!/usr/bin/env python
import pika
import sys
connection=pika.BlockingConnection(pika.ConnectionParameters(host='localhost'))
channel=connection.channel()
message=''.join(sys.argv[1:])or 'Hello World!'
channel.basic_publish(exchange='',routing_key='hello',body=message)
print "[x] Send %r"%(message,)
worker.py:
#!/usr/bin/env python
import pika
import time
connection=pika.BlockingConnection(pika.ConnectionParameters(host='localhost'))
channel=connection.channel()
channel.queue_declare(queue='hello')
print'[*] Waiting for message.To exit press CTRL+C'
def callback(ch,method,propertied,body):
print"[*] Received %r"%(body,)
time.sleep(body.count('.'))
print "[x] Done"
channel.basic_consume(callback,queue='hello',no_ack=True)
channel.start_consuming()
先运行两个worker的终端,再运行new_task来进行发送工作任务。
默认来说,RabbitMQ 会按顺序得把消息发送给每个消费者(consumer)。平均每个消费者都会收到同等数量得消息。这种发送消息得方式叫做——轮询(round-robin)。我们可以尝试着添加到三个或更多得工作者(workers)。
2、消息确认:
new_task.py:
#!/usr/bin/env python
import pika
import sys
connection=pika.BlockingConnection(pika.ConnectionParameters(host='localhost'))
channel=connection.channel()
message=' '.join(sys.argv[1:])or 'hello world!'
channel.basic_publish(exchange='',routing_key='hello',body=message)
print "[x] Sent %r"%(message,)
worker.py:
#!/usr/bin/env python
import pika
import time
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,)
time.sleep(body.count('.'))
print "[x] Done"
ch.basic_ack(delivery_tag=method.delivery_tag)
channel.basic_consume(callback,queue="hello")
channel.start_consuming()
消息确认:
当处理一个比较耗时得任务的时候,你也许想知道消费者(consumers)是否运行到一半就挂掉。当前的代码中,当消息被 RabbitMQ 发送给消费者(consumers)之后,马上就会在内存中移除。这种情况,你只要把一个工作者(worker)停止,正在处理的消息就会丢失。同时,所有发送到这个工作者的还没有处理的消息都会丢失。我们不想丢失任何任务消息。如果一个工作者(worker)挂掉了,我们希望任务会重新发送给其他的工作者(worker)。
为了防止消息丢失,RabbitMQ 提供了消息响应(acknowledgments)。消费者会通过一个 ack(响应),告诉 RabbitMQ 已经收到并处理了某条消息,然后 RabbitMQ 就会释放并删除这条消息。
如果消费者(consumer)挂掉了,没有发送响应,RabbitMQ 就会认为消息没有被完全处理,然后重新发送给其他消费者(consumer)。这样,及时工作者(workers)偶尔的挂掉,也不会丢失消息。
消息是没有超时这个概念的;当工作者与它断开连的时候,RabbitMQ 会重新发送消息。这样在处理一个耗时非常长的消息任务的时候就不会出问题了。
消息响应默认是开启的。之前的例子中我们可以使用 no_ack=True 标识把它关闭。是时候移除这个标识了,当工作者(worker)完成了任务,就发送一个响应。
3、确认 RabbitMQ 是否及时释放
通过 basic_ack() 告诉 RabbitMQ 已经收到并处理了某条消息,然后 RabbitMQ 就会释放并删除这条消息。一个很容易犯的错误就是忘了使用 basic_ack() 响应服务端,后果很严重。消息在你的程序退出之后就会重新发送,如果它不能够释放没响应的消息,成为死信,RabbitMQ 就会占用越来越多的内存。
为了排除这种错误,你可以使用 rabbitmqctl 命令,输出 messages_unacknowledged 字段:
当worker.py 忘记写ch.basic_ack(delivery_tag=method.delivery_tag)时:
#!/usr/bin/env python
import pika
import time
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,)
time.sleep(body.count('.'))
print "[x] Done"
#ch.basic_ack(delivery_tag=method.delivery_tag)
channel.basic_consume(callback,queue="hello")
channel.start_consuming()
发现:new_task.py 发送多条消息时,打开worker.py 此时会把消息队列中的消费掉,但是,ctrl+C 后,又new_task.py一两条消息时,再打开worker.py 还会从queue中拿出原先的消息依次执行一遍。
4、消息持久化
如果你没有特意告诉 RabbitMQ,那么在它退出或者崩溃的时候,将会丢失所有队列和消息。为了确保信息不会丢失,有两个事情是需要注意的:我们必须把 “队列” 和 “消息” 设为持久化。
首先,为了不让队列消失,需要把队列声明为持久化(durable),并且这个 queue_declare 必须在生产者(producer)和消费者(consumer)对应的代码中修改。这时候,我们就可以确保在 RabbitMq 重启之后 queue_declare 队列不会丢失。另外,我们需要把我们的消息也要设为持久化——将 delivery_mode 的属性设为2:
比如:new_task.py:
#!/usr/bin/env python
import pika
import sys
connection=pika.BlockingConnection(pika.ConnectionParameters(host='localhost'))
channel=connection.channel()
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,)#make message persistent
print "[x] Sent %r"%(message,)
connection.close()
4.1 注意:消息持久化
将消息设为持久化并不能完全保证不会丢失。以上代码只是告诉了 RabbitMq 要把消息存到硬盘,但从 RabbitMq 收到消息到保存之间还是有一个很小的间隔时间。因为 RabbitMq 并不是所有的消息都使用 fsync(2) ——它有可能只是保存到缓存中,并不一定会写到硬盘中。并不能保证真正的持久化,但已经足够应付我们的简单工作队列。如果你一定要保证持久化,你需要改写你的代码来支持事务(transaction)。
5、公平调度
你应该已经发现,它仍旧没有按照我们期望的那样进行分发。比如有两个工作者(workers),处理奇数消息的比较繁忙,处理偶数消息的比较轻松。然而 RabbitMQ 并不知道这些,它仍然一如既往的派发消息。
这时因为 RabbitMQ 只管分发进入队列的消息,不会关心有多少消费者(consumer)没有作出响应。它盲目的把 n-th 条消息发给第 n-th 个消费者。
我们可以使用 basic.qos 方法,并设置 prefetch_count=1。这样是告 RabbitMQ ,再同一时刻,不要发送超过1条消息给一个工作者(worker),直到它已经处理了上一条消息并且作出了响应。这样,RabbitMQ 就会把消息分发给下一个空闲的工作者(worker):
worker.py:
#!/usr/bin/env python
import pika
import time
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,)
time.sleep(body.count('.'))
print "[x] Done"
ch.basic_ack(delivery_tag=method.delivery_tag)
channel.basic_qos(prefetch_count=1)#告 RabbitMQ ,再同一时刻,不要发送超过1条消息给一个工作者(worker),直到它已经处理了上一条消息并且作出了响应。
channel.basic_consume(callback,queue="hello")
channel.start_consuming()
5.1 关于队列大小
如果所有的工作者都处理繁忙状态,你的队列就会被填满。你需要留意这个问题,要么添加更多的工作者(workers),要么使用其他策略。
6、实验总结
使用消息响应和 prefetch_count 你就可以搭建起一个工作队列了。这些持久化的选项使得在 RabbitMQ 重启之后仍然能够恢复。
现在我们可以移步下一节学习如何发送相同的消息给多个消费者(consumers)。