RabbitMQ的几种应用场景
之前的几篇文章介绍了一下的RabbitMQ的概念以及环境的搭建和配置,有了RabbitMQ的环境就可以基于其实现一些特殊的任务场景了.RabbitMQ官方有个很好的教程基本覆盖了的RabbitMQ的各中常见应用场景,现以代码加注的方式以其Python客户端pika为例简单介绍如下。更详细的信息可参阅:http ://www.rabbitmq.com/getstarted.html 。
之前的几篇文章:
RabbitMQ的概念及环境搭建(一)单节点安装与配置
的RabbitMQ概念及环境搭建(二)的RabbitMQ经纪人管理
的RabbitMQ概念及环境搭建(三)的RabbitMQ簇
的RabbitMQ概念及环境搭建(四)的RabbitMQ高可用性
RabbitMQ概念及环境搭建(五)与网络的整合
应用场景1-“Hello Word”
一个P向队列发送一个消息,一个Ç从该队列接收消息并打印。
send.py
生产者,连接至RabbitMQ服务器,声明队列,发送消息,关闭连接,退出。
- #!的/ usr / bin中/ python27
- #encoding:UTF8
- 进口 皮卡
- #与RabbitMQ Server建立连接
- #连接到的券商在本机-localhost上
- connection = pika.BlockingConnection(pika.ConnectionParameters(
- host = 'localhost' ))
- channel = connection.channel()
- #声明队列以向其发送消息消息
- #向不存在的位置发送消息时的RabbitMQ将消息丢弃
- #队列= '你好' 指定队列名字
- channel.queue_declare(queue = 'hello' ,durable = True )
- #message不能直接发送给队列,需经交换到达队列,此处使用以空字符串标识的默认的交换
- #使用默认交换时允许通过routing_key明确指定消息将被发送给哪个队列
- #body参数指定了要发送的消息内容
- channel.basic_publish(exchange = '' ,
- routing_key = 'hello' ,
- body = 'Hello World!' )
- 打印“[x]发送”Hello World!“”
- #关闭与RabbitMq Server间的连接
- connection.close()时
consumer,连接至RabbitMQ Server,声明队列,接收消息并进行处理这里为打印出消息,退出。
- #!/ usr / bin / env python
- #encoding:UTF8
- 进口 皮卡
- #建立到达RabbitMQ Server的连接
- #此处RabbitMQ Server位于本机-localhost
- connection = pika.BlockingConnection(pika.ConnectionParameters(
- host = 'localhost' ))
- channel = connection.channel()
- #声明队列,确认要从中接收消息的队列
- #queue_declare函数是幂等的,可运行多次,但只会创建一次
- #若可以确信队列是已存在的,则此处可省略该声明,如生产商已经生成了该队列
- #但在生产和消费中重复声明队列是一个好的习惯
- channel.queue_declare(queue = 'hello' )
- 打印'[*]等待消息。退出按CTRL + C'
- #定义回调函数
- #一旦从队列中接收到一个消息回调函数将被调用
- #ch:信道
- #方法:
- #properties:
- #body:消息
- def callback(ch,method,properties,body):
- 打印“[x]收到%r” %(正文)
- #从队列接收消息的参数设置
- #包括从哪个队列接收消息时,用于处理消息的回调,是否要确认消息
- #默认情况下是要对消息进行确认的,以防止消息丢失。
- #此处将NO_ACK明确指明为真,不对消息进行确认。
- channel.basic_consume(回调,
- queue = 'hello' ,
- no_ack = True )
- #开始循环从队列中接收消息并使用回调进行处理
- channel.start_consuming()
- python send.py
- python receive.py
应用场景2工作队列
将耗时的处理通过队列分配给多个消费者来处理,我们称此处的消费者为工作者,我们将此处的队列称为任务队列,其目的是为了避免资源密集型的任务的同步处理,也即立即处理任务并等待完成。相反,调度任务使其稍后被处理。也即把任务封装进消息并发送到任务队列,工人进程在后台运行,从任务队列取出任务并执行工作,若运行了多个工人,则任务可在多个工人间分配。
new_task.py
建立连接,声明队列,发送可以模拟耗时任务的消息,断开连接,退出。
- #!/ usr / bin / env python
- #encoding:UTF8
- 进口 皮卡
- 导入 系统
- connection = pika.BlockingConnection(pika.ConnectionParameters(
- host = 'localhost' ))
- channel = connection.channel()
- #仅仅对消息进行确认不能保证消息不丢失,比如RabbitMQ的崩溃了队列就会丢失
- #因此还需使用耐用=真声明队列是持久化的,这样即便拉布崩溃了重启后队列仍然存在
- channel.queue_declare(queue = 'task_queue' ,durable = True )
- #从命令行构造将要发送的消息
- message = ''. join(sys.argv [ 1 :]) 或“Hello World!”
- #除了要声明队列是持久化的外,还需声明消息是持久化的
- #basic_publish的性能参数指定信息的属性
- #此处pika.BasicProperties中的delivery_mode = 2指明消息为持久的
- #这样一来的RabbitMQ崩溃重启后队列仍然存在其中的消息也仍然存在
- #需注意的是将消息标记为持久的并不能完全保证消息不丢失,因为
- #从RabbitMQ的接收到的消息到将其存储到磁盘仍需一段时间,若此时的RabbitMQ崩溃则消息会丢失
- #况且的RabbitMQ不会对每条消息做FSYNC动作
- #可通过发布者确认实现更强壮的持久性保证
- channel.basic_publish(exchange = '' ,
- routing_key = 'task_queue' ,
- 体=消息
- 属性= pika.BasicProperties(
- delivery_mode = 2 , #make message persistent
- ))
- 打印“[x]发送%r” %(消息,)
- connection.close()时
建立连接,声明队列,不断的接收消息,处理任务,进行确认。
- #!/ usr / bin / env python
- #encoding:UTF8
- 进口 皮卡
- 进口 时间
- #默认情况RabbirMQ将消息以循环方式发送给下一个客户
- #每个消费者接收到的平均消息量是一样的
- #可以同时运行两个或三个该程序进行测试
- connection = pika.BlockingConnection(pika.ConnectionParameters(
- host = 'localhost' ))
- channel = connection.channel()
- #仅仅对消息进行确认不能保证消息不丢失,比如RabbitMQ的崩溃了
- #还需使用耐用=真声明队列是持久化的,这样即便拉布崩溃了重启后队列仍然存在其中的信息不会丢失
- #RabbitMQ中不允许使用不同的参数定义同名队列
- channel.queue_declare(queue = 'task_queue' ,durable = True )
- 打印'[*]等待消息。退出按CTRL + C'
- #回调函数,函数体模拟耗时的任务处理: '' 以消息中的数量表示睡眠的秒数
- def callback(ch,method,properties,body):
- 打印“[x]收到%r” %(正文)
- time.sleep(body.count('。' ))
- 打印“[x]完成”
- #对消息进行确认
- ch.basic_ack(delivery_tag = method.delivery_tag)
- #若存在多个消费者每个消费者的负载可能不同,有些处理的快有些处理的慢
- #RabbitMQ并不管这些,只是简单的以循环的方式分配消息
- #这可能造成某些消费者积压很多任务处理不完而一些消费者长期处于饥饿状态
- #可以使用prefetch_count = 1的basic_qos方法可告知的RabbitMQ只有在消费者处理并确认了上一个消息后才分配新的消息给他
- #否则分给另一个空闲的消费者
- channel.basic_qos(prefetch_count = 1 )
- #这里移除了NO_ACK =真这个参数,也即需要对消息进行确认(默认行为)
- #否则消费者在偶然下来后其正在处理和分配到该消费者还未处理的消息可能发生丢失
- #因为此时的RabbitMQ在发送完信息后立即从内存删除该消息
- #假如没有设置NO_ACK =真则消费者在偶然向下掉后其正在处理和分配至该消费者但还未来得及处理的消息会重新分配到其他消费者
- #没有设置NO_ACK =真则消费者在收到消息后会向RabbitMQ的反馈已收到并处理了消息告诉的RabbitMQ可以删除该消息
- #RabbitMQ中没有超时的概念,只有在消费者掉掉后重新分发消息
- channel.basic_consume(回调,
- queue = 'task_queue' )
- channel.start_consuming()
- python new_task.py“一个非常艰巨的任务需要两秒钟..”
- python worker.py
应用场景3-发布/订阅
在应用场景2中一个消息(任务)仅被传递给了一个comsumer(工人)。现在我们设法将一个消息传递给多个消费者。这种模式被称为发布/订阅。此处以一个简单的日志系统为例进行说明。该系统包含一个日志发送程序和一个登录接收并打印的程序。由日志发送者发送到队列的消息可以被所有运行的登录接收者接收。因此,我们可以运行一个登录接收者直接在屏幕上显示日志,同时运行另一个日志接收者将日志写入磁盘文件。
receive_logs.py
日志消息接收者:建立连接,声明交换,将交换与队列进行绑定,开始不停的接收日志并打印。
- #!/ usr / bin / env python
- #encoding:UTF8
- 进口 皮卡
- connection = pika.BlockingConnection(pika.ConnectionParameters(
- host = 'localhost' ))
- channel = connection.channel()
- #作为好的习惯,在生产和消费中分别声明一次以保证所要使用的交换存在
- channel.exchange_declare(exchange = 'logs' ,
- type = 'fanout' )
- #在不同的生产者和消费者间共享队列时指明队列的名称是重要的
- #但某些时候,比如日志系统,需要接收所有的日志消息而非一个子集
- #而且对对当前的消息流感兴趣,对于过时的消息不感兴趣,那么
- #可以申请一个临时队列这样,每次连接到RabbitMQ的时会以一个随机的名字生成
- #一个新的空的队列中,将独家置为真,这样在消费者从RabbitMQ的断开后会删除该队列
- result = channel.queue_declare(exclusive=True)
- #用于获取临时queue的name
- queue_name = result.method.queue
- #exchange与queue之间的关系成为binding
- #binding告诉exchange将message发送该哪些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,)
- #从指定地queue中consume message且不确认
- channel.basic_consume(callback,
- queue=queue_name,
- no_ack=True)
- channel.start_consuming()
日志消息发送者:建立连接,声明fanout类型的exchange,通过exchage向queue发送日志消息,消息被广播给所有接收者,关闭连接,退出。
- #!/usr/bin/env python
- #encoding:utf8
- import pika
- import sys
- connection = pika.BlockingConnection(pika.ConnectionParameters(
- host='localhost'))
- channel = connection.channel()
- #producer只能通过exchange将message发给queue
- #exchange的类型决定将message路由至哪些queue
- #可用的exchange类型:direct\topic\headers\fanout
- #此处定义一个名称为'logs'的'fanout'类型的exchange,'fanout'类型的exchange简单的将message广播到它所知道的所有queue
- channel.exchange_declare(exchange='logs',
- type='fanout')
- message = ' '.join(sys.argv[1:]) or "info: Hello World!"
- #将消息发布到名为日志的交换中
- #因为是扇出类型的交换,这里无需指定routing_key
- channel.basic_publish(exchange = 'logs' ,
- routing_key = '' ,
- 体=消息)
- 打印“[x]发送%r” %(消息,)
- connection.close()时
- python receive_logs.py
- python emit_log.py“info:这是日志消息”
应用场景4的路由
应用场景3中构建了简单的日志系统,可以将日志消息广播至多个接收者。现在我们将考虑只把指定的消息类型发送给其订阅者,比如,只把错误消息写到日志文件,而将所有的日志消息显示在控制台。
receive_logs_direct.py
log message接收者:建立连接,声明直接类型的交换,声明队列,使用提供的参数作为routing_key将队列绑定到交换,开始循环接收日志消息并打印。
- #!/usr/bin/env python
- import pika
- import sys
- connection = pika.BlockingConnection(pika.ConnectionParameters(
- host='localhost'))
- channel = connection.channel()
- #声明一个名为direct_logs类型为direct的exchange
- #同时在producer和consumer中声明exchage或queue是个好习惯,以保证其存在
- channel.exchange_declare(exchange='direct_logs',
- type='direct')
- result = channel.queue_declare(exclusive=True)
- queue_name = result.method.queue
- #从命令行获取参数:routing_key
- 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:
- #exchange和queue之间的binding可接受routing_key参数
- #该参数的意义依赖于exchange的类型
- #fanout类型的exchange直接忽略该参数
- #direct类型的exchange精确匹配该关键字进行message路由
- #对多个queue使用相同的binding_key是合法的
- 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()
emit_log_direct.py
log message发送者:建立连接,声明direct类型的exchange,生成并发送log message到exchange,关闭连接,退出。
- #!/usr/bin/env python
- #encoding:utf8
- import pika
- import sys
- connection = pika.BlockingConnection(pika.ConnectionParameters(
- host='localhost'))
- channel = connection.channel()
- #声明一个名为direct_logs的direct类型的exchange
- #direct类型的exchange
- channel.exchange_declare(exchange='direct_logs',
- type='direct')
- #从命令行获取basic_publish的配置参数
- severity = sys.argv[1] if len(sys.argv) > 1 else 'info'
- message = ' '.join(sys.argv[2:]) or 'Hello World!'
- #向名为direct_logs的exchage按照设置的routing_key发送message
- channel.basic_publish(exchange='direct_logs',
- routing_key=severity,
- body=message)
- print " [x] Sent %r:%r" % (severity, message)
- connection.close()
测试:
python receive_logs_direct.py info
python emit_log_direct.py info "The message"
应用场景5-topic
应用场景4中改进的log系统中用direct类型的exchange替换应用场景3中的fanout类型exchange实现将不同的log message发送给不同的subscriber(也即分别通过不同的routing_key将queue绑定到exchange,这样exchange便可将不同的message根据message内容路由至不同的queue)。但仍然存在限制,不能根据多个规则路由消息,比如接收者要么只能收error类型的log message要么只能收info类型的message。如果我们不仅想根据log的重要级别如info、warning、error等来进行log message路由还想同时根据log message的来源如auth、cron、kern来进行路由。为了达到此目的,需要topic类型的exchange。topic类型的exchange中routing_key中可以包含两个特殊字符:“*”用于替代一个词,“#”用于0个或多个词。
receive_logs_topic.py
log message接收者:建立连接,声明topic类型的exchange,声明queue,根据程序参数构造routing_key,根据routing_key将queue绑定到exchange,循环接收并处理message。
- #!/usr/bin/env python
- import pika
- import sys
- connection = pika.BlockingConnection(pika.ConnectionParameters(
- host='localhost'))
- channel = connection.channel()
- #声明一个名为direct_logs类型为direct的exchange
- #同时在producer和consumer中声明exchage或queue是个好习惯,以保证其存在
- channel.exchange_declare(exchange='direct_logs',
- type='direct')
- result = channel.queue_declare(exclusive=True)
- queue_name = result.method.queue
- #从命令行获取参数:routing_key
- 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:
- #exchange和queue之间的binding可接受routing_key参数
- #该参数的意义依赖于exchange的类型
- #fanout类型的exchange直接忽略该参数
- #direct类型的exchange精确匹配该关键字进行message路由
- #对多个queue使用相同的binding_key是合法的
- 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()
emit_log_topic.py
log message发送者:建立连接、声明topic类型的exchange、根据程序参数构建routing_key和要发送的message,以构建的routing_key将message发送给topic类型的exchange,关闭连接,退出。
- #!/usr/bin/env python
- #encoding:utf8
- import pika
- import sys
- connection = pika.BlockingConnection(pika.ConnectionParameters(
- host='localhost'))
- channel = connection.channel()
- #声明一个名为topic_logs的topic类型的exchange
- #topic类型的exchange可通过通配符对message进行匹配从而路由至不同queue
- channel.exchange_declare(exchange='topic_logs',
- type='topic')
- routing_key = sys.argv[1] if len(sys.argv) > 1 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()
测试:
- python receive_logs_topic.py "*.rabbit"
- python emit_log_topic.py red.rabbit Hello
应用场景6-PRC
在应用场景2中描述了如何使用work queue将耗时的task分配到不同的worker中。但是,如果我们task是想在远程的计算机上运行一个函数并等待返回结果呢。这根场景2中的描述是一个完全不同的故事。这一模式被称为远程过程调用。现在,我们将构建一个RPC系统,包含一个client和可扩展的RPC server,通过返回斐波那契数来模拟RPC service。
rpc_server.py
RPC server:建立连接,声明queue,定义了一个返回指定数字的斐波那契数的函数,定义了一个回调函数在接收到包含参数的调用请求后调用自己的返回斐波那契数的函数并将结果发送到与接收到message的queue相关联的queue,并进行确认。开始接收调用请求并用回调函数进行请求处理。
- #!/usr/bin/env python
- #encoding:utf8
- import pika
- #建立到达RabbitMQ Server的connection
- connection = pika.BlockingConnection(pika.ConnectionParameters(
- host='localhost'))
- channel = connection.channel()
- #声明一个名为rpc_queue的queue
- 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)
- #回调函数,从queue接收到message后调用该函数进行处理
- def on_request(ch, method, props, body):
- #由message获取要计算斐波那契数的数字
- n = int(body)
- print " [.] fib(%s)" % (n,)
- #调用fib函数获得计算结果
- response = fib(n)
- #exchage为空字符串则将message发送个到routing_key指定的queue
- #这里queue为回调函数参数props中reply_ro指定的queue
- #要发送的message为计算所得的斐波那契数
- #properties中correlation_id指定为回调函数参数props中co的rrelation_id
- #最后对消息进行确认
- 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)
- #只有consumer已经处理并确认了上一条message时queue才分派新的message给它
- channel.basic_qos(prefetch_count=1)
- #设置consumeer参数,即从哪个queue获取消息使用哪个函数进行处理,是否对消息进行确认
- channel.basic_consume(on_request, queue='rpc_queue')
- print " [x] Awaiting RPC requests"
- #开始接收并处理消息
- channel.start_consuming()
RPC client:远程过程调用发起者:定义了一个类,类中初始化到RabbitMQ Server的连接、声明回调queue、开始在回调queue上等待接收响应、定义了在回调queue上接收到响应后的处理函数on_response根据响应关联的correlation_id属性作出响应、定义了调用函数并在其中向调用queue发送包含correlation_id等属性的调用请求、初始化一个client实例,以30为参数发起远程过程调用。
- #!/usr/bin/env python
- #encoding:utf8
- import pika
- import uuid
- #在一个类中封装了connection建立、queue声明、consumer配置、回调函数等
- class FibonacciRpcClient(object):
- def __init__(self):
- #建立到RabbitMQ Server的connection
- 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
- #此处client既是producer又是consumer,因此要配置consume参数
- #这里的指明从client自己创建的临时队列中接收消息
- #并使用on_response函数处理消息
- #不对消息进行确认
- self.channel.basic_consume(self.on_response, no_ack=True,
- queue=self.callback_queue)
- #定义回调函数
- #比较类的corr_id属性与props中corr_id属性的值
- #若相同则response属性为接收到的message
- def on_response(self, ch, method, props, body):
- if self.corr_id == props.correlation_id:
- self.response = body
- def call(self, n):
- #初始化response和corr_id属性
- self.response = None
- self.corr_id = str(uuid.uuid4())
- #使用默认exchange向server中定义的rpc_queue发送消息
- #在properties中指定replay_to属性和correlation_id属性用于告知远程server
- #correlation_id属性用于匹配request和response
- self.channel.basic_publish(exchange='',
- routing_key='rpc_queue',
- properties=pika.BasicProperties(
- reply_to = self.callback_queue,
- correlation_id = self.corr_id,
- ),
- #message需为字符串
- 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,)
测试:
- python rpc_server.py
- python rpc_client.py