RabbitMQ学习笔记
MQ的作用
应用解耦
减少系统间的耦合, 使得某个应用出现异常时, 不会影响另一个应用, 提高系统稳定性
流量削峰
在流量高峰期时, 防止所有请求都同时访问到数据库, 可以使用消息队列作为缓冲, 让请求平缓地进入系统, 可能会导致用户端请求变慢, 但是比系统崩溃不能操作要好
异步处理
每个服务处理数据的速度并不相同, 有的处理很快, 有的处理很慢, 为了平衡两者的速率不一致的问题, 可以使用队列进行异步的处理
RabbitMQ概念
Broker:接收和分发消息的应用,RabbitMQ Server 就是 Message Broker
Virtual host:出于多租户和安全因素设计的,把 AMQP 的基本组件划分到一个虚拟的分组中,类似于网络中的 namespace 概念。当多个不同的用户使用同一个 RabbitMQ server 提供的服务时,可以划分出多个vhost,每个用户在自己的 vhost 创建 exchange/queue 等
Connection:publisher/consumer 和 broker 之间的 TCP 连接
Channel:如果每一次访问 RabbitMQ 都建立一个 Connection,在消息量大的时候建立 TCP Connection 的开销将是巨大的,效率也较低。Channel 是在 connection 内部建立的逻辑连接,如果应用程序支持多线程,通常每个 thread 创建单独的 channel 进行通讯,AMQP method 包含了 channel id 帮助客户端和 message broker 识别 channel,所以 channel 之间是完全隔离的。Channel 作为轻量级的 Connection 极大减少了操作系统建立 TCP connection 的开销
Exchange:message 到达 broker 的第一站,根据分发规则,匹配查询表中的 routing key,分发消息到 queue 中去。常用的类型有:direct (point-to-point), topic (publish-subscribe) and fanout (multicast)
Queue:消息最终被送到这里等待 consumer 取走
Binding:exchange 和 queue 之间的虚拟连接,binding 中可以包含 routing key,Binding 信息被保存到 exchange 中的查询表中,用于 message 的分发依据
消费者消息应答
为了确保每条消息都能被消费者成功消费, 即为了保证消息发送时不丢失,rabbitmq 引入消息应答机制,消息应答就是:消费者在接收到消息并且处理该消息之后,告诉 rabbitmq 它已经处理了,rabbitmq 可以把该消息删除了。
自动应答
channel.consume(queue_name, auto_ack=True)
消息发送后立即被认为已经传送成功,这种模式需要在高吞吐量和数据传输安全性方面做权衡, 因为这种模式如果消息在接收到之前,消费者那边出现连接或者 channel 关闭,那么消息就丢失了
当然另一方面这种模式消费者那边可以传递过载的消息,没有对传递的消息数量进行限制, 当然这样有可能使得消费者这边由于接收太多还来不及处理的消息,导致这些消息的积压,最终使得内存耗尽,最终这些消费者线程被操作系统杀死,所以这种模式仅适用在消费者可以高效并以某种速率能够处理这些消息的情况下使用。
手动应答
肯定确认, RabbitMQ 已知道该消息并且成功的处理消息,可以将其丢弃了:
channel.basic_ack(delivery_tag=0, multiple=False):
# delivery_tag:该消息的index
# multiple:是否批量. true:将一次性ack所有小于deliveryTag的消息。
否定确认, RabbitMQ知道该消息被消费者拒绝了, 根据消费者返回的参数决定是否将该消息重新加入队列
channel.basic_nack(delivery_tag=0, multiple=False, requeue=True)
channel.basic_reject(delivery_tag=0, requeue=True)
# delivery_tag:该消息的index
# multiple:是否批量. true:将一次性ack所有小于deliveryTag的消息。
# requeue: 被拒绝的消息是否重新加入MQ队列
# basic_nack可以拒绝多条消息, 而basic_reject只能拒绝一条
消费者是push还是pull
在rabbitmq中有两种消息处理的模式,一种是推模式/订阅模式/投递模式(也叫push模式),消费者调用channel.basicConsume方法订阅队列后,由RabbitMQ主动将消息推送给订阅队列的消费者;另一种是拉模式/检索模式(也叫pull模式),需要消费者调用channel.basicGet方法,主动从指定队列中拉取消息。
推模式(push)
1:推模式接收消息是最有效的一种消息处理方式。channel.basicConsume(queneName,consumer)方法将信道(channel)设置成投递模式,直到取消队列的订阅为止;在投递模式期间,当消息到达RabbitMQ时,RabbitMQ会自动地、不断地投递消息给匹配的消费者,而不需要消费端手动来拉取,当然投递消息的个数还是会受到channel.basicQos的限制。
2:推模式将消息提前推送给消费者,消费者必须设置一个缓冲区缓存这些消息。优点是消费者总是有一堆在内存中待处理的消息,所以当真正去消费消息时效率很高。缺点就是缓冲区可能会溢出。
3:由于推模式是信息到达RabbitMQ后,就会立即被投递给匹配的消费者,所以实时性非常好,消费者能及时得到最新的消息。
拉模式(pull)
1:如果只想从队列中获取单条消息而不是持续订阅,则可以使用channel.basicGet方法来进行消费消息。
2:拉模式在消费者需要时才去消息中间件拉取消息,这段网络开销会明显增加消息延迟,降低系统吞吐量。
3:由于拉模式需要消费者手动去RabbitMQ中拉取消息,所以实时性较差;消费者难以获取实时消息,具体什么时候能拿到新消息完全取
决于消费者什么时候去拉取消息。
QOS预取
无论是推送Consume、拉取Get哪一种方式,只要消费者不进行消息确认, RabbitMQ就认为消息没有成功处理(前提条件是消费者是手动消息应答的), 为了避免消费者还没有确认而RabbitMQ还一直给消费者消息, 导致消费者处理不过来的情况, 可以通过设置QOS来控制MQ发送给消费者的消息数量.
设置了QOS的值后, 如果有超过QOS值的消息没有返回确认, 那么就不会对新的消息进行消费. QOS的默认值为0, 表示不进行消费控制
channel.basic_qos(prefetch_size=0, prefetch_count=0, global_qos=False)
# prefetch_size: 没有消息返回确认的消息大小
# prefetch_count: 没有消息返回确认的消息数量
# global_qos: True则当前qos为所有的channel生效, False为只在当前channel生效
RabbitMQ持久化
Exchange持久化
在申明exchange时传入持久化参数durable
为True
.
channel.exchange_declare("mq_test", durable=True)
队列持久化
在申明队列的时候, 传入持久化参数durable
为True
channel.queue_declare(queue="mq_test_q", durable=True)
注意:如果已存在一个非持久化的 queue 或 exchange ,执行上述代码会报错,因为当前状态不能更改 queue 或 exchange 存储属性,需要删除重建。如果 queue 和 exchange 中一个声明了持久化,另一个没有声明持久化,则不允许绑定。
消息持久化
在发布消息的时候, 传入持久化参数properties
# delivery_mode = 2 声明消息在队列中持久化,delivery_mod = 1 消息非持久化
# TRANSIENT_DELIVERY_MODE = 1
# PERSISTENT_DELIVERY_MODE = 2
channel.basic_publish(exchange = '',routing_key = 'python-test',body = message,
properties=pika.BasicProperties(delivery_mode = 2))
生产者发布确认
通过调用方法``来开启发布确认模式
channel.confirm_delivery()
一旦信道进入 confirm 模式,所有在该信道上面发布的 消息都将会被指派一个唯一的 ID(从 1 开始),一旦消息被投递到所有匹配的队列之后,broker 就会发送一个确认给生产者(包含消息的唯一 ID),这就使得生产者知道消息已经正确到达目的队 列了,如果消息和队列是可持久化的,那么确认消息会在将消息写入磁盘之后发出,broker 回传 给生产者的确认消息中 delivery-tag 域包含了确认消息的序列号,此外 broker 也可以设置 basic.ack 的 multiple 域,表示到这个序列号之前的所有消息都已经得到了处理。
Binding(绑定)
binding 其实是 exchange 和 queue 之间的桥梁,它告诉我们 exchange 和那个队列进行了绑定关系. 一个exchange可以绑定多个队列, 绑定时也可以再指定一个routing_key
, 可以理解为两者绑定的关系名称.
channel.queue_bind(queue="mq_test_q", exchange="mq_test_e", routing_key="mq_test_k")
-
相同的exchange和队列时, 可以指定多个
routing_key
-
一个exchange绑定多个队列时, 也可以指定为相同的
routing_key
Exchange(交换机)
生产者生产的消息从不会直接发送到队列, 只能将消息发送到交换机(exchange),交换机工作的内容非常简单,一方面它接收来自生产者的消息,另一方面将它们推入队列。交换机必须确切知道如何处理收到的消息。是应该把这些消息放到特定队列还是说把他们放到许多队列中还是说应该丢弃它们。这就得由交换机的类型来决定。交换机类型有: 直接(direct), 主题(topic) ,标题(headers) , 扇出(fanout)
Fanout(广播)
是将接收到的所有消息广播到它知道的所有队列中
Direct(直接)
上面Fanout模式会将消息发送给所有的队列, 如果想指定某条消息发送给某个队列, 就可以通过Direct模式, 发送消息时通过指定routing_key
将消息发送给对应的队列上
在这种绑定情况下,生产者发布消息到 exchange 上,绑定键为 orange 的消息会被发布到队列 Q1。绑定键为 blackgreen 和的消息会被发布到队列 Q2,其他消息类型的消息将被丢弃。
Topics(主题)
topic 交换机的消息的 routing_key 不能随意写,必须满足一定的要求,它必须是一个单词列表,以点号分隔开。这些单词可以是任意单词,比如说:"stock.usd.nyse", 也可以使用两个替换符:
*(星号) 可以代替一个单词
#(井号) 可以替代零个或多个单词
Q1--> 绑定的是 中间带 orange 带 3 个单词的字符串(.orange.)
Q2--> 绑定的是 最后一个单词是 rabbit 的 3 个单词(..rabbit) 和 第一个单词是 lazy 的多个单词(lazy.#)
死信队列
死信的来源
- 消息TTL过期
- 队列满了, 新的消息无法添加到队列中
- 消息被消费者拒绝(basic.reject或basic.nack), 并且requeue=False
如何使用死信队列
- 先申明死信
exchange
和死信队列, 并绑定routing_key
- 申明正常队列时, 添加设置
x-dead-letter-exchange
和x-dead-letter-routing-key
# 死信队列
dead_exchange = "dead_exchange"
dead_queue = "dead_queue"
dead_routing_key = "dead_key"
normal_exchange = "normal_exchange"
normal_queue = "normal_queue"
normal_routing_key = "normal_key"
# 申明死信exchange
channel.exchange_declare(dead_exchange)
# 申明死信队列
channel.queue_declare(dead_queue)
# 死信队列与死信交换机绑定
channel.queue_bind(dead_queue, dead_exchange, dead_routing_key)
# 申明正常exchange
channel.exchange_declare(normal_exchange)
# 申明正常队列, 传入死信队列相关设置
args = {"x-dead-letter-exchange": dead_exchange, "x-dead-letter-routing-key": dead_routing_key}
channel.queue_declare(normal_exchange, arguments=args)
# 正常队列绑定正常exchange
channel.queue_bind(normal_queue, normal_exchange, normal_routing_key)
延迟队列
延时队列就是用来存放需要在指定时间被处理的元素的队列。如: 订单在十分钟之内未支付则自动取消
设置消息的TTL
单位为毫秒
channel.basic_publish(exchange='mq_test_e', routing_key='mq_test_q', body=b"my_test_msg",
properties=pika.BasicProperties(expiration=1000))
设置队列的TTL
创建队列时设置x-message-ttl
属性, 那么队列中的所有消息都有了TTL
args = {"x-message-ttl": 1000}
channel.queue_declare(normal_exchange, arguments=args)
实现延迟队列
当消息设置了TTL并且到期了之后, 若配置了死信队列, 则消息会放入死信队列中, 此时再消费死信队列的消息, 整个流程就实现了延迟队列的效果. 即将需要延迟消费的消息设置TTL, 但并不直接消费, 而是等其过期自动进入死信队列, 再从死信队列中消费该消息.