Loading

MQ - RabbitMQ基础篇

About MQ

参见:https://www.cnblogs.com/Neeo/articles/13914727.html

About RabbitMQ

RabbitMQ 中文文档

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

AMQP :Advanced Message Queue,高级消息队列协议。它是应用层协议的一个开放标准,为面向消息的中间件设计,基于此协议的客户端与消息中间件可传递消息,并不受产品、开发语言等条件的限制。RabbitMQ 最初起源于金融系统,用于在分布式系统中存储转发消息,在易用性、扩展性、高可用性等方面表现不俗。具体特点括:

  1. 可靠性(Reliability):RabbitMQ 使用一些机制来保证可靠性,如持久化、传输确认、发布确认。
  2. 灵活的路由(Flexible Routing):在消息进入队列之前,通过 Exchange 来路由消息的。对于典型的路由功能,RabbitMQ已经提供了一些内置的 Exchange 来实现。针对更复杂的路由功能,可以将多个Exchange 绑定在一起,也通过插件机制实现自己的 Exchange 。
  3. 消息集群(Clustering):多个 RabbitMQ 服务器可以组成一个集群,形成一个逻辑 Broker 。
  4. 高可用(Highly Available Queues):队列可以在集群中的机器上进行镜像,使得在部分节点出问题的情况下队列仍然可用。
  5. 多种协议(Multi-protocol):RabbitMQ 支持多种消息队列协议,比如 STOMP、MQTT 等等。
  6. 多语言客户端(Many Clients):RabbitMQ 几乎支持所有常用语言,比如 Java、.NET、Ruby 等等。
  7. 管理界面(Management UI):RabbitMQ 提供了一个易用的用户界面,使得用户可以监控和管理消息 Broker 的许多方面。
  8. 跟踪机制(Tracing):如果消息异常,RabbitMQ 提供了消息跟踪机制,使用者可以找出发生了什么。
  9. 插件机制(Plugin System):RabbitMQ 提供了许多插件,来从多方面进行扩展,也可以编写自己的插件。

RabbitMQ架构及主要概念

RabbitMQ架构图:

主要概念:

  • RabbitMQ Server: 也叫Broker Server,它是一种传输服务。 它的角色就是维护一条从Producer到Consumer的路线,保证数据能够按照指定的方式进行传输。
  • Producer: 消息生产者程序,消息生产者连接RabbitMQ服务器然后将消息投递到Exchange。
  • Consumer:消息消费者程序,消息消费者订阅队列,RabbitMQ将Queue中的消息发送到消息消费者。
  • Exchange:生产者将消息发送到Exchange(交换器),由Exchange将消息路由到一个或多个Queue中(或者丢弃)。Exchange并不存储消息。RabbitMQ中的Exchange有direct、fanout、topic、headers四种类型,每种类型对应不同的路由规则。
  • Queue:(队列)是RabbitMQ的内部对象,用于存储消息。消息消费者就是通过订阅队列来获取消息的,RabbitMQ中的消息都只能存储在Queue中,生产者生产消息并最终投递到Queue中,消费者可以从Queue中获取消息并消费。多个消费者可以订阅同一个Queue,这时Queue中的消息会被平均分摊给多个消费者进行处理,而不是每个消费者都收到所有的消息并处理。
  • RoutingKey:生产者在将消息发送给Exchange的时候,一般会指定一个routing key,来指定这个消息的路由规则,而这个routing key需要与Exchange Type及binding key联合使用才能最终生效。在Exchange Type与binding key固定的情况下(在正常使用时一般这些内容都是固定配置好的),我们的生产者就可以在发送消息给Exchange时,通过指定routing key来决定消息流向哪里。RabbitMQ为routing key设定的长度限制为255bytes。
  • Connection(连接):Producer和Consumer都是通过TCP连接到RabbitMQ Server的。
  • Channels: (信道):它建立在上述的TCP连接中。数据流动都是在Channel中进行的。也就是说,一般情况是程序起始建立TCP连接,第二步就是建立这个Channel。
  • VirtualHost:权限控制的基本单位,一个VirtualHost里面有若干Exchange和MessageQueue,以及指定被哪些User使用。

另外,从上面的架构图可以看出消息生产者并没有直接将消息发送给消息队列,而是通过建立与Exchange的Channel,将消息发送给Exchange,Exchange根据规则,将消息转发给指定的消息队列。消费者通过建立与消息队列相连的Channel,从消息队列中获取消息。

这里谈到的Channel可以理解为建立在生产者/消费者和RabbitMQ服务器之间的TCP连接上的虚拟连接,一个TCP连接上可以建立多个Channel。RabbitMQ服务器的Exchange对象可以理解为生产者发送消息的容器。Exchange对象根据它定义的规则和消息包含的Routing Key以及header信息将消息转发到消息队列。

根据转发消息的规则不同,RabbitMQ服务器中使用的Exchange对象有四种:Direct Exchange、Fanout Exchange、Topic Exchange、 Header Exchange。如果定义Exchange时没有指定类型和名称,,RabbitMQ将会为每个消息队列设定一个Default Exchange,它的Routing Key是消息队列名称。

RabbitMQ 安装与连接

下载安装

for docker

  1. 拉取镜像:
docker pull rabbitmq:management
# 注意,如果docker pull rabbitmq 后面不带management,启动rabbitmq后是无法打开管理界面的,所以我们要下载带management插件的rabbitmq
  1. 启动镜像:
docker run \
-d \
--name rabbitmq \
--hostname myrabbitmq \
--restart=always \
-e RABBITMQ_DEFAULT_USER=guest \
-e RABBITMQ_DEFAULT_PASS=12346 \
-v /data/rabbitmq_data:/var/lib/rabbitmq \
-p 5672:5672 \
-p 15672:15672 \
rabbitmq:management

其中:

  • -d:后台运行容器。
  • --name rabbitmq:容器名。
  • --hostname myrabbitmq:主机名,RabbitMQ的一个重要注意事项是它根据所谓的 "节点名称"存储数据,默认为主机名。
  • --restart=always,表示将容器跟随docker服务重启而重启。
  • -e:指定环境变量:
    • RABBITMQ_DEFAULT_USER:默认用户名。
    • RABBITMQ_DEFAULT_PASS:默认密码。
  • -v:挂在本地目录。
  • -p:指定映射端口:
    • 5672:5672:应用访问端口。
    • 15672:15672:web访问端口。

连接

你如果下载的是带web插件版的,可以通过ip:15672访问web页面:

也可以通过接口与其他语言进行通信,我这里及后续都通过Python与RabbitMQ通信,所以这里需要下载通信模块:

pip install pika==1.1.0

测试:

import pika

credentials = pika.PlainCredentials('guest', '12346')  # mq用户名和密码
connection = pika.BlockingConnection(
    pika.ConnectionParameters(
        host='192.168.10.91',
        port=5672,
        virtual_host='/',
        credentials=credentials)
)

WEB管理界面

我这里使用的是docker安装的RabbitMQ的management版本,该版本自带web管理界面。

当你启动后,就可以通过ip:15672来访问,用户名和密码在docker run的时候指定了(其他安装方式的默认用户密码都是guest):

其中:

  • Overview:综合面板,查看各项参数。
  • Connections:管理连接信息。
  • Channels:管理通道信息。
  • Exchanges:管理交换机信息。
  • Queues:管理队列信息。
  • Admin:管理登录用户信息。

其中以Exchanges和Queues这两部分用的最多,怎么使用嘛,就是随着对RabbitMQ了解的深入,慢慢也就摸清一些用法。

简单示例

下面的示例展示了RabbitMQ的基本使用:

producer
import pika

# 连接 RabbitMQ Server
credentials = pika.PlainCredentials('guest', '12346')  # RabbitMQ 用户名和密码
connection = pika.BlockingConnection(
    pika.ConnectionParameters(
        host='192.168.10.91',
        port=5672,
        virtual_host='/',
        credentials=credentials)
)

# 生产者通过 channel 对象与 RabbitMQ Server 打交道
channel = connection.channel()

# 声明一个队列,如果该队列不存在就创建
channel.queue_declare(queue='news1')

# 声明并配置交换机规则并路由到指定的队列,然后投递消息
channel.basic_publish(
    exchange='',  # 当 exchange 值为空时,使用默认的交换机模式
    routing_key='news1',   # 指定路由队列
    body='今日油条涨价......'   # 投递消息
)


print("[x] Sent '今日油条涨价......'")  # 测试使用,与主体逻辑无关
consumer
import pika

# 连接 RabbitMQ Server
credentials = pika.PlainCredentials('guest', '12346')  # RabbitMQ 用户名和密码
connection = pika.BlockingConnection(
    pika.ConnectionParameters(
        host='192.168.10.91',
        port=5672,
        virtual_host='/',
        credentials=credentials)
)

# 消费者通过 channel 对象与 RabbitMQ Server 打交道
channel = connection.channel()

# 声明一个队列,如果该队列不存在就创建
channel.queue_declare(queue='news1')


# 回调函数
def callback(ch, method, properties, body):
    """
    回调函数执行时,表示消费者已经从队列中拿到了消息,可以实现具体的逻辑
    :param ch:
    :param method:
    :param properties:
    :param body: bytes类型的消息
    :return: None
    """

    print(" [x] Received %r" % body.decode())


# 配置监听队列的相关参数
channel.basic_consume(
    queue='news1',  # 监听指定队列
    auto_ack=True,  # auto_ack:True 表示当消费者从队列中取出消息后,缓存中就不再保存该消息了
    on_message_callback=callback  # 当监听的队列有消息时调用回调函数执行具体逻辑
)

# 开始监听
channel.start_consuming()

在声明队列时,还有其他的参数需要我们了解:

  • queue:"",队列名称。
  • passive:False:只检查队列是否存在。
  • durable:False:队列是否持久化。如果是False,队列在内存中,服务器挂掉后,队列就没了;如果是True,服务器重启后,队列将会重新生成。注意,只是队列持久化,不代表队列中的消息持久化,也就是那些消息需要持久化,需要说明。
  • exclusive:False:队列是否专属,专属的范围针对的是连接,也就是说,一个连接下面的多个信道是可见的,对于其他连接是不可见的,连接断开后,该队列会被删除。注意,不是信道断开,是连接断开,并且,就算设置成了持久化,也会删除。
  • auto_delete:False:是否自动删除,当最后一个消费者断开连接之后队列是否自动被删除(就算队列中还有消息,但所有消费者都断开后也删除)。
  • arguments:None:队列的配置
    • Message TTL(x-message-ttl):设置队列中的所有消息的生存周期(统一为整个队列的所有消息设置生命周期),也可以在发布消息的时候单独为某个消息指定剩余生存时间,单位毫秒,生存时间到了,消息会被从队里中删除,注意是消息被删除,而不是队列被删除。
    • Auto Expire(x-expires): 当队列在指定的时间没有被访问(consume, basicGet, queueDeclare…)就会被删除,Features=Exp。
    • Max Length(x-max-length): 限定队列的消息的最大值长度,超过指定长度将会把最早的几条删除掉, 类似于mongodb中的固定集合,例如保存最新的100条消息,Feature=Lim。
    • Max Length Bytes(x-max-length-bytes): 限定队列最大占用的空间大小, 一般受限于内存、磁盘的大小, Features=Lim B。
    • Dead letter exchange(x-dead-letter-exchange): 当队列消息长度大于最大长度、或者过期的等,将从队列中删除的消息推送到指定的交换机中去而不是丢弃掉,Features=DLX。
    • Dead letter routing key(x-dead-letter-routing-key):将删除的消息推送到指定交换机的指定路由键的队列中去,Feature=DLK。
    • Maximum priority(x-max-priority):优先级队列,声明队列时先定义最大优先级值(定义最大值一般不要太大),在发布消息的时候指定该消息的优先级, 优先级更高(数值更大的)的消息先被消费。
    • Lazy mode(x-queue-mode=lazy): Lazy Queues: 先将消息保存到磁盘上,不放在内存中,当消费者开始消费的时候才加载到内存中。
    • Master locator(x-queue-master-locator):将队列设置为主位置模式,确定在节点集群上声明队列主位置时所使用的规则。

基本操作

那些基操勿六的操作:

import pika
import requests
from requests.auth import HTTPBasicAuth

# 连接 RabbitMQ Server
credentials = pika.PlainCredentials('guest', '12346')  # RabbitMQ 用户名和密码
connection = pika.BlockingConnection(
    pika.ConnectionParameters(
        host='192.168.10.91',
        port=5672,
        virtual_host='/',
        credentials=credentials)
)

channel = connection.channel()
# channel.queue_delete("news2")  # 删除指定队列
# channel.exchange_delete("logs5")  # 删除指定交换机
# channel.channel_number  # 返回 channel 对象数量
# channel.queue_purge(queue='news')  # 从指定队列中清除所有消息
# connection.close()  # 关闭连接

也可以在通过WEB管理插件提供的API来获取相关信息:

上图中的HTTP API,展示了所有支持的HTTP API接口,我们可以通过访问这些API来查看你的RabbitMQ Server的信息。

我这里演示如何用Python的requests模块来通过HTTP API获取数据:

import requests
from requests.auth import HTTPBasicAuth

# ------------ 获取 exchange 列表 ------------

response = requests.get(
    url="http://192.168.10.91:15672/api/exchanges",
    auth=HTTPBasicAuth(username='guest', password='12346'),
    headers={"content-type": "application/json"}
)

print(response.json())
for exchange in response.json():
    print(exchange['name'], exchange['type'])

"""
amq.direct direct
amq.fanout fanout
amq.headers headers
amq.match headers
amq.rabbitmq.trace topic
amq.topic topic
"""

# ------------ 获取 queue 列表 ------------

response = requests.get(
    url="http://192.168.10.91:15672/api/queues",
    auth=HTTPBasicAuth(username='guest', password='12346'),
    headers={"content-type": "application/json"}
)

for exchange in response.json():
    print(exchange['name'], exchange['type'])

"""
amq.gen-6duQxZqlML6mWbGtigoU_A classic
amq.gen-HBe6122n-5epq8_1YUZNXQ classic
amq.gen-T4wCwpWyolgUErmAShHGew classic
amq.gen-cZOTYFS-5a9DVOV8dHW5SQ classic
"""

不会requests?疯狂点击:https://www.cnblogs.com/Neeo/articles/11511087.html

消息确认

针对于RabbitMQ Server中的队列中的消息来说,当消费者从队列中取走消息后,有两种情况:

  • 自动确认(auto_ack=True):当消费者从队列中取走消息后,RabbitMQ Server的缓存中直接删除该消息。
  • 手动确认(auto_ack=False):当消费者从队列中取走消息后,该消息被缓存在RabbitMQ Server的缓存中,直到得到消费者手动确认后才删除。

根据示例进一步展开来说。

自动确认

producer端代码不变。

producer
import pika

# 连接 RabbitMQ Server
credentials = pika.PlainCredentials('guest', '12346')  # mq用户名和密码
connection = pika.BlockingConnection(
    pika.ConnectionParameters(
        host='192.168.10.91',
        port=5672,
        virtual_host='/',
        credentials=credentials)
)

# 生产者通过 channel 对象与 RabbitMQ Server 打交道
channel = connection.channel()

# 声明一个队列,如果该队列不存在就创建
channel.queue_declare(queue='news1')

# 配置交换机规则并路由到指定的队列,然后投递消息
channel.basic_publish(
    exchange='',  # 当 exchange 值为空时,使用默认的交换机模式
    routing_key='news1',   # 指定路由队列
    body='今日油条涨价......'   # 投递消息
)


print("[x] Sent '今日油条涨价......'")  # 测试使用,与主体逻辑无关

consumer这里通过在配置监听队列的参数时指定:

channel.basic_consume(
    queue='news1',  # 监听指定队列
    auto_ack=True,  # auto_ack:True RabbitMQ Server 缓存不会缓存该消息
    on_message_callback=callback  # 当监听的队列有消息时调用回调函数执行具体逻辑
)
consumer
import pika

# 连接 RabbitMQ Server
credentials = pika.PlainCredentials('guest', '12346')  # RabbitMQ 用户名和密码
connection = pika.BlockingConnection(
    pika.ConnectionParameters(
        host='192.168.10.91',
        port=5672,
        virtual_host='/',
        credentials=credentials)
)

# 消费者通过 channel 对象与 RabbitMQ Server 打交道
channel = connection.channel()

# 声明一个队列,如果该队列不存在就创建
channel.queue_declare(queue='news1')


# 回调函数
def callback(ch, method, properties, body):
    """
    回调函数执行时,表示消费者已经从队列中拿到了消息,可以实现具体的逻辑
    :param ch:
    :param method:
    :param properties:
    :param body: bytes类型的消息
    :return:
    """

    print(" [x] Received %r" % body.decode())


# 配置监听队列的相关参数
channel.basic_consume(
    queue='news1',  # 监听指定队列
    auto_ack=True,  # auto_ack:True RabbitMQ Server 缓存不会缓存该消息
    on_message_callback=callback  # 当监听的队列有消息时调用回调函数执行具体逻辑
)

# 开始监听
channel.start_consuming()

很明显,自动确认存在问题,当消费者也就是上例中的callback回调函数内执行逻辑失败,而RabbitMQ Server 缓存中有没有缓存该消息,那这个消息就......丢了......你想想淘宝的订单在某个环节失败从而重新走订单流程时,发现该订单没了....

手动确认

producer这里还不变。

producer
import pika

# 连接 RabbitMQ Server
credentials = pika.PlainCredentials('guest', '12346')  # RabbitMQ 用户名和密码
connection = pika.BlockingConnection(
    pika.ConnectionParameters(
        host='192.168.10.91',
        port=5672,
        virtual_host='/',
        credentials=credentials)
)

# 生产者通过 channel 对象与 RabbitMQ Server 打交道
channel = connection.channel()

# 声明一个队列,如果该队列不存在就创建
channel.queue_declare(queue='news1')

# 配置交换机规则并路由到指定的队列,然后投递消息
channel.basic_publish(
    exchange='',  # 当 exchange 值为空时,使用默认的交换机模式
    routing_key='news1',   # 指定路由队列
    body='今日油条涨价......'   # 投递消息
)


print("[x] Sent '今日油条涨价......'")  # 测试使用,与主体逻辑无关

变化在consumer这里,通过在配置监听队列的参数和回调函数时确认消息是否删除:

"""
重点代码:
auto_ack=False
ch.basic_ack(delivery_tag=method.delivery_tag)
"""

# 回调函数
def callback(ch, method, properties, body):
    """
    回调函数执行时,表示消费者已经从队列中拿到了消息,可以实现具体的逻辑
    :param ch:
    :param method:
    :param properties:
    :param body: bytes类型的消息
    :return:
    """

    print(" [x] Received %r" % body.decode())
    '''
    当代码执行到 ch.basic_ack(delivery_tag=method.delivery_tag) 的时候,
    表示消费者这边所有逻辑执行成功了,可以告诉 RabbitMQ Server 可以从缓存中删除该消息了
    '''
    ch.basic_ack(delivery_tag=method.delivery_tag)
# 配置监听队列的相关参数
channel.basic_consume(
    queue='news1',  # 监听指定队列
    auto_ack=False,  # auto_ack:False RabbitMQ Server 缓存会缓存该消息
    on_message_callback=callback  # 当监听的队列有消息时调用回调函数执行具体逻辑
)
consumer
import pika

# 连接 RabbitMQ Server
credentials = pika.PlainCredentials('guest', '12346')  # RabbitMQ 用户名和密码
connection = pika.BlockingConnection(
    pika.ConnectionParameters(
        host='192.168.10.91',
        port=5672,
        virtual_host='/',
        credentials=credentials)
)

# 消费者通过 channel 对象与 RabbitMQ Server 打交道
channel = connection.channel()

# 声明一个队列,如果该队列不存在就创建
channel.queue_declare(queue='news1')


# 回调函数
def callback(ch, method, properties, body):
    """
    回调函数执行时,表示消费者已经从队列中拿到了消息,可以实现具体的逻辑
    :param ch:
    :param method:
    :param properties:
    :param body: bytes类型的消息
    :return:
    """

    print(" [x] Received %r" % body.decode())
    '''
    当代码执行到 ch.basic_ack(delivery_tag=method.delivery_tag) 的时候,
    表示消费者这边所有逻辑执行成功了,可以告诉 RabbitMQ Server 可以从缓存中删除该消息了
    '''
    ch.basic_ack(delivery_tag=method.delivery_tag)

# 配置监听队列的相关参数
channel.basic_consume(
    queue='news1',  # 监听指定队列
    auto_ack=False,  # auto_ack:False RabbitMQ Server 缓存会缓存该消息
    on_message_callback=callback  # 当监听的队列有消息时调用回调函数执行具体逻辑
)

# 开始监听
channel.start_consuming()

这下就完美了......

消息持久化

为了防止RabbitMQ Server自身崩溃或其他异常情况,而导致消息丢失,我们要进行相关配置,进而让消息持久化。

配置有两步:

"""
在最开始声明队列的时候,就要声明为支持持久化的队列,
且这个动作要在producer和consumer两端都要配置,因为不知道谁先运行
"""
# 声明一个支持持久化的队列,如果该队列不存在就创建
channel.queue_declare(queue='news2', durable=True)  # durable=True 表示支持持久化

"""
虽然队列是持久化队列,但插入的数据是否要持久化,是可选的,如果要对某个消息进行持久化,那就么加如下参数
properties=pika.BasicProperties(
	delivery_mode=2,  # make message persistent
)
加在哪?肯定是send的时候喽
"""
# 配置交换机规则并路由到指定的队列,然后投递消息
channel.basic_publish(
    exchange='',  # 当 exchange 值为空时,使用默认的交换机模式
    routing_key='news2',  # 指定路由队列
    body='今日面包涨价......',  # 投递消息
    properties=pika.BasicProperties(
        delivery_mode=2,  # make message persistent  让消息持久化
    )
)

delivery_mode=2 # 支持持久化
delivery_mode=1 # 不支持持久化
properties=None  # 不支持持久化

来个示例,在producer代码中,声明一个支持持久化的队列,然后分别send到队列中5个消息(通过参数设置3个持久化,2个不持久化)。

producer
import pika

# 连接 RabbitMQ Server
credentials = pika.PlainCredentials('guest', '12346')  # RabbitMQ 用户名和密码
connection = pika.BlockingConnection(
    pika.ConnectionParameters(
        host='192.168.10.91',
        port=5672,
        virtual_host='/',
        credentials=credentials)
)

# 生产者通过 channel 对象与 RabbitMQ Server 打交道
channel = connection.channel()

# 声明一个支持持久化的队列,如果该队列不存在就创建
channel.queue_declare(queue='news2', durable=True)  # durable=True 表示支持持久化

# 配置交换机规则并路由到指定的队列,然后投递消息
msg_list = [
    ('今日面包涨价......', True),
    ('今日牛奶涨价......', True),
    ('今日咖啡涨价......', False),
    ('今日豆浆涨价......', True),
    ('今日油条涨价......', False),
]
for item in msg_list:   # 如果item[1]为True,表示持久化该消息,否则不持久化该消息
    channel.basic_publish(
        exchange='',  # 当 exchange 值为空时,使用默认的交换机模式
        routing_key='news2',  # 指定路由队列
        body=item[0],  # 投递消息
        properties=pika.BasicProperties(
            delivery_mode=2,  # make message persistent  让消息持久化
        ) if item[1] else None
    )

print("[x] Sent '都发送完了......'")  # 测试使用,与主体逻辑无关

你可以在重启RabbitMQ Server之前通过web查看该队列的message total:

然后重启dockers中的RabbitMQ Server容器,再通过web观察该队列的message total:

还剩3个了,符合预期,这3个消息也可以通过consumer取出来:

consumer
import pika

# 连接 RabbitMQ Server
credentials = pika.PlainCredentials('guest', '12346')  # RabbitMQ 用户名和密码
connection = pika.BlockingConnection(
    pika.ConnectionParameters(
        host='192.168.10.91',
        port=5672,
        virtual_host='/',
        credentials=credentials)
)

# 消费者通过 channel 对象与 RabbitMQ Server 打交道
channel = connection.channel()

# 声明一个支持持久化的队列,如果该队列不存在就创建
channel.queue_declare(queue='news2', durable=True)  # durable=True 表示支持持久化


# 回调函数
def callback(ch, method, properties, body):
    """
    回调函数执行时,表示消费者已经从队列中拿到了消息,可以实现具体的逻辑
    :param ch:
    :param method:
    :param properties:
    :param body: bytes类型的消息
    :return:
    """
    print(" [x] Received %r" % body.decode())
    '''
    当代码执行到 ch.basic_ack(delivery_tag=method.delivery_tag) 的时候,
    表示消费者这边所有逻辑执行成功了,可以告诉 RabbitMQ Server 可以从队列中删除这个消息了
    '''
    ch.basic_ack(delivery_tag=method.delivery_tag)


# 配置监听队列的相关参数
channel.basic_consume(
    queue='news2',  # 监听指定队列
    auto_ack=False,  # auto_ack:False 表示当消费者从队列中取出消息后,需要手动确认后才从队列中删除该消息
    on_message_callback=callback  # 当监听的队列有消息时调用回调函数执行具体逻辑
)

# 开始监听
channel.start_consuming()

这里还有要特别注意的点:

  • 队列的名字是唯一的。
  • 队列创建时候指定的标志durable=True的唯一含义就是具有这个标志的队列会在重启之后重新建立,它不表示说在队列中的消息会在重启后恢复 ,所以,我们在send消息的时候也通过properties参数声明哪些消息需要持久化。
  • 一旦创建了队列,就不能修改其标志了,例如,创建了一个non-durable的队列,然后想把它改变成durable的,唯一的办法就是删除这个队列然后重新创建。
  • exclusive参数设置队列为排他队列,true为排他。如果一个队列被声明为排他队列,该队列仅对首次声明他它的连接可见,并在连接断开时自动删除(即一个队列只能有一个消费者)。

消息分发机制

当有多个消费者时,RabbitMQ有两种消息分发机制,来并发处理。

循环分发(Round-robin dispathching)

默认状态下,RabbitMQ将第n条message分发给第n个消费者,n是取余后的,它不管消费者是否还有未确认的消息(unacked message)。举个例子,有a、b、c、d、e、f这六条消息和两个消费者,那么RabbitMQ Server将会分别将a、c、eb、d、f发送给两个消费者。

如果你仔细寻思一下,这个循环分发机制不太优雅,因为它没有考虑到每个消费者的情况,如上面的两个消费者,其中一个消费者的能力不行(性能问题,负载较重等),处理三个消息用了半个小时;而另一个消费者能力强,处理三个消息之花了1分钟。这就可能导致一个后果,一个消费者毫无休息的机会,另一个消费者基本无事可做。

公平分发(Fair dispathching)

为了解决循环分发的问题,可以在消费者端代码中通过设置channel.basic_qos(prefetch_count=1)参数,然后RabbitMQ就会使每个消费者在同一个时间节点最多处理一个message,换句话说,再接受到该消费者的ack之前,RabbitMQ不会将新的message分发给该消费者。也就实现了能者多劳的情况。我们称这种消息分发机制为——公平分发(Fair dispathching)。

注意,公平分发可能会导致一个问题,那就是队列容易满,也就是活多人又菜,处理不过来,导致任务积压......这种情况下,你就要考虑提高消费者的能力或者添加更多的消费者来处理任务,或者细化你的逻辑设计避免这种问题。

循环分发(Round-robin dispathching)

通过time.sleep模拟两个不同能力的消费者,来观察循环分发机制的分发过程:

consumer
import time
import pika

# 连接 RabbitMQ Server
credentials = pika.PlainCredentials('guest', '12346')  # RabbitMQ 用户名和密码
connection = pika.BlockingConnection(
    pika.ConnectionParameters(
        host='192.168.10.91',
        port=5672,
        virtual_host='/',
        credentials=credentials)
)

# 消费者通过 channel 对象与 RabbitMQ Server 打交道
channel = connection.channel()

# 声明一个队列,如果该队列不存在就创建
channel.queue_declare(queue='news3')


# 回调函数
def callback(ch, method, properties, body):
    """
    回调函数执行时,表示消费者已经从队列中拿到了消息,可以实现具体的逻辑
    :param ch:
    :param method:
    :param properties:
    :param body: bytes类型的消息
    :return:
    """
    # time.sleep(5)
    time.sleep(1)
    print(" [x] Received %r" % body.decode())
	'''
    当代码执行到 ch.basic_ack(delivery_tag=method.delivery_tag) 的时候,
    表示消费者这边所有逻辑执行成功了,可以告诉 RabbitMQ Server 可以从队列中删除这个消息了
    '''
    ch.basic_ack(delivery_tag=method.delivery_tag)

# 配置监听队列的相关参数
channel.basic_consume(
    queue='news3',  # 监听指定队列
    auto_ack=False,  # auto_ack:False 表示当消费者从队列中取出消息后,需要手动确认后才从队列中删除该消息
    on_message_callback=callback  # 当监听的队列有消息时调用回调函数执行具体逻辑
)

# 开始监听
channel.start_consuming()
而生产者这边直接生产消息就完了:
producer
import pika

# 连接 RabbitMQ Server
credentials = pika.PlainCredentials('guest', '12346')  # mq用户名和密码
connection = pika.BlockingConnection(
    pika.ConnectionParameters(
        host='192.168.10.91',
        port=5672,
        virtual_host='/',
        credentials=credentials)
)

# 生产者通过 channel 对象与 RabbitMQ Server 打交道
channel = connection.channel()

# 声明一个队列,如果该队列不存在就创建
channel.queue_declare(queue='news3')

# 配置交换机规则并路由到指定的队列,然后投递消息
for item in list('abcdef'):
    channel.basic_publish(
        exchange='',  # 当 exchange 值为空时,使用默认的交换机模式
        routing_key='news3',   # 指定路由队列
        body=item   # 投递消息
    )


print("[x] Sent '发送完了'")  # 测试使用,与主体逻辑无关
现象如下:

公平分发(Fair dispathching)

producer端只负责生产消息,代码不变:

producer
import pika

# 连接 RabbitMQ Server
credentials = pika.PlainCredentials('guest', '12346')  # mq用户名和密码
connection = pika.BlockingConnection(
    pika.ConnectionParameters(
        host='192.168.10.91',
        port=5672,
        virtual_host='/',
        credentials=credentials)
)

# 生产者通过 channel 对象与 RabbitMQ Server 打交道
channel = connection.channel()

# 声明一个队列,如果该队列不存在就创建
channel.queue_declare(queue='news4')

# 配置交换机规则并路由到指定的队列,然后投递消息
for item in list('abcdef'):
    channel.basic_publish(
        exchange='',  # 当 exchange 值为空时,使用默认的交换机模式
        routing_key='news4',   # 指定路由队列
        body=item   # 投递消息
    )

print("[x] Sent '发送完了'")  # 测试使用,与主体逻辑无关

consumer这里要通过如下参数来设置公平分发机制:

channel.basic_qos(prefetch_count=1)
consumer
import time
import pika

# 连接 RabbitMQ Server
credentials = pika.PlainCredentials('guest', '12346')  # RabbitMQ 用户名和密码
connection = pika.BlockingConnection(
    pika.ConnectionParameters(
        host='192.168.10.91',
        port=5672,
        virtual_host='/',
        credentials=credentials)
)

# 消费者通过 channel 对象与 RabbitMQ Server 打交道
channel = connection.channel()

# 声明一个队列,如果该队列不存在就创建
channel.queue_declare(queue='news4')


# 回调函数
def callback(ch, method, properties, body):
    """
    回调函数执行时,表示消费者已经从队列中拿到了消息,可以实现具体的逻辑
    :param ch:
    :param method:
    :param properties:
    :param body: bytes类型的消息
    :return:
    """
    # time.sleep(5)
    time.sleep(1)
    print(" [x] Received %r" % body.decode())
    '''
    当代码执行到 ch.basic_ack(delivery_tag=method.delivery_tag) 的时候,
    表示消费者这边所有逻辑执行成功了,可以告诉 RabbitMQ Server 可以从队列中删除这个消息了
    '''
    ch.basic_ack(delivery_tag=method.delivery_tag)

# 设置公平分发机制
channel.basic_qos(prefetch_count=1)

# 配置监听队列的相关参数
channel.basic_consume(
    queue='news4',  # 监听指定队列
    auto_ack=False,  # auto_ack:False 表示当消费者从队列中取出消息后,需要手动确认后才从队列中删除该消息
    on_message_callback=callback  # 当监听的队列有消息时调用回调函数执行具体逻辑
)

# 开始监听
channel.start_consuming()

效果:

交换机工作模式

是时候将目光转移到exchange上了,我们一般称之为消息交换机或者交换器都可以。

再次拿出RabbitMQ Server的架构图,我们可以看到,生产者生产的消息没有直接给到队列,而是先给了消息交换机,然后根据规则路由到指定的队列上,再由消费者来消费。

本节我们就要通过研究Exchange的几种工作模式搭配不同的路由规则实现不同的消息分发机制。

在配置交换机规则channel.basic_publish(exchange=''),根据指定的exchange参数值不同,有以下几种工作模式:

  • exchange='direct',关键字模式(Direct Exchange):直接匹配,通过指定exchange名称和routingkey来收发消息。
  • exchange='fanout',发布订阅模式(Fanout Exchange):该交换机会将消息发送到所有绑定自己的队列上去,就像打开了app的消息推送提醒似的,app接到消息,就会给你推送,无论是否包含你感兴趣的消息.....
  • exchange='topic',主题模式(Topic Exchange):很明显,发布订阅模式虽然用的较多,但是不够优雅!那么在繁杂的消息中,如何只推送我感兴趣的呢?这就用到了主题模式了,你可以为routingkey设置匹配规则,符合规则的才发给我,这多完美......
  • exchange='',默认模式(Default Exchange):如果用空字符串去声明一个exchange,那么RabbitMQ Server会使用 amq.direct这个exchange。我们在创建队列时,默认的都会有一个和新建队列同名的routingkey绑定到这个默认的exchange上去。
  • 消息头订阅模式(Headers Exchange):在消息发布前,为消息定义一个或多个key:value的消息头,而消费者接受消息的时候,也要定义类似的请求头,只有请求头与消息头匹配才能收取消息,此过程忽略routingkey,有种地下党根据暗号换取情报一样......bug,该模式性能差,也很少在生产中使用,所以我们有所了解即可。

理解Exchange

Exchange 收到消息时,它是如何知道需要发送至哪些 Queue 呢?这里就需要了解 Binding 和 RoutingKey 的概念:

Binding 表示 Exchange 与 Queue 之间的关系,我们也可以简单的认为队列对该交换机上的消息感兴趣,绑定可以附带一个额外的参数 RoutingKey。Exchange 就是根据这个 RoutingKey 和当前 Exchange 所有绑定的 Binding 做匹配,如果满足匹配,就往 Exchange 所绑定的 Queue 发送消息,这样就解决了我们向 RabbitMQ 发送一次消息,可以分发到不同的 Queue。RoutingKey 的意义依赖于交换机的类型。

发布订阅模式(Fanout Exchange)

Fanout Exchange会忽略RoutingKey的设置,直接将message广播到所有绑定它的队列中。

producer
"""
producer这里要干三件事情:
1. 连接rabbitmq
2. 声明交换机,如果 consumer 先声明了,那这里什么都不做
3. 向交换机send message
"""

import pika

# 连接 RabbitMQ Server
credentials = pika.PlainCredentials('guest', '12346')  # RabbitMQ 用户名和密码
connection = pika.BlockingConnection(
    pika.ConnectionParameters(
        host='192.168.10.91',
        port=5672,
        virtual_host='/',
        credentials=credentials)
)

channel = connection.channel()

# 声明交换机
channel.exchange_declare(
    exchange='logs1',  # 交换机的名称
    exchange_type='fanout'  # 指定交换机的工作模式
)

msg_list = [
    'info message.......',
    'error message.......',
    'warning message.......',
]

for msg in msg_list:
    channel.basic_publish(
        exchange='logs1',  # 要向哪个交换机发送数据
        routing_key='',  # 由于fanout模式不牵扯到队列,所以routing_key为空
        body=msg,  # 发送的消息内容
    )

print("[x] Sent '数据发送完了.......'")

connection.close()

consumer
"""
每一个consumer都要干:
1. 创建队列
2. 声明交换机,如果producer先声明了,那这里什么都不做
3. 将队列绑定到声明的交换机
4. 等待交换机中有消息就接受
"""
import pika

# 连接 RabbitMQ Server
credentials = pika.PlainCredentials('guest', '12346')  # RabbitMQ 用户名和密码
connection = pika.BlockingConnection(
    pika.ConnectionParameters(
        host='192.168.10.91',
        port=5672,
        virtual_host='/',
        credentials=credentials)
)

# 消费者通过 channel 对象与 RabbitMQ Server 打交道
channel = connection.channel()

# 声明交换机
channel.exchange_declare(
    exchange='logs1',
    exchange_type='fanout'
)

# 声明队列
result = channel.queue_declare("", exclusive=True)  # exclusive=True 表示让 RabbitMQ 自己起名字
queue_name = result.method.queue
print("queue_name: ", queue_name)

# 将队列绑定导致指定交换机
channel.queue_bind(
    exchange='logs1',
    queue=queue_name
)

print(' [*] Waiting for logs1. To exit press CTRL+C')


def callback(ch, method, properties, body):
    print(" [x] Received %r" % body.decode())
    ch.basic_ack(delivery_tag=method.delivery_tag)


channel.basic_consume(
    queue=queue_name,
    auto_ack=False,
    on_message_callback=callback
)

channel.start_consuming()

效果:

关键字模式(Direct Exchange)

Direct Exchange 是 RabbitMQ 默认的 Exchange,完全根据 RoutingKey 来路由消息。设置 Exchange 和 Queue 的 Binding 时需指定 RoutingKey(一般为 Queue Name),发消息时也指定一样的 RoutingKey,消息就会被路由到对应的Queue。

Producer在生产消息时都会为该消息设置关键字,然后Send到Exchange中。

每个Consumer声明的队列在Bind时,会通过RoutingKey参数设置关键字,一个队列可以设置多个关键字。

当Exchange有消息需要分发时,只有消息的关键字跟队列的关键字对上,才发送到队列。

producer
"""
producer这里要干三件事情:
1. 连接rabbitmq
2. 声明交换机
3. 向交换机send message,并为 message 设置关键字
"""

import pika

# 连接 RabbitMQ Server
credentials = pika.PlainCredentials('guest', '12346')  # RabbitMQ 用户名和密码
connection = pika.BlockingConnection(
    pika.ConnectionParameters(
        host='192.168.10.91',
        port=5672,
        virtual_host='/',
        credentials=credentials)
)

channel = connection.channel()

channel.exchange_declare(
    exchange='logs2',  # 交换机的名称
    exchange_type='direct'  # 指定交换机的工作模式
)

msg_list = [
    ('info', 'info message.......'),
    ('error', 'error message.......'),
    ('warning', 'warning message.......'),
]
for msg in msg_list:
    channel.basic_publish(
        exchange='logs2',  # 要向哪个交换机发送数据
        routing_key=msg[0],  # 为消息设置关键字
        body=msg[1],  # 发送的消息内容
    )

print("[x] Sent '数据发送完了.......'")

connection.close()

consumer
"""
每一个consumer都要干:
1. 创建队列
2. 声明交换机,如果producer先声明了,那这里什么都不做
3. 将队列绑定到声明的交换机,并通过 RoutingKey 参数设置关键字
4. 当交换机中有了消息,对比 Queue 和 Exchange 中消息的关键字,匹配上次才接收
"""
import pika

# 连接 RabbitMQ Server
credentials = pika.PlainCredentials('guest', '12346')  # RabbitMQ 用户名和密码
connection = pika.BlockingConnection(
    pika.ConnectionParameters(
        host='192.168.10.91',
        port=5672,
        virtual_host='/',
        credentials=credentials)
)

# 消费者通过 channel 对象与 RabbitMQ Server 打交道
channel = connection.channel()

# 声明交换机
channel.exchange_declare(
    exchange='logs2',
    exchange_type='direct'
)

# 声明队列
result = channel.queue_declare("", exclusive=True)  # exclusive=True 表示让 RabbitMQ 自己起名字
queue_name = result.method.queue
print("queue_name: ", queue_name)

# 将队列绑定导致指定交换机
# routing_key = ['info']
# routing_key = ['info', 'error']
routing_key = ['warning']
for key in routing_key:
    channel.queue_bind(
        exchange='logs2',
        queue=queue_name,
        routing_key=key
    )

print(' [*] Waiting for logs2. To exit press CTRL+C')


def callback(ch, method, properties, body):
    print(" [x] Received %r" % body.decode())
    ch.basic_ack(delivery_tag=method.delivery_tag)


channel.basic_consume(
    queue=queue_name,
    auto_ack=False,
    on_message_callback=callback
)

channel.start_consuming()

效果

主题模式(Topic Exchange)

Topic Exchange也叫通配符模式。
Topic Exchange和Direct Exchange 类似,也需要通过 RoutingKey 来路由消息,区别在于Direct Exchange 对 RoutingKey 是精确匹配,而 Topic Exchange 支持模糊匹配。分别支持*#通配符,*表示匹配一个单词,#则表示匹配0或者多个单词。

producer
import pika

# 连接 RabbitMQ Server
credentials = pika.PlainCredentials('guest', '12346')  # RabbitMQ 用户名和密码
connection = pika.BlockingConnection(
    pika.ConnectionParameters(
        host='192.168.10.91',
        port=5672,
        virtual_host='/',
        credentials=credentials)
)

# 消费者通过 channel 对象与 RabbitMQ Server 打交道
channel = connection.channel()

channel.exchange_declare(
    exchange='logs3',  # 交换机的名称
    exchange_type='topic'  # 指定交换机的工作模式
)

msg_list = [
    ('usa.news', '美国当地时间.......'),
    ('usa.weather', '美国加州天气.......'),
    ('europe.news', '欧洲当地时间.......'),
    ('europe.weather', '欧洲法国巴黎遭遇百年一遇大雾.......'),
]
for msg in msg_list:
    channel.basic_publish(
        exchange='logs3',  # 要向哪个交换机发送数据
        routing_key=msg[0],  # 为消息设置关键字
        body=msg[1],  # 发送的消息内容
    )

print("[x] Sent '数据发送完了.......'")

connection.close()
consumer
import pika

# 连接 RabbitMQ Server
credentials = pika.PlainCredentials('guest', '12346')  # RabbitMQ 用户名和密码
connection = pika.BlockingConnection(
    pika.ConnectionParameters(
        host='192.168.10.91',
        port=5672,
        virtual_host='/',
        credentials=credentials)
)

# 消费者通过 channel 对象与 RabbitMQ Server 打交道
channel = connection.channel()

# 声明交换机
channel.exchange_declare(
    exchange='logs3',
    exchange_type='topic'
)

# 声明队列
result = channel.queue_declare("", exclusive=True)
queue_name = result.method.queue
print(queue_name)

# 将队列绑定导致指定交换机
# routing_key = ['usa.#']
routing_key = ['europe.#']

for key in routing_key:
    channel.queue_bind(
        exchange='logs3',
        queue=queue_name,
        routing_key=key
    )

print(' [*] Waiting for logs3. To exit press CTRL+C')


def callback(ch, method, properties, body):
    print(" [x] Received %r" % body.decode())
    ch.basic_ack(delivery_tag=method.delivery_tag)


channel.basic_consume(
    queue=queue_name,
    auto_ack=False,
    on_message_callback=callback
)

channel.start_consuming()

效果:

默认模式(Default Exchange)

Default Exchange是一种特殊的Direct Exchange。当你手动创建一个队列时,后台会自动将这个队列绑定到一个名称为空的 Direct Exchange 上,绑定 RoutingKey 与队列名称相同。有了这个默认的交换机和绑定,使我们只关心队列这一层即可,这个比较适合做一些简单的应用。

示例跟最开始的简单示例类似。

producer
import pika

# 连接 RabbitMQ Server
credentials = pika.PlainCredentials('guest', '12346')  # RabbitMQ 用户名和密码
connection = pika.BlockingConnection(
    pika.ConnectionParameters(
        host='192.168.10.91',
        port=5672,
        virtual_host='/',
        credentials=credentials)
)

# 消费者通过 channel 对象与 RabbitMQ Server 打交道
channel = connection.channel()

# 声明一个队列,如果该队列不存在就创建
result = channel.queue_declare('logs4')
queue_name = result.method.queue

channel.basic_publish(
    exchange='',  # 当 exchange 值为空时,使用默认的交换机模式
    routing_key=queue_name,  # 指定路由队列
    body='今日油条涨价......'  # 投递消息
)
print(queue_name)

print("[x] Sent '今日油条涨价......'")
consumer
import pika

# 连接 RabbitMQ Server
credentials = pika.PlainCredentials('guest', '12346')  # RabbitMQ 用户名和密码
connection = pika.BlockingConnection(
    pika.ConnectionParameters(
        host='192.168.10.91',
        port=5672,
        virtual_host='/',
        credentials=credentials)
)

# 消费者通过 channel 对象与 RabbitMQ Server 打交道
channel = connection.channel()

# 声明队列
result = channel.queue_declare("logs4")
queue_name = result.method.queue
print(queue_name)


def callback(ch, method, properties, body):
    print(" [x] Received %r" % body.decode())
    ch.basic_ack(delivery_tag=method.delivery_tag)


# 配置监听队列的相关参数
channel.basic_consume(
    queue=queue_name,  # 监听指定队列
    auto_ack=False,  # auto_ack:False 表示当消费者从队列中取出消息后,需要手动确认后才从队列中删除该消息
    on_message_callback=callback  # 当监听的队列有消息时调用回调函数执行具体逻辑
)

# 开始监听
channel.start_consuming()

效果:

消息头订阅模式(Headers Exchange)

Headers Exchange 会忽略 RoutingKey 而根据消息中的 Headers 和创建绑定关系时指定的 Arguments 来匹配决定路由到哪些 Queue。

Headers Exchange 的性能比较差,而且 Direct Exchange 完全可以代替它,所以不建议使用。

另外,我在官网上也没找到Python相关的示例。

但从网上找到了一些Java示例:https://blog.csdn.net/warybee/article/details/103126707


that's all, see also:

python – 获取RabbitMQ队列中的消息数 | python监控rabbitmq的消息队列数量 | Docker实战:Docker安装部署RabbitMQ | MQ - RabbitMQ - 架构及工作原理 | docker 安装rabbitMQ | 理解 RabbitMQ Exchange | RabbitMQ系列教程(六)RabbitMQ Exchange类型之headers Exchange | 消息队列 | python - pika模块的使用中,如何动态的删除一个 durable=True 的持久化队列?

posted @ 2020-11-02 18:05  听雨危楼  阅读(136)  评论(0编辑  收藏  举报