RabbitMQ详解

http://rabbitmq.mr-ping.com 中文文档html

什么叫消息队列

消息(Message)是指在应用间传送的数据。消息能够很是简单,好比只包含文本字符串,也能够更复杂,可能包含嵌入对象。python

消息队列(Message Queue)是一种应用间的通讯方式,消息发送后能够当即返回,由消息系统来确保消息的可靠传递。消息发布者只管把消息发布到 MQ 中而不用管谁来取,消息使用者只管从 MQ 中取消息而不论是谁发布的。这样发布者和使用者都不用知道对方的存在。web

为什么用消息队列

从上面的描述中能够看出消息队列是一种应用间的异步协做机制,那何时须要使用 MQ 呢?服务器

以常见的订单系统为例,用户点击【下单】按钮以后的业务逻辑可能包括:扣减库存、生成相应单据、发红包、发短信通知。在业务发展初期这些逻辑可能放在一块儿同步执行,随着业务的发展订单量增加,须要提高系统服务的性能,这时能够将一些不须要当即生效的操做拆分出来异步执行,好比发放红包、发短信通知等。这种场景下就能够用 MQ ,在下单的主流程(好比扣减库存、生成相应单据)完成以后发送一条消息到 MQ 让主流程快速完结,而由另外的单独线程拉取MQ的消息(或者由 MQ 推送消息),当发现 MQ 中有发红包或发短信之类的消息时,执行相应的业务逻辑。负载均衡

RabbitMQ 

RabbitMQ 是一个由 Erlang 语言开发的 AMQP 的开源实现。异步

rabbitMQ是一款基于AMQP协议的消息中间件,它可以在应用之间提供可靠的消息传输。在易用性,扩展性,高可用性上表现优秀。使用消息中间件利于应用之间的解耦,生产者(客户端)无需知道消费者(服务端)的存在。并且两端可使用不一样的语言编写,大大提供了灵活性。ide

 

中文文档函数

rabbitMQ安装

for Linux: 安装配置epel源 $ rpm -ivh http://dl.fedoraproject.org/pub/epel/6/i386/epel-release-6-8.noarch.rpm 安装erlang $ yum -y install erlang 安装RabbitMQ $ yum -y install rabbitmq-server 注意:service rabbitmq-server start/stop
for Mac: bogon:~ yuan$ brew install rabbitmq bogon:~ yuan$ export PATH=$PATH:/usr/local/sbin bogon:~ yuan$ rabbitmq-server

rabbitMQ工做模型

简单模式

示例

# ######################### 生产者 ######################### #!/usr/bin/env python
import pika connection = pika.BlockingConnection(pika.ConnectionParameters( host='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()
# ########################## 消费者 ##########################
 connection = pika.BlockingConnection(pika.ConnectionParameters(host='localhost')) channel = connection.channel() channel.queue_declare(queue='hello') def callback(ch, method, properties, body): print(" [x] Received %r" % body) channel.basic_consume( callback, queue='hello', no_ack=True) print(' [*] Waiting for messages. To exit press CTRL+C') channel.start_consuming()

相关参数

(1)no-ack = False,若是消费者遇到状况(its channel is closed, connection is closed, or TCP connection is lost)挂掉了,那么,RabbitMQ会从新将该任务添加到队列中。性能

  • 回调函数中的ch.basic_ack(delivery_tag=method.delivery_tag)
  • basic_comsume中的no_ack=False

消息接收端应该这么写:fetch

import pika connection = pika.BlockingConnection(pika.ConnectionParameters( host='10.211.55.4')) channel = connection.channel() channel.queue_declare(queue='hello') 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='hello', no_ack=False) print(' [*] Waiting for messages. To exit press CTRL+C') channel.start_consuming()

(2)  durable  :消息不丢失

# 生产者 #!/usr/bin/env python
import pika connection = pika.BlockingConnection(pika.ConnectionParameters(host='10.211.55.4')) channel = connection.channel() # make message persistent
channel.queue_declare(queue='hello', durable=True) channel.basic_publish(exchange='', routing_key='hello', body='Hello World!', properties=pika.BasicProperties( delivery_mode=2, # make message persistent
 )) print(" [x] Sent 'Hello World!'") connection.close() # 消费者 #!/usr/bin/env python # -*- coding:utf-8 -*-
import pika connection = pika.BlockingConnection(pika.ConnectionParameters(host='10.211.55.4')) channel = connection.channel() # make message persistent
channel.queue_declare(queue='hello', durable=True) 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='hello', no_ack=False) print(' [*] Waiting for messages. To exit press CTRL+C') channel.start_consuming()

(3) 消息获取顺序

默认消息队列里的数据是按照顺序被消费者拿走,例如:消费者1 去队列中获取 奇数 序列的任务,消费者1去队列中获取 偶数 序列的任务。

channel.basic_qos(prefetch_count=1) 表示谁来谁取,再也不按照奇偶数排列

#!/usr/bin/env python # -*- coding:utf-8 -*-
import pika connection = pika.BlockingConnection(pika.ConnectionParameters(host='10.211.55.4')) channel = connection.channel() # make message persistent
channel.queue_declare(queue='hello') 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_qos(prefetch_count=1) channel.basic_consume(callback, queue='hello', no_ack=False) print(' [*] Waiting for messages. To exit press CTRL+C') channel.start_consuming()

exchange模型

3.1 发布订阅

发布订阅和简单的消息队列区别在于,发布订阅会将消息发送给全部的订阅者,而消息队列中的数据被消费一次便消失。因此,RabbitMQ实现发布和订阅时,会为每个订阅者建立一个队列,而发布者发布消息时,会将消息放置在全部相关队列中。

exchange type = fanout
# 生产者 #!/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') 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() # 消费者 #!/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_name = result.method.queue channel.queue_bind(exchange='logs', 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()
View Code

 3.2 关键字发送

exchange type = direct

以前事例,发送消息时明确指定某个队列并向其中发送消息,RabbitMQ还支持根据关键字发送,即:队列绑定关键字,发送者将数据根据关键字发送到消息exchange,exchange根据 关键字 断定应该将数据发送至指定队列。

#!/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: sys.stderr.write("Usage: %s [info] [warning] [error]\n" % 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()
View Code

 3.3 模糊匹配

exchange type = topic

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

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

  • # 表示能够匹配 0 个 或 多个 单词
  • *  表示只能匹配 一个 单词

 示例:

#!/usr/bin/env python
import pika import sys connection = pika.BlockingConnection(pika.ConnectionParameters( host='localhost')) channel = connection.channel() channel.exchange_declare(exchange='topic_logs', type='topic') result = channel.queue_declare(exclusive=True) queue_name = result.method.queue binding_keys = sys.argv[1:] if not binding_keys: sys.stderr.write("Usage: %s [binding_key]...\n" % sys.argv[0]) sys.exit(1) for binding_key in binding_keys: channel.queue_bind(exchange='topic_logs', queue=queue_name, routing_key=binding_key) 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的RPC

Callback queue 回调队列

一个客户端向服务器发送请求,服务器端处理请求后,将其处理结果保存在一个存储体中。而客户端为了得到处理结果,那么客户在向服务器发送请求时,同时发送一个回调队列地址reply_to

Correlation id 关联标识

一个客户端可能会发送多个请求给服务器,当服务器处理完后,客户端没法辨别在回调队列中的响应具体和那个请求时对应的。为了处理这种状况,客户端在发送每一个请求时,同时会附带一个独有correlation_id属性,这样客户端在回调队列中根据correlation_id字段的值就能够分辨此响应属于哪一个请求。


客户端发送请求:某个应用将请求信息交给客户端,而后客户端发送RPC请求,在发送RPC请求到RPC请求队列时,客户端至少发送带有reply_to以及correlation_id两个属性的信息 服务器端工做流: 等待接受客户端发来RPC请求,当请求出现的时候,服务器从RPC请求队列中取出请求,而后处理后,将响应发送到reply_to指定的回调队列中 客户端接受处理结果: 客户端等待回调队列中出现响应,当响应出现时,它会根据响应中correlation_id字段的值,将其返回给对应的应用

服务器端

#!/usr/bin/env python
import pika # 创建链接,服务器地址为localhost,可指定ip地址
connection = pika.BlockingConnection(pika.ConnectionParameters( host='localhost')) # 创建会话
channel = connection.channel() # 声明RPC请求队列
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) # 对RPC请求队列中的请求进行处理
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) channel.basic_consume(on_request, queue='rpc_queue') print(" [x] Awaiting RPC requests") channel.start_consuming()

客户端

#!/usr/bin/env python
import pika import uuid class FibonacciRpcClient(object): def __init__(self): ”“” 客户端启动时,建立回调队列,会开启会话用于发送RPC请求以及接受响应 “”“ # 创建链接,指定服务器的ip地址
        self.connection = pika.BlockingConnection(pika.ConnectionParameters( host='localhost')) # 创建一个会话,每一个channel表明一个会话任务
        self.channel = self.connection.channel() # 声明回调队列,再次声明的缘由是,服务器和客户端可能前后开启,该声明是幂等的,屡次声明,但只生效一次
        result = self.channel.queue_declare(exclusive=True) # 将次队列指定为当前客户端的回调队列
        self.callback_queue = result.method.queue # 客户端订阅回调队列,当回调队列中有响应时,调用`on_response`方法对响应进行处理; 
        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 # 发出RPC请求
    def call(self, n): # 初始化 response
        self.response = None #生成correlation_id 
        self.corr_id = str(uuid.uuid4()) # 发送RPC请求内容到RPC请求队列`rpc_queue`,同时发送的还有`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() # 发送RPC请求
print(" [x] Requesting fib(30)") response = fibonacci_rpc.call(30) print(" [.] Got %r" % response)

简介

RabbitMQ:接受消息再传递消息,能够视为一个“邮局”。发送者和接受者经过队列来进行交互,队列的大小能够视为无限的,多个发送者能够发生给一个队列,多个接收者也能够从一个队列中接受消息。

code

rabbitmq使用的协议是amqp,用于python的推荐客户端是pika

pip install pika -i https://pypi.douban.com/simple/

生产者:send.py

import pika # 创建一个链接
connection = pika.BlockingConnection(pika.ConnectionParameters( 'localhost'))  # 链接本地的RabbitMQ服务器
channel = connection.channel()  # 得到channel

这里连接的是本机的,若是想要链接其余机器上的服务器,只要填入地址或主机名便可。

接下来咱们开始发送消息了,注意要确保接受消息的队列是存在的,不然rabbitmq就丢弃掉该消息

channel.queue_declare(queue='hello')  # 在RabbitMQ中建立hello这个队列
channel.basic_publish(exchange='',  # 使用默认的exchange来发送消息到队列
                  routing_key='hello',  # 发送到该队列 hello 中
                  body='Hello World!')  # 消息内容
 connection.close() # 关闭 同时flush

RabbitMQ默认须要1GB的空闲磁盘空间,不然发送会失败。

这时已在本地队列hello中存放了一个消息,若是使用 rabbitmqctl list_queues 可看到

hello 1

说明有一个hello队列 里面存放了一个消息

消费者:receive.py

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

仍是先连接到服务器,和以前发送时相同

channel.queue_declare(queue='hello')  # 此处就是声明了 来确保该队列 hello 存在 能够屡次声明 这里主要是为了防止接受程序先运行时出错

def callback(ch, method, properties, body):  # 用于接收到消息后的回调
    print(" [x] Received %r" % body) channel.basic_consume(callback, queue='hello',  # 收指定队列hello的消息
                      no_ack=True)  #在处理完消息后不发送ack给服务器
channel.start_consuming()  # 启动消息接受 这会进入一个死循环

工做队列(任务队列)

工做队列是用于分发耗时任务给多个工做进程的。不当即作那些耗费资源的任务(须要等待这些任务完成),而是安排这些任务以后执行。例如咱们把task做为message发送到队列里,启动工做进程来接受并最终执行,且可启动多个工做进程来工做。这适用于web应用,即不该在一个http请求的处理窗口内完成复杂任务。

channel.basic_publish(exchange='', routing_key='task_queue', body=message, properties=pika.BasicProperties( delivery_mode = 2, # 使得消息持久化
                  ))

分配消息的方式为 轮询 即每一个工做进程得到相同的消息数。

消息ack

若是消息分配给某个工做进程,可是该工做进程未处理完成就崩溃了,可能该消息就丢失了,由于rabbitmq一旦把一个消息分发给工做进程,它就把该消息删掉了。

为了预防消息丢失,rabbitmq提供了ack,即工做进程在收到消息并处理后,发送ack给rabbitmq,告知rabbitmq这时候能够把该消息从队列中删除了。若是工做进程挂掉 了,rabbitmq没有收到ack,那么会把该消息 从新分发给其余工做进程。不须要设置timeout,即便该任务须要很长时间也能够处理。

ack默认是开启的,以前咱们的工做进程显示指定了no_ack=True

channel.basic_consume(callback, queue='hello')  # 会启用ack

带ack的callback:

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)  # 发送ack

消息持久化

可是,有时RabbitMQ重启了,消息也会丢失。可在建立队列时设置持久化:
(队列的性质一旦肯定没法改变)

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

同时在发送消息时也得设置该消息的持久化属性:

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

可是,若是在RabbitMQ刚接收到消息还没来得及存储,消息仍是会丢失。同时,RabbitMQ也不是在接受到每一个消息都进行存盘操做。若是还须要更完善的保证,须要使用publisher confirm。

公平的消息分发

轮询模式的消息分发可能并不公平,例如奇数的消息都是繁重任务的话,某些进程则会一直运行繁 重任务。即便某工做进程上有积压的消息未处理,如不少都没发ack,可是RabbitMQ仍是会按照顺序发消息给它。能够在接受进程中加设置:

channel.basic_qos(prefetch_count=1)

告知RabbitMQ,这样在一个工做进程没回发ack状况下是不会再分配消息给它。

群发

通常状况下,一条消息是发送给一个工做进程,而后完成,有时想把一条消息同时发送给多个进程:

exchange

发送者是否是直接发送消息到队列中的,事实上发生者根本不知道消息会发送到那个队列,发送者只能把消息发送到exchange里。exchange一方面收生产者的消息,另外一方面把他们推送到队列中。因此做为exchange,它须要知道当收到消息时它须要作什么,是应该把它加到一个特殊的队列中仍是放到不少的队列中,或者丢弃。exchange有direct、topic、headers、fanout等种类,而群发使用的即fanout。以前在发布消息时,exchange的值为 '' 即便用default exchange。

channel.exchange_declare(exchange='logs', type='fanout')  # 该exchange会把消息发送给全部它知道的队列中

临时队列

result = channel.queue_declare()  # 建立一个随机队列
result = channel.queue_declare(exclusive=True)  # 建立一个随机队列,同时在没有接收者链接该队列后则销毁它
queue_name = result.method.queue

这样result.method.queue便是队列名称,在发送或接受时便可使用。

绑定exchange 和 队列

channel.queue_bind(exchange='logs', queue='hello')

logs在发送消息时给hello也发一份。

在发送消息是使用刚刚建立的 logs exchange

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

路由

以前已经使用过bind,即创建exchange和queue的关系(该队列对来自该exchange的消息有兴趣),bind时可另外指定routing_key选项。

使用direct exchange

将对应routing key的消息发送到绑定相同routing key的队列中

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

发送函数,发布不一样severity的消息:

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

接受函数中绑定对应severity的:

channel.queue_bind(exchange='direct_logs', queue=queue_name, routing_key=severity)

使用topic exchange

以前使用的direct exchange 只能绑定一个routing key,可使用这种能够拿.隔开routing key的topic exchange,例如:

"stock.usd.nyse" "nyse.vmw"

和direct exchange同样,在接受者那边绑定的key与发送时指定的routing key相同便可,另外有些特殊的值:

* 表明1个单词 # 表明0个或多个单词

若是发送者发出的routing key都是3个部分的,如:celerity.colour.species。

Q1: *.orange.* 对应的是中间的colour都为orange的 Q2: *.*.rabbit 对应的是最后部分的species为rabbit的 lazy.# 对应的是第一部分是lazy的

qucik.orange.rabbit Q1 Q2均可接收到,quick.orange.fox 只有Q1能接受到,对于lazy.pink.rabbit虽然匹配到了Q2两次,可是只会发送一次。若是绑定时直接绑定#,则会收到全部的。

RPC

在远程机器上运行一个函数而后得到结果。

1、客户端启动 同时设置一个临时队列用于接受回调,绑定该队列

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)

2、客户端发送rpc请求,同时附带reply_to对应回调队列,correlation_id设置为每一个请求的惟一id(虽说能够为每一次RPC请求都建立一个回调队列,可是这样效率不高,若是一个客户端只使用一个队列,则须要使用correlation_id来匹配是哪一个请求),以后阻塞在回调队列直到收到回复

注意:若是收到了非法的correlation_id直接丢弃便可,由于有这种状况--服务器已经发了响应可是还没发ack就挂了,等一会服务器重启了又会从新处理该任务,又发了一遍相应,可是这时那个请求已经被处理掉了

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)

三、请求会发送到rpc_queue队列
四、RPC服务器从rpc_queue中取出,执行,发送回复

channel.basic_consume(on_request, queue='rpc_queue')  # 绑定 等待请求

# 处理以后:
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)  # 发送ack

五、客户端从回调队列中取出数据,检查correlation_id,执行相应操做

if self.corr_id == props.correlation_id: self.response = body
相关文章
相关标签/搜索