学习笔记(RabbitMQ)

来源:互联网 发布:js tr ctrl 左键 监听 编辑:程序博客网 时间:2024/05/20 23:03

参考:http://blog.csdn.net/column/details/rabbitmq.html


RabbitMQ是一个用于消息传递的中间件,可以保证数据按照指定的方式进行传输,但是这个保证不是100%的。

对于商业系统来说,可以再做一层数据一致性的guard,就可以彻底保证系统的一致性了。


一条消息(Message)由有效载荷(payload)和标签(label)两部分组成。

payload就是传输的数据,label描述了payload,RabbitMQ通过label来决定把消息发给哪个消费者。


生产者把消息放到消息队列中,RabbitMQ把消息中的label去掉后再发送给订阅者,订阅者是无法获取到label中的内容的,包括是信息是谁发给他的,除非payload中包含了这部分信息。




一些定义:

Exchanges :生产者发布信息的地方,消息发布后首先进入到Exchanges

Queues:消息结束和消费者接收消息的地方

Bindings:消息如何从exchanges路由到特定队列

Connection: 就是一个TCP的连接。Producer和Consumer都是通过TCP连接到RabbitMQ Server的。以后我们可以看到,程序的起始处就是建立这个TCP连接。

Channels: 虚拟连接。它建立在上述的TCP连接中。数据流动都是在Channel中进行的。也就是说,一般情况是程序起始建立TCP连接,第二步就是建立这个Channel。


那么,为什么使用Channel,而不是直接使用TCP连接?


    对于OS来说,建立和关闭TCP连接是有代价的,频繁的建立关闭TCP连接对于系统的性能有很大的影响,而且TCP的连接数也有限制,这也限制了系统处理高并发的能力。但是,在TCP连接中建立Channel是没有上述代价的。对于Producer或者Consumer来说,可以并发的使用多个Channel进行Publish或者Receive


通过ACK确认消息的到达


拒收消息:

有两种方式,第一种的Reject可以让RabbitMQ Server将该Message 发送到下一个Consumer。第二种是从queue中立即删除该Message。


生产者和消费者都可以创建消息队列,但只有第一次创建的时候是有效的,后面再创建同一个消息队列仍然是可以的,但是没有任何作用,前面已经创建的队列不会受到任何影响,即属性不会有任何变化,即使前后两次创建时属性不一样,仍保留首次创建时的属性。


消息从Exchanges路由到queue中有三种路由算法:

Direct exchange:根据routing key(默认是queue的名字)来绑定exchanges

Fanout exchange:向响应的queue广播

Topic exchanges:对key进行模式匹配,比如ab*可以传递到所有ab*的queue


Virtual hosts:每个virtual host本质上都是一个RabbitMQ Server,拥有它自己的queue,exchagne,和bings rule等等。这保证了你可以在多个不同的application中使用RabbitMQ。


介绍完了一大堆概念,下面我们开始正式进入RabbitMQ!


一. Hello World

写两个python程序,一个是生产者,一个是消费者,生产者把‘Hello World!’发送到消息队列中,消费者从消息队列中获取到消息并把消息内容打印出来。

首先直接上代码吧,在逐行分析:

# producer.pyimport pikaconnection = pika.BlockingConnection(pika.ConnectionParameters('localhost'))channel = connection.channel()channel.queue_declare(queue='hello')channel.basic_publish(exchange='', routing_key='hello', body='Hello World!')print "[x] sent 'Hello World!"connection.close()

上面是生产者的代码,也就是消息发布者。首先import了一个叫pika的AMQP库,没有安装该库的可以通过以下命令进行安装:

sudo pip install pika==0.9.8
没有安装pip工具的需要先安装pip,具体操作参考:http://blog.csdn.net/anzhsoft/article/details/19570187

第4行代码:建立连接

第5行代码:建立通道

第6行代码:声明了一个名字为hello的队列

第7行:发布消息。生产者的消息只能发布到Exchanges中去,这里使用了空字符串''表示发送到默认的Exchanges,默认的Exchanges允许你通过routing_key把消息发送到指定的队列中去,body部分为消息的内容。

最后一行代码记得在程序结束的时候把连接关掉。


再来看一下消费者的代码:

# consumerimport pikaconnection = pika.BlockingConnection(pika.ConnectionParameters(host='localhost'))channel = connection.channel()channel.queue_declare(queue='hello')print '[*] Waitting for message. To exit press Ctrl + C'def callback(ch, method, properties, body):    print "[x] Receive %r" % (body,)channel.basic_consume(callback, queue='hello', no_ack=True)channel.start_consuming()

前面几行代码同样是:导入AMQP库,建立连接,建立通道,声明队列,可以说这些步骤基本上是固定的,无论是生产者还是消费者,都需要声明队列,防止队列不存在,造成消息丢失。


第9-10行代码定义了一个callback函数,这个函数用于处理接收到的消息,这里只是简单地把消息打印出来。

关于callback()函数的几个参数解释:

ch:进行通信的channel

method:目前只知道通过method.queue可以获得queue的名字

properties:发布message的时候需要填写properties参数,也可以使用默认值不填写。这里的properties就是发布的消息的属性。

body:消息的内容,格式为一个str

第12行代码指定程序从队列hello中接收信息,并用callback函数进行处理,no_ack参数指定了是否需要ack确认



二.任务分发机制


RabbitMQ默认的分发机制为循环分发(Round-robin),RabbitMQ的分发机制非常适合扩展,而且它是专门为并发程序设计的。如果现在load加重,那么只需要创建更多的Consumer来进行任务处理即可。

稍微修改一下‘Hello World‘中的代码,通过sleep方法来模拟耗时操作并验证一下RabbitMQ的分发机制

# producer.pyimport sysimport pikaconnection = pika.BlockingConnection(pika.ConnectionParameters('localhost'))channel = connection.channel()channel.queue_declare(queue='hello')message = ' '.join(sys.argv[1:]) or 'Hello World'channel.basic_publish(exchange='', routing_key='hello', body=message)print "[x] sent " + messageconnection.close()

# consumer.pyimport timeimport pikaconnection = pika.BlockingConnection(pika.ConnectionParameters(host='localhost'))channel = connection.channel()channel.queue_declare(queue='hello')print '[*] Waitting for message. To exit press Ctrl + C'def callback(ch, method, properties, body):    print "[x] Receive %r" % (body,)    time.sleep(body.count('.'))    print "[x] Done"channel.basic_consume(callback, queue='hello', no_ack=True)channel.start_consuming()

上面的代码根据message中 (.) 的数量来模拟耗时操作的长短,在控制台1开启producer.py,在控制台2和3开启consumer.py

运行的结果为:

# producer1[root@localhost zoulr]# python producer.py first message.[x] sent first message.[root@localhost zoulr]# python producer.py second message..[x] sent second message..[root@localhost zoulr]# python producer.py third message...[x] sent third message...[root@localhost zoulr]# python producer.py forth message....[x] sent forth message...[root@localhost zoulr]# python producer.py fifth message.....[x] sent fifth message.....

# consumer1[root@localhost zoulr]# python consumer.py [*] Waitting for message. To exit press Ctrl + C[x] Receive 'first message.'[x] Done[x] Receive 'third message...'[x] Done[x] Receive 'fifth message.....'[x] Done

# consumer2[root@localhost zoulr]# python consumer.py [*] Waitting for message. To exit press Ctrl + C[x] Receive 'second message..'[x] Done[x] Receive 'forth message....'[x] Done

Consumer1接收到了1,3,5三条信息,consumer2接收到了2,4两条信息。


默认情况下,RabbitMQ 会顺序的分发每个Message。当每个收到ack后,会将该Message删除,然后将下一个Message分发到下一个Consumer。这种分发方式叫做round-robin。


每个Consumer可能需要一段时间才能处理完收到的数据。如果在这个过程中,Consumer出错了,异常退出了,而数据还没有处理完成,那么非常不幸,这段数据就丢失了。

因为我们采用no-ack的方式进行确认,也就是说,每次Consumer接到数据后,而不管是否处理完成,RabbitMQ Server会立即把这个Message标记为完成,然后从queue中删

除了。


为了保证数据不被丢失,RabbitMQ支持消息确认机制,即acknowledgments。为了保证数据能被正确处理而不仅仅是被Consumer收到,那么我们不能采用no-ack。而应该是在处理完数据后发送ack。


在处理数据后发送的ack,就是告诉RabbitMQ数据已经被接收,处理完成,RabbitMQ可以去安全的删除它了。

如果Consumer退出了但是没有发送ack,那么RabbitMQ就会把这个Message发送到下一个Consumer。这样就保证了在Consumer异常退出的情况下数据也不会丢失。



    这里并没有用到超时机制。RabbitMQ仅仅通过Consumer的连接中断来确认该Message并没有被正确处理。也就是说,RabbitMQ给了Consumer足够长的时间来做数据处理。


    默认情况下,消息确认是打开的(enabled)。但在上文的代码中,我们通过no_ack = True把ack关掉了。


打开ack确认后,我们要在callback函数中处理完成后发送ack:

ch.basic_ack(delivery_tag = method.delivery_tag)

这样即使你通过Ctr-C中断了consumer.py,那么Message也不会丢失了,它会被分发到下一个Consumer。


      如果忘记了ack,那么后果很严重。当Consumer退出时,Message会重新分发。然后RabbitMQ会占用越来越多的内存,由于RabbitMQ会长时间运行,因此这个“内存泄漏”是致命的。


要调试这种错误,可以通过以下命令进行调试:

sudo rabbitmqctl list_queues name messages_ready messages_unacknowledged

Message durability消息持久化:

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

queue的持久化需要在声明时指定durable=True:

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

持久化Message,即在Publish的时候指定一个properties,方式如下:

channel.basic_publish(exchange='',                      routing_key="hello",                      body=message,                      properties=pika.BasicProperties(                         delivery_mode = 2, # make message persistent                      ))

为了防止数据丢失,我们采取的策略有ack(防止消费者崩溃)和持久化(防止RabbitMQ崩溃)。

但是RabbitMQ把数据写到磁盘也是需要时间的,因此仍然不能保证数据100%不会丢失。


公平分发:

默认状态下,RabbitMQ将第n个Message分发给第n(取余后)个Consumer。但是有些任务比较重,有些任务比较轻,它不管Consumer是否还有unacked Message,只是按

照这个默认机制进行分发。这样就会造成有些consumer比较忙,有些则比较空闲。


为了解决这个问题,我们可以让每个节点同时处理的未确认消息为1条。通过 basic.qos 方法设置prefetch_count=1 。这样RabbitMQ就会使得每个Consumer在同一个时间点最

多处理一个Message。换句话说,在接收到该Consumer的ack前,他它不会将新的Message分发给它。 设置方法如下:

在consumer.py的channel.queue_declare()后添加:

channel.basic_qos(prefetch_count=1)

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


三. 分发到多个Consumer(fanout)


将一个message分发到多个Consumer中,这种模式叫做‘publish/subscribe’模式。

下面将以构造一个日志系统为例,producer.py产生日志,consumer1打印日志,consumer2保存日志

前面有提过message从Exchanges路由到queue有三种方法,其中有一种fanout的方法是通过广播的方式来传递消息的。

在producer.py中创建一个fanout的Exchanges:

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

通过sudo rabbitmqctl list_exchanges可以查看RabbitMQ上所有的Exchanges


发布message的时候通过Exchanges进行发布,而不是routing_key:

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

对于我们将要构建的日志系统,并不需要有名字的queue。我们希望得到所有的log,而不是它们中间的一部分。而且我们只对当前的log感兴趣,因此我们仅需在创建新的consumer的时候创建一个queue,并把queue绑定(binding)到Exchanges上面去,然后就可以接收log信息了。

新建queue时如果不指定名字,系统会给queue随机分配一个名字,可以通过queue_declare()方法返回的result.method.queue获取到queue的名字。

当Consumer关闭时,由其所创建的queue也没用了,在创建queue的时候指定参数exclusive为True可以在consumer关闭时候自动关闭该queue。

queue绑定到Exchanges上可以通过以下方法:

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

第一个参数为要绑定的exchange的名字。

第二个参数中的result是创建queue时返回的结果,result.method.queue可以获取到queue的名字。


使用命令:

rabbitmqctl list_bindings

可以查看绑定关系。


完整代码:

# producer.pyimport sysimport pikaconnection = pika.BlockingConnection(pika.ConnectionParameters('localhost'))channel = connection.channel()channel.exchange_declare(exchange='logs', type='fanout')message = ' '.join(sys.argv[1:]) or 'Hello World'channel.basic_publish(exchange='logs', routing_key='', body=message)print "[x] sent " + messageconnection.close()

# consumer.pyimport timeimport pikaconnection = pika.BlockingConnection(pika.ConnectionParameters(host='localhost'))channel = connection.channel()result = channel.queue_declare(exclusive=True)channel.exchange_declare(exchange='logs', type='fanout')channel.queue_bind(exchange='logs', queue=result.method.queue)def callback(ch, method, properties, body):    print "[x] Receive %r" % (body,)channel.basic_consume(callback, queue=result.method.queue, no_ack=True)channel.start_consuming()

这里面只有一个打印消息的consumer,运行两个consumer可以看到两个consumer都收到了消息并打印出来。

如果要实现记录logs的功能,可以再写一个consumer2.py,重写callback的处理逻辑,也可以用重定向来实现。这两种方法一种属于python IO的范畴,一种属于linux的范畴,笔者暂时均未涉猎。

linux重定向方法可参考:http://blog.csdn.net/anzhsoft/article/details/19617305 文章的最后

python consumer.py > logs_from_rabbit.log
把打印在屏幕上的信息保存在logs_from_rabbit.log文件中。


四. routing_key(direct)


在前面的代码中有说过,如果使用默认的exchange(‘’空字符串),那么message会被发到参数routing_key指定的queue中,routing_key即为queue的名字。

但是这里的routing_key跟上面的不一样。

在路由算法 derect中,exchange和queue绑定的时候需要指定routing_key,消息发布的时候也要指定routing_key,exchange根据routing_key来对message进行路由。

绑定时指定routing_key的方法如下:

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

创建一个使用direct算法进行路由的exchange:

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

发布一条routing_key为‘black’的message:

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



五. 使用主题进行分发(Topic)


接下来介绍最后一种路由算法:Topic

Topic是根据routing_key的正则表达式进行路由的,因此对routing_key的格式是有限制的,用(.)来分割key,* 号表示1个单词, # 表示任意个单词,包括是0个。

举个例子:

*.orange.*lazy.#

声明exchange的时候把参数type=‘topic’即可。



六. 适用于云计算集群的远程过程调用RPC

总体来说,在RabbitMQ进行RPC远程调用是比较容易的。client发送请求的Message然后server返回响应结果。

为了收到响应client在publish message时需要提供一个”callback“(回调)的queue地址。


根据惯例,还是先上代码然后逐行解析。

以下分别实现了一个server和一个client,client发送message请求计算斐波那契数列的值,server收到后进行计算,并把结果返回给client


#client.pyimport pikaimport uuidclass FibRPCClient(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 == None:            self.connection.process_data_events()        return int(self.response)print 'Request for fib(5)'fib_client = FibRPCClient()print 'RPC response : %d' % fib_client(5)

以上是client.py的代码

我们用一个类来对代码进行了封装,当然不使用类也是可以的。

首先在构造函数__init__里建立了了连接,通道和一个用于接收返回信息的queue,并从该queue中收取消息。

on_response()方法是客户端处理服务器返回结果的方法

__call__方法中,通过basic_publish方法发送消息,并在参数properties中设置了返回的消息应该放到哪个queue(reply_to参数)以及消息的correlation_id。correlation_id中文名叫关联RPC响应,用于标识在返回消息队列中,该消息是返回给你的,因为可能有多个服务器使用该队列返回消息给多个不用的客户端。

关于basic_publish()的properties参数:

elivery_mode: 持久化一个Message(通过设定值为2)。其他任意值都是非持久化。
content_type: 描述mime-type 的encoding。比如设置为JSON编码:设置该property为application/json。
reply_to: 一般用来指明用于回调的queue(Commonly used to name a callback queue)。
correlation_id: 在请求中关联处理RPC响应(correlate RPC responses with requests)。

代码的最后用了一个忙等待来等待返回结果。


服务器端的代码:

# sever.pyimport pikadef fib(n):    if n==0:        return 0    elif n==1:        return 1    else:        return fib(n-1) + fib(n-2)connection = pika.BlockingConnection(pika.ConnectionParameters(host='localhost'))channel = connection.channel()channel.queue_declare(queue='rpc_queue')def on_request(ch, method, props, body):    n = int(body)    print 'Calculate Fib %d' % n    response = fib(n)    ch.basic_publish(exchange='', routing_key=props.reply_to,                     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 'RPC ing'channel.start_consuming()

第4行:定义了一个计算斐波那契数列的函数

第12-14行:建立连接,通道,和队列。队列名为rpc_queue,客户端往该queue发送消息

第16行:定义了一个处理message的回调函数,在收到消息后,往props指定的队列返回一个包含处理结果的消息,并发送ACK

第27行:指定一个服务器最多有一个未ACK的消息在处理

第28行:从队列rcp_queue中接收消息

第31行:开始监听rcp_queue



七. Google Protocal Buffer

一种轻便高效的结构化数据存储格式

参考:http://blog.csdn.net/anzhsoft/article/details/19771671



八. Publisher的消息确认机制

http://blog.csdn.net/anzhsoft/article/details/21603479




原创粉丝点击