本篇博文将结合自己的使用经验介绍rabitmq在Python中的相关用法,主要参考Pika的API,介绍内容:Message acknowledgment(消息确认机制),Fair Dispatch()
最基础的发布订阅实例
1.conusmer:消息的订阅者,即使用消息(自身也可以发布消息)
2.producer:消息的发布者(自身也可以订阅消息),先多嘴一句,消息是不能直接send给consumer的,必须经过交换机,如果没有事先声明交换机我们可以直接采用默认匿名交换机就行,不影响程序。
3.queue:消息储存的管道,即producer将消息发布到queue当中,然后consumer根据自身所需去订阅相关消息,队列仅受主机内存和磁盘限制的约束,本质上是一个大的消息缓冲区。 许多生产者可以将消息发送到一个队列,许多消费者可以尝试从一个队列接收数据,只要声明routing_key就行。
exchange,routing_key,ack这些概念后续会逐一详细介绍。
实例:用rabitmq发布helloworld
publisher逻辑:声明connection–>声明channel–>declare一个queue–>basic_publish(exchange = ’ ‘,routing_key=’’, body=’’)
receiver逻辑:声明connection–>声明channel–>declare一个queue–>basic_consume(queue=‘队列名’,on_message_callback = ‘回调函数名’ )
################send.py#################
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!')
connection.close()
#################receiver.py###############
impotr pika
connetcion = pika.BlockingConnection(
pika.ConnectionParameters(host='localhost'))
channel = connection.channel()
channel.queue_declare(queue='hello')#记住这里的queue name必须和send的相同!
def callback(ch, method, properties, body):
print(" [x] Received %r" % body) #回调函数,打印收到的消息,也可以不写或者随便写
channel.basic_consume(
queue='hello', on_message_callback=callback, auto_ack=True)
channel.start_consuming()
Working queue(任务队列)
任务队列:避免立即执行资源密集型的任务而使其他任务被堵赛,为解决这个问题,我们将任务封装成消息,consumer会逐个pop这些任务并执行,如果同时有多个consumer来pop就可以提高执行效率。这就是告诉我们当数据传输量较大或者传输的任务较复杂的时候,我们可以让多个consumer来进行分布式订阅消息。
rabbitmq如何防止消息丢失呢?1.rabbitmq支持消息确认机制,当consumer收到消息的时候回发回ack来告诉broker当前消息已经收到,那么broker就可以自由的删除该消息。2.如果在订阅过程中一个consumer突然die并且没有回发ack,那么rabbitmq会任务该消息发送失败,如果此时有有其他consumer在线且也订阅了该消息,那么broker会立即将当前的消息发送给其他consumer,如果没有在线的consumer该消息会一直排队等待。3.如果rabbitmq服务端突然崩掉消息也会丢失的因为在正常情况下rabbitmq在退出或者崩掉时候回忘掉当前的消息贺队列,除非我们声明不要忘记。那么我们将队列声明成durable类型,及时rabbitmq重启队列和消息也不会丢失。4.将message声明成可持久性,即告诉rabbitmq将消息写到内存当中,只有再确认收到的时候才删除。
properties=pika.BasicProperties(
delivery_mode = 2, # make message persistent
)
rabbitmq如何做到公平分配任务呢?rabbitmq只会在消息入队列的时候调度消息而不会查看消费者来确认消息数量,因此可能会造成一个producer特别忙而其他producer特别轻松,我们可以用basic.qos和prefetch_count=1来解决这个问题,即在确认当前消息被consumer消费之前不会再向当前的producer分配新任务而是将任务转发给其他light producer
Publish and Subscribe(发布订阅)
在最开始的代码中我们直接在producer端声明队列来发布消息然后在consumer端声明同样的队列名来订阅消息,其实这是极其不规范的,从引入exchange开始,我们在producer端不再声明队列,而是声明exchange,将消息转发到exchange,然后再在consumer端去声明队列并绑定exchange,实现消息的发布订阅。
最典型的发布订阅模型:
producer将message通过exchange转发到绑定的queue,然后consumer根据自己的routing来订阅需要的消息。
前面已经提到过的exchange就是用来转发消息的
4.exchange分为四种类型,direct,topic,headers,fanout
fanout类型:将收到的消息广播到与之绑定的所有队列当中
其他类型会逐一介绍到
发送端完整代码:
import pika
connection = pika.BlockingConnection(
pika.ConnectionParameters(host='localhost'))
channel = connection.channel()
channel.exchange_declare(exchange='logs',type='fanout')
channel.basic_publish(exchange='logs', routing_key='', body=message)
接收端完整代码:
import pika
connection = pika.BlockingConnection(
pika.ConnectionParameters(host='localhost'))
channel = connection.channel()
channel.exchange_declare(exchange='logs', exchange_type='fanout')
result = channel.queue_declare(queue='', exclusive=True)
queue_name = result.method.queue
channel.queue_bind(exchange='logs', queue=queue_name)
def callback(ch, method, properties, body):
print(" [x] %r" % body)
channel.basic_consume(
queue=queue_name, on_message_callback=callback, auto_ack=True)
channel.start_consuming()
在发送端首先声明一个exchange:channel.exchange_declare(exchange=‘logs’,type=‘fanout’)
然后在publish的时候带上exchange:channel.basic_publish(exchange=‘logs’, routing_key=’’, body=message)
在接收端同样需要先声明一个exchange,然后声明一个队列用来告诉exchange将消息发送给我们的队列:
result = channel.queue_declare(queue=’’, exclusive=True)
channel.queue_bind(exchange=‘logs’, queue=queue_name)#绑定
Routing(路由)
那么如果我们希望producer发布的不同消息传递给不同的队列让不同的consumer群体来订阅该如何解决呢?这里就引进了exchange的第二个常见类型:direct以及我们前面经常提到的routing_key
一个producer,声明一个direct类型的exchange,然后给每条消息带上一个非空的routing_key发布对对应的queue当中,最后不同的consumer根据自身的routing_key来订阅消息
发送端完整代码:
import pika
import sys
connection = pika.BlockingConnection(
pika.ConnectionParameters(host='localhost'))
channel = connection.channel()
channel.exchange_declare(exchange='direct_logs', exchange_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()
首先声明exchange名和类型,然后在publish的时候带上exchange和routing_key
接收端完整代码:
import pika
import sys
connection = pika.BlockingConnection(
pika.ConnectionParameters(host='localhost'))
channel = connection.channel()
channel.exchange_declare(exchange='direct_logs', exchange_type='direct')
result = channel.queue_declare(queue='', 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(
queue=queue_name, on_message_callback=callback, auto_ack=True)
channel.start_consuming()
首先也是声明exchange,然后绑定queue,绑定routing_key,最后consume消息。
以上代码,比如终端发送了3条消息对应的routing_key分别是r1=asdf45,r2=ghkyi66,r3=wyuihdl89,同样在接收端也声明了3个队列并绑定了r1,r2,r3,那么消息就被分为三类了,每一类消息发送到对应的queue当中供consumer订阅。
topic(基于主题的交换)
那么之前介绍过direct类型的exchange,就是传递不同的routing_key到不同的queue中然后consumer自由选择queue去订阅,这里引进了topic类型的exchange,发送到主题交换的消息不能再具有任意的routing_key,而是由点分割的单词列表组成,模型如下:
* 表示可以接一个Word
#表示可以接任何多个的Word
如:routing_key = "quick.orange.rabbit"则会将消息转发给Q1和Q2
routing_key = "lazy.orange.elephant"则会将消息转发给Q1和Q2
routing_key = "quick.orange.fox"则只会讲消息发给Q1
直白讲,topic类型交换机主要指routing_key可以表示一类路由型,即包含多条单个的direct类型的routing,是一个集合,适用于消息分类明显的数据流业务中
发送端完整代码:
import pika
import sys
connection = pika.BlockingConnection(
pika.ConnectionParameters(host='localhost'))
channel = connection.channel()
channel.exchange_declare(exchange='topic_logs', exchange_type='topic')
routing_key = sys.argv[1] if len(sys.argv) > 2 else 'anonymous.info'
message = ' '.join(sys.argv[2:]) or 'Hello World!'
channel.basic_publish(
exchange='topic_logs', routing_key=routing_key, body=message)
print(" [x] Sent %r:%r" % (routing_key, message))
connection.close()
接收端代码:
import pika
import sys
connection = pika.BlockingConnection(
pika.ConnectionParameters(host='localhost'))
channel = connection.channel()
channel.exchange_declare(exchange='topic_logs', exchange_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(
queue=queue_name, on_message_callback=callback, auto_ack=True)
channel.start_consuming()
RPC模型
当客户端启动时会创建一个匿名的exclusive回调队列,对于rpc请求,客户端发送带有两个属性的消息:replay_to:设置回调队列,correlation_id:为每个请求设置唯一的id,request发送到rpc_queue,服务端等待该队列的请求,当请求出现的时候,server会执行并使用replay_to字段中的队列将结果返回给客户端,client等待回调队列上的数据,出现消息时,它会检查correlation_id属性,如果他与请求中的值匹配,则将消息返回给应用程序
通过代码完整的分析一下:
此时的发送端我们称之为consumer(和前面刚好相反),client发送请求消息,server接收请求并返回响应。发送端完整代码:
import pika
import uuid
class FibonacciRpcClient(object):
def __init__(self):
self.connection = pika.BlockingConnection(
pika.ConnectionParameters(host='localhost'))
self.channel = self.connection.channel()
result = self.channel.queue_declare(queue='', exclusive=True)
self.callback_queue = result.method.queue
self.channel.basic_consume(
queue=self.callback_queue,
on_message_callback=self.on_response,
auto_ack=True)
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 is None:
self.connection.process_data_events()
return int(self.response)
fibonacci_rpc = FibonacciRpcClient()
print(" [x] Requesting fib(30)")
response = fibonacci_rpc.call(30)
print(" [.] Got %r" % response)
client声明channel,queue(这个queue是用来读取server发回来的消息),然后调用basic_consume来读取发回来的消息,最后在发送请求消息的时候routing_key设置为server端的queue名(rpc_queue),并且在message属性中设置replay·_to和correlation_id
server端
import pika
connection = pika.BlockingConnection(
pika.ConnectionParameters(host='localhost'))
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)
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(queue='rpc_queue', on_message_callback=on_request)
print(" [x] Awaiting RPC requests")
channel.start_consuming()
server端:声明一个roc_queue来获取client发送的请求,然后回调函数直接publish消息,这里的routing_key要绑定client端的队列名,即reply_to,并且带上correlation_id这样就完美了
在工作中还有一个库叫aioamqp,使用方法大同小异,主要是aioamqp支持消息的异步发送订阅,有时间我会再总结一下。