上节中的消息队列是一对一,这节的消息队列是一对多。
在上一篇教程中,我们创建了工作队列。工作队列背后的假设是每个任务只能传递给一个工作人员。 在这一部分,我们将做一些完全不同的事情 - 我们会向多个消费者传递信息。这种模式被称为“发布/订阅”。
一、发布、订阅模式
在发布、订阅模式中,生产者采用广播、组播,多个消费者收取。
在发布、订阅模式中,每个消费者一个队列,生产者将消息提交给exchange交换机,谁订阅了消息,exchange就将消息转发到谁的队列中(使用系统随机分配的队列)。
在发布、订阅模式中,为什么要每个消费都要有一个自己的队列,为什么不共用一个队列?
因为,当有消费者消费此消息时,如果从队列中取出此消息,其它消息者就不能取得同一队列中的这条消息了;如果不取出,同时同一消费者将重复消费此消息。因此,每个订阅者一个队列,消费者消费了就从自己的队列中取出此消息,避免了相互影响。
exchange的广播类型(交换类型)exchange_type:
- fanout: 所有bind到此exchange的queue都可以接收消息,广播模式
- direct: 通过routing_key决定此exchange的那些匹配的queue可以接收消息,组播模式
- topic:所有匹配routing_key的queue可以接收消息,此时type可以是一个表达式
- headers: 通过headers 来决定把消息发给哪些queue,比较少用。
发布者在发布时,如果订阅者是在之后才订阅的,肯定是看不到之前的广播或组播的。广播或组播是实时的,就像现实中的广播和听众一样。
关于队列、exchange:
无论哪一种广播类型,发布者都无需要声明和指定队列,而是声明和指定exchange,同时指定广播类型。
无论哪一种广播类型,订阅者都需要使用系统随机分配的队列(可以确保唯一性,如果自定义的queue可能重名)来接收消息,并将队列绑定到exchange;同时声明和指定exchange,同时指定广播类型。
同上一节类似,发布者和订阅者,可以都声明exchange及其类型,也可以由先启动的一方来声明,后启动的一方可以不声明。
关于routing_key:
routing_key,即路由的标识符,可以为任意字符;也可以使用队列名作为标识符,但它本身的含义不是队列名。
不同类型的广播,区别在于exchange_type不同,使得不同类型的routing_key不同。
- fanout类型,发布者无需要指定routing_key。
- direct类型,发布者需要指定routing_key,订阅者使用相同的routing_key匹配,就像分组一样。
- topic类型,发布者需要指定routing_key,订阅者可使用带通配符的routing_key去匹配,是比分组更精细的匹配。
二、广播示例:交换机的类型exchange_type='fanout'
无需routing_key。订阅者将queue绑定在exchange之上。
发布者:
import pika
import sys
connection = pika.BlockingConnection(pika.ConnectionParameters(
host='localhost'))
channel = connection.channel()
# 声明交换机:exchange参数为交换机名称,type参数为广播类型
channel.exchange_declare(exchange='logs', exchange_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()
订阅者:
# _*_coding:utf-8_*_
import pika
def callback(ch, method, properties, body):
print(" [x] %r" % body)
connection = pika.BlockingConnection(pika.ConnectionParameters(
host='localhost'))
channel = connection.channel()
channel.exchange_declare(exchange='logs',
exchange_type='fanout')
# 不指定queue名字,使用排它参数exclusive=True随机分配一个队列名
# 此队列名,会在使用此queue的消费者断开后,自动将queue删除
result = channel.queue_declare(exclusive=True)
queue_name = result.method.queue
# 订阅:将消费者自己的queue绑定到交换机上
channel.queue_bind(exchange='logs',
queue=queue_name)
print(' [*] Waiting for logs. To exit press CTRL+C')
# 广播时:听众处理完消息后,不需要发送确认标志,无意义
channel.basic_consume(callback,
queue=queue_name,
no_ack=True)
channel.start_consuming()
三、组播示例:交换机的类型exchange_type='direct'
发布者与订阅者使用相同的关键字作为routing_key使用。订阅者将queue和routing_key绑定在exchange之上。
发布者:
import pika
import sys
connection = pika.BlockingConnection(pika.ConnectionParameters(
host='localhost'))
channel = connection.channel()
# 声明交换机:exchange参数为交换机名称,type参数为广播类型
channel.exchange_declare(exchange='direct_logs', exchange_type='direct')
# 广播级别
severity = sys.argv[1] if len(sys.argv) > 1 else "info"
message = ' '.join(sys.argv[1:]) or "info: Hello World!"
# 在交换机上发送消息,即广播
channel.basic_publish(exchange='direct_logs',
routing_key=severity, # 这里使用广播级别来作为队列名称
body=message)
print(" [x] Sent %r" % message)
connection.close()
订阅者:
# _*_coding:utf-8_*_
import pika
import sys
def callback(ch, method, properties, body):
print(" [x] %r" % body)
connection = pika.BlockingConnection(pika.ConnectionParameters(
host='localhost'))
channel = connection.channel()
channel.exchange_declare(exchange='direct_logs',
exchange_type='direct')
# 不指定queue名字,使用排它参数exclusive=True随机分配一个队列名
# 此队列名,会在使用此queue的消费者断开后,自动将queue删除
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:
# 订阅:将消费者自己的queue绑定到交换机上,同时绑定广播级别
channel.queue_bind(exchange='direct_logs',
queue=queue_name,
routing_key=severity)
print(' [*] Waiting for logs. To exit press CTRL+C')
channel.basic_consume(callback,
queue=queue_name,
no_ack=True)
channel.start_consuming()
假设称routing_key为路由键或路由密钥,一个队列怎么绑定多个路由键?
示例:
result = channel.queue_declare(exclusive=True)
queue_name = result.method.queue
severities = ["info", "debug", "warning", "error"]
for severity in severities:
channel.queue_bind(exchange='direct_logs',
queue=queue_name,
routing_key=severity)
四、更细致的消息过滤:exchange_type='topic',可以使用通配符
订阅者可以使用通配符去匹配发布者的routing_key。订阅者将queue和routing_key绑定在exchange之上。
发送到topic交换的消息必须有规范的routing_key - 它必须是由点分隔的单词列表。单词可以是任何东西,但通常它们指定了与该消息相关的一些功能。 一些有效的routing_key例子: "stock.usd.nyse","nyse.vmw","quick.orange.rabbit"。只要您愿意,路由键中可以有任意的单词,但最多255个字节。
绑定键也必须是相同的形式。topic交换背后的逻辑与direct topic交换背后的逻辑类似 - 使用特定路由键发送的消息将被传递到与匹配绑定键绑定的所有队列。 但是绑定键有两个重要的特殊情况:
- * (star) 可以代替一个字。
- # (hash) 可以替代零个或多个单词。
订阅者使用通配符示例:
# "#"匹配某交换机中的所有routing_key
python receive_logs_topic.py "#"
# "kern.*"匹配某交换机中所有kern.开头的routing_key
python receive_logs_topic.py "kern.*"#
# "*.critical"匹配某交换机中以.critical结尾的routing_key
python receive_logs_topic.py "*.critical"
# 匹配其中之一
python emit_log_topic.py "kern.critical" "A critical kernel error"
发布者:
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) > 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()
订阅者:
import pika
import sys
def callback(ch, method, properties, body):
print(" [x] %r:%r" % (method.routing_key, body))
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')
channel.basic_consume(callback,
queue=queue_name,
no_ack=True)
channel.start_consuming()