RabbitMQ是实现AMQP(高级消息队列协议)的消息中间件的一种,最初起源于金融系统,用于在分布式系统中存储转发消息,在易用性、扩展性、高可用性等方面表现不俗。和普通的queue比较起来,生产和消费不再针对内存里的一个Queue对象,而是某台服务器上的RabbitMQ Server实现的消息队列。

RabbitMQ主要是为了实现系统之间的双向解耦而实现的。当生产者大量产生数据时,消费者无法快速消费,那么需要一个中间层。保存这个数据。



首先来他需要安装erlang语言包和rabbitmq-server,启动服务,然后打开端口5672


服务器(CentOS7)

yum install erlang
yum install rabbitmq-server
systemctl start rabbitmq-server
systemctl status rabbitmq-server
firewall-cmd --add-port=5672/tcp --permanent
systemctl restart firewalld




Python客户端(windows),安装pika模块


C:\WINDOWS\system32>pip install pika
Collecting pika
  Downloading pika-0.10.0-py2.py3-none-any.whl (92kB)
    100% |################################| 102kB 632kB/s
Installing collected packages: pika
Successfully installed pika-0.10.0



现在看看Python里面如何使用:


例1 Hello World


Python 学习笔记 - RabbitMQ_第1张图片


发送


import pika
# ######################### 生产者 #########################
#绑定到一个broker上面
connection = pika.BlockingConnection(pika.ConnectionParameters(
        host='sydnagios'))
channel = connection.channel()

#创建一个queue用来传输信息
channel.queue_declare(queue='hello1')

#RabbitMQ不可以直接给queue发送信息,必须通过exchange,这里空字符串表示默认的exchange
channel.basic_publish(exchange='',
                      routing_key='hello1',
                      body='Hello World!')
print(" [x] Sent 'Hello World!'")

#清空queue,断开连接
connection.close()



接收


#!/usr/bin/env python
import pika

# ########################## 消费者 ##########################
connection = pika.BlockingConnection(pika.ConnectionParameters(
    host='sydnagios'))
channel = connection.channel()
# 和生产者一样,这里也需要定义一个queue,这是因为我们不知道到底是生产者和消费者谁先执行;这个queue即使多次定义也只会创建一个对象
channel.queue_declare(queue='hello1')


# 每当接收到一个信息,pika库会自动调用callback函数
def callback(ch, method, properties, body):
    print(" [x] Received %r" % body)


# 指定callback从哪个queue获取信息    
channel.basic_consume(callback,
                      queue='hello1',
                      no_ack=True)
print(' [*] Waiting for messages. To exit press CTRL+C')

# 死循环,不停阻塞接收信息
channel.start_consuming()



如果在RabbitMQ的服务器上执行以下操作,可以看见queue里面有几个信息

比如发送者发送了2条信息之后,可以看见hello1数目变成2,如果我用客户端去取了2次,那么他又会变成0

[root@sydnagios nagvis]# sudo rabbitmqctl list_queues
Listing queues ...
hello   0
hello1  2
kakaxi1 0
...done.


例2, 工作队列和可靠性


Python 学习笔记 - RabbitMQ_第2张图片

例1里面我们通过一个queue发送和接受了信息;我们也可以创建一个工作队列(Task Queue)来给多个客户端发送信息,这种模型适合于那种特别耗时的任务;感觉这个和之前线程池的方式类似,所有的任务放在queue里面,然后每个线程(客户端)不停地去取任务执行。


在默认情况下,任务的分发是通过round-robin(轮换)的方式实现的,比如C1接受任务1,C2任务2,C1任务3,C2任务4...这样的缺点是如果任务的耗时不同,可能C1一直在执行一堆繁重的任务,而C2分到的都是轻量级的任务,一直很空闲。我们可以通过指定channel.basic_qos(prefetch_count=1)来实现公平分发,换句话说消息只会分发给空闲的客户端。


RabbitMQ里面有3种方式来确保消息的可靠性。


第一个方式是在消费者方面进行ACK的确认,每次成功接收消息之后发送确认信号,如果意外中止了,RabbitMQ会把任务重新放入队列中,然后发给下一个客户端,比如C1如果刚刚收到任务1就挂了,因为C1没有确认,那么RabitMQ会把任务1重新发给C2;确认是通过下面的代码实现的


import pika
# ########################## 消费者 ##########################
connection = pika.BlockingConnection(pika.ConnectionParameters(
        host='sydnagios'))
channel = connection.channel()
channel.queue_declare(queue='hello1')
def callback(ch, method, properties, body):
    print(" [x] Received %r" % body)
    import time
    time.sleep(10)
    print ('ok')
    ch.basic_ack(delivery_tag = method.delivery_tag)
channel.basic_consume(callback,
           queue='hello1',
           no_ack=False)
print(' [*] Waiting for messages. To exit press CTRL+C')
channel.start_consuming()
---------
 [*] Waiting for messages. To exit press CTRL+C
 [x] Received b'Hello World!'
ok


注意no_ack默认是Fasle,因此可以不写

ch.basic_ack如果忘记写了,后果会很严重,客户端掉线的时候,RabbitMQ会转发消息给下一个客户,但是他不会释放掉没有被ACK的消息,这样内存不被不断的吃掉。

可以通过下面命令进行debug

[root@sydnagios nagvis]# rabbitmqctl list_queues name messages_ready messages_unacknowledged
Listing queues ...
hello   0       0
hello1  0       0
kakaxi1 0       0
task_queue      0       0


第二种方式是确保queue不会丢失,注意这种方式对已经创建过的queue无效!

注意客户端和服务器端申明的时候都要指定

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


第三种可靠性的方法是消息的持久化,针对生产者指定delivery_mode=2,这样即使生产者那边挂了,生产者那边会重新把任务放入队列。 

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


最后来个完整的例子


生产者

#!/usr/bin/env python
# -*- coding:utf-8 -*-
# Author Yuan Li
#!/usr/bin/env python
import pika
import sys
connection = pika.BlockingConnection(pika.ConnectionParameters(
        host='sydnagios'))
channel = connection.channel()
#queue durable
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()



消费者

#!/usr/bin/env python
# -*- coding:utf-8 -*-
# Author Yuan Li
#!/usr/bin/env python
import pika
import time
connection = pika.BlockingConnection(pika.ConnectionParameters(
        host='sydnagios'))
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(prefetch_count=1)
channel.basic_consume(callback,
                      queue='task_queue')
channel.start_consuming()


例3 发布订阅


RabbitMQ可以通过一个Exchange来同时给多个Queue发送消息,一般情况下,P(发布者)并不知道信息应该发给哪个queue,这些都是有Exchange的类型决定的。Exchange有3种常用类型

  • fanout 转发消息到所有的绑定队列

  • direct 通过一个关键字(routingkey)匹配转发消息到指定的队列

  • topic 模糊匹配转发消息到指定的队列

  • header


可以看见exchange的列表

[root@sydnagios nagvis]# 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
...done.



Python 学习笔记 - RabbitMQ_第3张图片

Fanout类型

消费者

# Author:Alex Li
#!/usr/bin/env python
import pika
connection = pika.BlockingConnection(pika.ConnectionParameters(
        host='sydnagios'))
channel = connection.channel()
channel.exchange_declare(exchange='logs_fanout',
                         type='fanout')
# 随机创建队列,exclusive=True表示当我们断开和消费者的连接,这个queue会自动删除
result = channel.queue_declare(exclusive=True)
queue_name = result.method.queue
# 绑定
channel.queue_bind(exchange='logs_fanout',
                   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()


生产者

# Author:Alex Li
#!/usr/bin/env python
import pika
import sys
connection = pika.BlockingConnection(pika.ConnectionParameters(
        host='sydnagios'))
channel = connection.channel()
channel.exchange_declare(exchange='logs_fanout',
                         type='fanout')
message = '456'

#注意在fanout模式里面,routing_key为空
channel.basic_publish(exchange='logs_fanout',
                      routing_key='',
                      body=message)
print(" [x] Sent %r" % message)
connection.close()



Direct类型,消息发送给和他自己的routing key同名binding key的队列,多个队列可以使用同一个binding key

Python 学习笔记 - RabbitMQ_第4张图片


direct可以指定关键字来绑定queue,比如第一个客户循环地绑定了error,info,warning3个关键字所在的queue


第二个客户只绑定了error


客户1

# Author:Alex Li
import pika
import sys
connection = pika.BlockingConnection(pika.ConnectionParameters(
        host='sydnagios'))
channel = connection.channel()
channel.exchange_declare(exchange='direct_logs_test_1',
                         type='direct')
result = channel.queue_declare(exclusive=True)
queue_name = result.method.queue
severities = ['error', 'info', 'warning']
for severity in severities:
    channel.queue_bind(exchange='direct_logs_test_1',
                       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()


客户2

# Author:Alex Li
import pika
import sys
connection = pika.BlockingConnection(pika.ConnectionParameters(
        host='sydnagios'))
channel = connection.channel()
channel.exchange_declare(exchange='direct_logs_test_1',
                         type='direct')
result = channel.queue_declare(exclusive=True)
queue_name = result.method.queue
severities = ['error',]
for severity in severities:
    channel.queue_bind(exchange='direct_logs_test_1',
                       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()


生产者可以指定给哪些routingkey的queue发送信息,比如只发给info,那么只有客户2收到;如果发给error,那么两个客户都能收到

# Author:Alex Li
#!/usr/bin/env python
import pika
connection = pika.BlockingConnection(pika.ConnectionParameters(
        host='sydnagios'))
channel = connection.channel()
channel.exchange_declare(exchange='direct_logs_test_1',
                         type='direct')
severity = 'info'
message = '456'
channel.basic_publish(exchange='direct_logs_test_1',
                      routing_key=severity,
                      body=message)
print(" [x] Sent %r:%r" % (severity, message))
connection.close()

Python 学习笔记 - RabbitMQ_第5张图片


在topic类型下,可以让队列绑定几个模糊的关键字,之后发送者将数据发送到exchange,exchange将传入”路由值“和 ”关键字“进行匹配,匹配成功,则将数据发送到指定队列。

  • # 表示可以匹配 0 个 或 多个 单词

  • *  表示只能匹配 一个 单词

1
2
3
发送者路由值              队列中
old.boy.python          old. *   - -  不匹配
old.boy.python          old. #  -- 匹配



最后我们来看看RPC


简单的说就是在远程电脑执行命令然后返回结果

Python 学习笔记 - RabbitMQ_第6张图片

基本思路:

客户端发送请求(包括请求的correlation_id和reply_to队列),服务器端收到之后执行命令,返回结果到reply_to的队列里面,然后客户端从reply_to 队列读取数据


例如

result = channel.queue_declare(exclusive=True)
callback_queue = result.method.queue
channel.basic_publish(exchange='',
                      routing_key='rpc_queue',
                      properties=pika.BasicProperties(
                            reply_to = callback_queue,
                            ),
                      body=request)



注意properties属性里面有14个预定义的值,其中4个最为常见:

  • delivery_mode: 消息的持久化(2)

  • content_type:用于mime-type的编码,一般Json使用application/json

  • reply_to:callback的队列

  • correlation_id:关联RPC反馈信息和请求命令,每个请求都需要有唯一的值



演示源码


服务器端(需要先接受信息,再发布信息回去)

#!/usr/bin/env python
# -*- coding:utf-8 -*-
# Author Yuan Li
#!/usr/bin/env python
import pika

#绑定broker,创建一个队列
connection = pika.BlockingConnection(pika.ConnectionParameters(
        host='sydnagios'))
channel = connection.channel()
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)
        
#定义一个回调函数给basic_consume使用        
def on_request(ch, method, props, body):
    n = int(body)
    print(" [.] fib(%s)" % 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)

#接受请求之后,自动调用on_request,内部执行函数,然后发回结果
channel.basic_consume(on_request, queue='rpc_queue')
print(" [x] Awaiting RPC requests")
channel.start_consuming()


发布者

#!/usr/bin/env python
# -*- coding:utf-8 -*-
# Author Yuan Li
#!/usr/bin/env python
import pika
import uuid
class FibonacciRpcClient(object):

    def __init__(self):
            #绑定
        self.connection = pika.BlockingConnection(pika.ConnectionParameters(
                host='sydnagios'))
        self.channel = self.connection.channel()
        
        #生成随机队列
        result = self.channel.queue_declare(exclusive=True)
        self.callback_queue = result.method.queue
        
        #指定on_response从callback_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())
        
        #发送两个参数 reply_to和 correlation_id
        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 is None:
            self.connection.process_data_events()
        return int(self.response)
        
#实例化对象
fibonacci_rpc = FibonacciRpcClient()
print(" [x] Requesting fib(30)")

#调用call,发送数据
response = fibonacci_rpc.call(30)
print(" [.] Got %r" % response)


结果如下:

"C:\Program Files\Python3\python.exe" C:/Users/yli/pycharmprojects/Exercise/Week11/c.py
 [x] Requesting fib(30)
 [.] Got 832040



参考资料:

http://www.cnblogs.com/wupeiqi/articles/5132791.html

https://www.rabbitmq.com/tutorials/tutorial-five-python.html

https://geewu.gitbooks.io/rabbitmq-quick/content/RabbitMQ%E4%BB%8B%E7%BB%8D.html