RabbitMQ介绍

RabbitMQ

转自: anzhsoft: http://blog.csdn.net/anzhsoft/article/details/19630147

RabbitMQ是一个在AMQP基础上完整的,可复用的企业消息系统。他遵循Mozilla Public License开源协议。

MQ全称为Message Queue, 消息队列(MQ)是一种应用程序对应用程序的通信方法。应用程序通过读写出入队列的消息(针对应用程序的数据)来通信,而无需专用连接来链接它们。消息传递指的是程序之间通过在消息中发送数据进行通信,而不是通过直接调用彼此来通信,直接调用通常是用于诸如远程过程调用的技术。排队指的是应用程序通过 队列来通信。队列的使用除去了接收和发送应用程序同时执行的要求。

RabbitMQ安装
安装RabbitMQ(会自动安装erlang)
   $ yum -y install rabbitmq-server

注意:service rabbitmq-server start/stop

安装API

pip install pika
or
easy_install pika
or

源码

https://pypi.python.org/pypi/pika

使用API操作RabbitMQ

基于Queue实现生产者消费者模型

#!/usr/bin/env python
# -*- coding:utf-8 -*-
import Queue
import threading


message = Queue.Queue(10)


def producer(i):
    while True:
        message.put(i)


def consumer(i):
    while True:
        msg = message.get()


for i in range(12):
    t = threading.Thread(target=producer, args=(i,))
    t.start()

for i in range(10):
    t = threading.Thread(target=consumer, args=(i,))
    t.start()

对于RabbitMQ来说,生产和消费不再针对内存里的一个Queue对象,而是某台服务器上的RabbitMQ Server实现的消息队列。

#!/usr/bin/env python

import pika

# ######################### 生产者 #########################

connection = pika.BlockingConnection(pika.ConnectionParameters(
        host='localhost'))  # 创建连接,传入的参数就是RabbitMQ Server的ip或者name
channel = connection.channel()  # 创建channel

channel.queue_declare(queue='hello')  # 创建名字为hello的queue

# 现在我们已经准备好了发送了。
# 从架构图可以看出,Producer只能发送到exchange,它是不能直接发送到queue的。现在我们使用默认的exchange(名字是空字符)。这个默认的exchange允许我们发送给指定的queue。routing_key就是指定的queue名字。

channel.basic_publish(exchange='',
                      routing_key='hello',
                      body='Hello World!')
print(" [x] Sent 'Hello World!'")
connection.close()  # 退出前要关闭connection
#!/usr/bin/env python

import pika

# ########################## 消费者 ##########################

connection = pika.BlockingConnection(pika.ConnectionParameters(
        host='localhost'))
channel = connection.channel()

channel.queue_declare(queue='hello')

# 接下来要subscribe了。在这之前,需要声明一个回调函数来处理接收到的数据。

def callback(ch, method, properties, body):
    print(" [x] Received %r" % body.decode(encoding='utf8'))

channel.basic_consume(callback,
                      queue='hello',
                      no_ack=True)
# 采用no-ack的方式进行确认,也就是说,每次Consumer接到数据后,而不管是否处理完成,RabbitMQ Server会立即把这个Message标记为完成,然后从queue中删除
# 默认情况下,消息确认是打开的(enabled),

# 最后,准备好无限循环监听吧:

print(' [*] Waiting for messages. To exit press CTRL+C')
channel.start_consuming()
Message acknowledgment 消息确认

如果忘记了ack,那么后果很严重。当Consumer退出时,Message会重新分发。然后RabbitMQ会占用越来越多的内存,由于RabbitMQ会长时间运行,因此这个“内存泄漏”是致命的。去调试这种错误,可以通过一下命令打印un-acked Messages:

rabbitmqctl list_queues name messages_ready messages_unacknowledged
Message durability 消息持久化

如果Consumer异常退出,Message也不会丢失。但是如果RabbitMQ Server退出呢?比如软件都有bug,被其它软件影响,或者系统重启了,系统panic了。。。

为了保证在RabbitMQ退出或者crash了数据仍没有丢失,需要将queue和Message都要持久化。

  • queue的持久化

需要在声明时指定durable=True:

channel.queue_declare(queue='hello', durable=True)

  • Message的持久化

需要在Publish的时候指定一个properties,方式如下:

channel.basic_publish(exchange='',  
                      routing_key="task_queue",  
                      body=message,  
                      properties=pika.BasicProperties(  
                         delivery_mode = 2, # make message persistent  
                      ))
关于持久化的进一步讨论:

为了数据不丢失,我们采用了:

  • 在数据处理结束后发送ack,这样RabbitMQ Server会认为Message Deliver 成功。
  • 持久化queue,可以防止RabbitMQ Server 重启或者crash引起的数据丢失。
  • 持久化Message,理由同上。

但是这样能保证数据100%不丢失吗?

答案是否定的。问题就在与RabbitMQ需要时间去把这些信息存到磁盘上,这个time window虽然短,但是它的确还是有。在这个时间窗口内如果数据没有保存,数据还会丢失。还有另一个原因就是RabbitMQ并不是为每个Message都做fsync:它可能仅仅是把它保存到Cache里,还没来得及保存到物理磁盘上。

因此这个持久化还是有问题。但是对于大多数应用来说,这已经足够了。当然为了保持一致性,你可以把每次的publish放到一个transaction中。这个transaction的实现需要user defined codes。

那么商业系统会做什么呢?一种可能的方案是在系统panic时或者异常重启时或者断电时,应该给各个应用留出时间去flash cache,保证每个应用都能exit gracefully。

Fair dispatch 公平分发

默认状态下,RabbitMQ将第n个Message分发给第n个Consumer。当然n是取余后的。它不管Consumer是否还有unacked Message,只是按照这个默认机制进行分发。

那么如果有个Consumer工作比较重,那么就会导致有的Consumer基本没事可做,有的Consumer却是毫无休息的机会。那么,RabbitMQ是如何处理这种问题呢?

通过 basic.qos 方法设置prefetch_count=1 。这样RabbitMQ就会使得每个Consumer在同一个时间点最多处理一个Message。换句话说,在接收到该Consumer的ack前,他它不会将新的Message分发给它。 设置方法如下:

 channel.basic_qos(prefetch_count=1)

注意,这种方法可能会导致queue满。当然,这种情况下你可能需要添加更多的Consumer,或者创建更多的virtualHost来细化你的设计。

rabbit_MQ_3_send.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()

示例

rabbit_MQ_3_recv.py

#!/usr/bin/env python

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.decode(encoding='utf8'),))
    time.sleep(body.decode(encoding='utf8').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()
exchanges

RabbitMQ 的Messaging Model就是Producer并不会直接发送Message到queue。实际上,Producer并不知道它发送的Message是否已经到达queue。

Producer发送的Message实际上是发到了Exchange中。它的功能也很简单:从Producer接收Message,然后投递到queue中。Exchange需要知道如何处理Message,是把它放到那个queue中,还是放到多个queue中?这个rule是通过Exchange 的类型定义的。

exchanges的类型
  • direct
  • topic
  • fanout

fanout就是广播模式,会将所有的Message都放到它所知道的queue中。创建一个名字为logs,类型为fanout的Exchange:

channel.exchange_declare(exchange='logs',  
                         type='fanout')

通过rabbitmqctl可以列出当前所有的Exchange:

###### rabbitmqctl list_exchanges

Listing exchanges ...
        direct
amq.direct      direct
amq.fanout      fanout
amq.headers     headers
amq.match       headers
amq.rabbitmq.log        topic
amq.rabbitmq.trace      topic
amq.topic       topic
Temporary queues

1、如果在声明queue时不指定名字,那么RabbitMQ会随机为我们选择这个名字。方法:

result = channel.queue_declare()

通过result.method.queue 可以取得queue的名字。基本上都是这个样子:amq.gen-JzTY20BRgKO-HjmUJj0wLg。

2、当Consumer关闭连接时,这个queue要被deleted。可以加个exclusive的参数。方法:

result = channel.queue_declare(exclusive=True)  
Bindings绑定

现在我们已经创建了fanout类型的exchange和没有名字的queue(实际上是RabbitMQ帮我们取了名字)。那exchange怎么样知道它的Message发送到哪个queue呢?答案就是通过bindings:绑定。

方法:

channel.queue_bind(exchange='logs',  
                   queue=result.method.queue)

现在logs的exchange就将它的Message附加到我们创建的queue了。

Listing bindings

使用命令rabbitmqctl list_bindings

示例:

rabbitMQ_fanout_send

#!/usr/bin/env python

import pika
import sys

connection = pika.BlockingConnection(pika.ConnectionParameters(
        host='localhost'))
channel = connection.channel()

channel.exchange_declare(exchange='logs',
                         type='fanout')
# 这里声明了exchange。publish到一个不存在的exchange是被禁止的。如果没有queue bindings exchange的话,log是被丢弃的。

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()

rabbitMQ_fanout_recv.py

#!/usr/bin/env python

import pika
connection = pika.BlockingConnection(pika.ConnectionParameters(
        host='localhost'))
channel = connection.channel()

channel.exchange_declare(exchange='logs',
                         type='fanout')

result = channel.queue_declare(exclusive=True) #不指定queue名字,rabbit会随机分配一个名字,exclusive=True会在使用此queue的消费者断开后,自动将queue删除
queue_name = result.method.queue

channel.queue_bind(exchange='logs', #把你的queue绑定 到exchange
                   queue=queue_name)

print(' [*] Waiting for logs. To exit press CTRL+C')


def callback(ch, method, properties, body):
    print(" [x] %r" % body)

channel.basic_consume(callback,
                      queue=queue_name,
                      no_ack=True)

channel.start_consuming()

使用命令rabbitmqctl list_bindings你可以看我们创建的queue
结果:

Listing bindings ...
        exchange        amq.gen-ahKWE8rmSWgSmqVBpq56kg  queue   amq.gen-ahKWE8rmSWgSmqVBpq56kg  []
        exchange        amq.gen-iHQe4o8r29hnPoFILUFIyA  queue   amq.gen-iHQe4o8r29hnPoFILUFIyA  []
        exchange        hello   queue   hello   []
logs    exchange        amq.gen-ahKWE8rmSWgSmqVBpq56kg  queue   amq.gen-ahKWE8rmSWgSmqVBpq56kg  []
logs    exchange        amq.gen-iHQe4o8r29hnPoFILUFIyA  queue   amq.gen-iHQe4o8r29hnPoFILUFIyA  []
Direct exchange

使用一个key来创建binding:

channel.queue_bind(exchange=exchange_name,  
                   queue=queue_name,  
                   routing_key='black')  

对于fanout的exchange来说,这个参数是被忽略的。
Direct exchange的路由算法非常简单:通过routing_key的完全匹配,如图:

exchange X和两个queue绑定在一起,Q1的routing_key是orange,Q2的routing_key是black。
当routing_key是orange时,exchange会把它放到Q1。如果是black,那么就会到Q2。其余的Message都会被丢弃。

多个queue绑定同一个key是可以的
比如,Q1和Q2的routing_key是orange,对于routing key是orange的Message,会被deliver到Q1和Q2。其余的Message都会被丢弃,如图:

Emitting logs

首先是我们要创建一个direct的exchange:

channel.exchange_declare(exchange='direct_logs',
                         type='direct')

我们将使用log的severity作为routing key,这样Consumer可以针对不同severity的log进行不同的处理。
publish:

channel.basic_publish(exchange='direct_logs',
                      routing_key=severity,
                      body=message)

我们使用三种severity:'info', 'warning', 'error'.

Subscribing

对于queue,我们需要绑定severity:

result = channel.queue_declare(exclusive=True)
queue_name = result.method.queue

for severity in severities:
    channel.queue_bind(exchange='direct_logs',
                       queue=queue_name,
                       routing_key=severity)

完整示例如下:,如图:

rabbitMQ_direct_recv.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()

rabbitMQ_direct_send.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()

我们想把warning和error的log记录到一个文件中:

$ python receive_logs_direct.py warning error > logs_from_rabbit.log

打印所有log到屏幕:

$ python receive_logs_direct.py info warning error
 [*] Waiting for logs. To exit press CTRL+C

未完。。。

posted @ 2016-04-07 22:29  鬼凤  阅读(116)  评论(0)    收藏  举报