1.相关概念
AMQP
AMQP:即Advanced Message Queuing Protocol,是一个应用层标准高级消息队列协议,提供统一消息服务。是应用层协议的一个开放标准,为面向消息的中间件设计。基于此协议的客户端与消息中间件可传递消息,并不受客户端/中间件不同产品,不同的开发语言等条件的限制。Erlang中的实现有RabbitMQ等。
RabbitMQ
RabbitMQ:(Rabbit,兔子)由erlang语言开发,基于AMQP协议,在erlang语言特性的加持下,RabbitMQ稳定性要比其他的MQ产品好一些,而且erlang语言本身是面向高并发的编程的语言,所以RabbitMQ速度也非常快。且它基于AMQP协议,对分布式、微服务更友好。
一套完整的RabbitMQ数据通信流程由四大部分组成:
- 生产者: 发送消息的程序就是一个生产者(producer)
- 交换机(exchange): RabbitMQ的设计思想就是生产者的消息不直接发送到队列中,而是发到交换机上处理,由交换机决定发往哪个队列
- 队列(queue): 实质上队列就是个巨大的消息缓冲区,它的大小只受主机内存和硬盘限制。多个生产者(producers)可以把消息发送给同一个队列,同样,多个消费者(consumers)也能够从同一个队列(queue)中获取数据。
- 消费者: 一个消费者(consumer)就是一个等待获取消息的程序。
2.实现生产者和消费者
使用python pika库实现,由于交换机相对较为复杂,这里先使用默认exchange进行实例,交换机相关的会在后面一起介绍
生产者
import pika
# Step 1 建立连接
conn = pika.BlockingConnection(pika.ConnectionParameters('127.0.0.1'))
channel = conn.channel()
# Step 2 创建一个名为'hello'的队列
channel.queue_declare(queue='hello')
# Step 3 向默认交换机中发送消息,并指定由交换机发往'hello'队列,消息内容为'Hello World'
channel.basic_publish(exchange='',routing_key='hello',body='Hello World!')
print(" [x] Sent 'Hello World!'")
# Step 4 释放连接
conn.close()
消费者
import pika
# Step 1 建立连接
conn = pika.BlockingConnection(pika.ConnectionParameters('127.0.0.1'))
channel = conn.channel()
# Step 2 创建一个名为'hello'的队列
channel.queue_declare(queue='hello')
# Step 3 需要为消费者定义一个回调函数,当消费者受到消息时进行调用
def callback(ch, method, properties, body):
print(" [x] Received %r" % body)
# Step 4 指定接收哪个队列的消息,接收到后进行怎么的行为
channel.basic_consume(queue='hello', on_message_callback=callback, auto_ack=True)
# Step 5 循环监听
print(' [*] Waiting for messages. To exit press CTRL+C')
channel.start_consuming()
可以发现,虽然我们在创建生产者时已经创建了队列,但是为什么在创建消费者时重复声明了这个队列呢?
如果我们确定了队列是已经存在的,那么我们可以不这么做,比如此前预先运行了send.py程序。可是我们并不确定哪个程序会首先运行。这种情况下,在程序中重复将队列重复声明一下是种值得推荐的做法。(即使重复声明也只会生效一次,即如果已经声明过,则再次声明不会进行任何操作,只是为了提高程序的健壮性)
整体运行
首先启动RabbitMQ服务,启动后运行生产者程序向队列中发送消息(生产者与消费者的启动顺序并没有强制要求,这里先启动生产者是为了可以在队列中看到数据的存在)
$ python send.py
[x] Sent 'Hello World!'
使用命令行工具查看当前队列状态,可以看到此时已经创建了名为hello的队列且当前队列中有一个消息等待消费
$ rabbitmqctl list_queues
Timeout: 60.0 seconds ...
Listing queues for vhost / ...
name messages
hello 1
运行消费者程序
$ python recive.py
[*] Waiting for messages. To exit press CTRL+C
[x] Received b'Hello World!'
...
消费者程序运行后直接将消息消费掉(此时队列中的数据数量重新变为0),并处于循环监听状态,等待下一条数据
3.交换机
具体细节可以参考RabbitMQ官方文档
RabbitMQ消息传递模型中的核心思想是生产者从不将任何消息直接发送到队列。实际上,生产者经常甚至根本不知道是否将消息传递到任何队列。生产者只能将消息发送到交换机,由交换器决定其发往哪个队列
交换机支持四种类型的exchange:
1.direct exchange
exchange在和queue进行binding时会设置routingkey
channel.queue_declare(queue='queue001') # 创建队列
channel.exchange_declare(exchange='exchange001', exchange_type='direct') # 创建交换机
channel.queue_bind(queue="queue001", exchange="exchange001", routing_key="001") # 为交换机创建规则,如果命中key则转发到对应的队列中
# 发送消息
channel.basic_publish(exchange="exchange001",
routingKey="001",
body="hello world");
在direct类型的exchange中,只有这两个routingkey完全相同,exchange才会选择对应的binging进行消息路由。
2.topic exchange
此类型exchange和上面的direct类型差不多,但direct类型要求routingkey完全相等,这里的routingkey可以有通配符:'*', '#'.
其中'*'表示匹配一个单词, '#'则表示匹配没有或者多个单词
channel.queue_bind(queue="queue001", exchange="exchange001", routingKey="001.*"); # 为交换机创建规则,如果命中key则转发到对应的队列中
# 发送消息
channel.basic_publish(exchange="exchange001",
routingKey="001.test",
body="hello world");
3.fanout exchange
此exchange的路由规则很简单直接将消息路由到所有绑定
的队列中,无须对消息的routingkey进行匹配操作。
4.header exchange
此类型的交换机匹配规则遇前三种差异较大,不使用routingkey最为匹配条件,而是使用header进行匹配(这种方式很少用到),使用方式如下
## 绑定
aHeader = {}
aHeader["rule01"] = "01"
aHeader["rule02"] = "02"
aHeader["x-match"] = "all"
channel.queue_bind(queue="queue001", exchange="exchange001", routingKey="", arguments=aHeader);
## 发送消息时附带header
mHeader = {}
mHeader["rule01"] = "01"
mHeader["rule02"] = "02"
properties = pika.BasicProperties(headers=mHeader)
channel.basic_publish(exchange='exchange001',routing_key='002',body='Send to 002_3',properties=properties)
示例程序
以direct类型为例,为一个交换机绑定两条规则,不同的key会分别转发到对应的队列中;
生产者程序
import pika
conn = pika.BlockingConnection(pika.ConnectionParameters('127.0.0.1'))
channel = conn.channel()
channel.queue_declare(queue='queue001') #创建队列1
channel.queue_declare(queue='queue002') #创建队列2
channel.exchange_declare(exchange='exchange001', exchange_type='direct') #创建交换机
channel.queue_bind(queue="queue001", exchange="exchange001", routing_key="001") #创建绑定关系
channel.queue_bind(queue="queue002", exchange="exchange001", routing_key="002") #创建绑定关系
channel.basic_publish(exchange='exchange001',routing_key='001',body='Send to 001_1')
channel.basic_publish(exchange='exchange001',routing_key='001',body='Send to 001_2')
channel.basic_publish(exchange='exchange001',routing_key='001',body='Send to 001_3')
channel.basic_publish(exchange='exchange001',routing_key='002',body='Send to 002_1')
channel.basic_publish(exchange='exchange001',routing_key='002',body='Send to 002_2')
channel.basic_publish(exchange='exchange001',routing_key='002',body='Send to 002_3')
print(" [x] Sent Success! ")
conn.close()
消费者程序1
import pika
conn = pika.BlockingConnection(pika.ConnectionParameters('127.0.0.1'))
channel = conn.channel()
channel.queue_declare(queue='queue001')
def callback(ch, method, properties, body):
print(" [x] Received %r" % body)
channel.basic_consume(queue='queue001', on_message_callback=callback, auto_ack=True)
print(' [*] Waiting for messages. To exit press CTRL+C')
channel.start_consuming()
消费者程序2
import pika
conn = pika.BlockingConnection(pika.ConnectionParameters('127.0.0.1'))
channel = conn.channel()
channel.queue_declare(queue='queue002')
def callback(ch, method, properties, body):
print(" [x] Received %r" % body)
channel.basic_consume(queue='queue002', on_message_callback=callback, auto_ack=True)
print(' [*] Waiting for messages. To exit press CTRL+C')
channel.start_consuming()
默认交换机
现在再回过头来看一下之前所使用的的默认交换机,先用rabbitmqctl list_exchanges
命令列出所有的交换机
$ rabbitmqctl list_exchanges
Listing exchanges for vhost / ...
name type
amq.fanout fanout
amq.rabbitmq.trace topic
amq.direct direct
amq.match headers
amq.headers headers
amq.topic topic
direct
最后一行那个名字为空的direct类型的交换机就是默认交换机,其用法与手动创建的direct类型的交换机最大的区别就是在创建队列后对自动绑定,无需手动bind(使用方式可以翻到第一章节的生产者程序查看);
4.死信队列
死信队列全称 Dead-Letter-Exchange 简称 DLX,消息在一段时间之后没有被消费就会变成死信被重新 publish 到另一个 DLX 交换器队列中,因此称为死信队列。Dead-Letter-Exchange 也是一种普通的 Exchange。
消息会在以下几种情况下被移入死信队列
- 消息被拒绝(basic_reject/basic_nack)
- 消息 TTL 过期
- 队列达到最大长度
下面演示下死信队列的配置以及消费者拒绝消息并将消息置入死信队列的全过程
生产者
import pika
conn = pika.BlockingConnection(pika.ConnectionParameters('127.0.0.1'))
channel = conn.channel()
# 声明队列并设置死信队列
channel.queue_declare(queue='queue004', arguments={"x-dead-letter-exchange": "dlx-queue004","x-dead-letter-routing-key": "dlx-004"})
channel.exchange_declare(exchange='exchange004', exchange_type='direct') # 声明交换机
channel.queue_bind(queue="queue004", exchange="exchange004", routing_key="004") # 绑定
channel.queue_declare(queue="pt-dlx-queue") # 声明死信队列
channel.exchange_declare(exchange="dlx-queue004", exchange_type='direct') # 声明死信交换机
channel.queue_bind(queue="pt-dlx-queue", exchange="dlx-queue004", routing_key="dlx-004") # 绑定
channel.basic_publish(exchange='exchange004',routing_key='004',body='Send to 004_1')
print(" [x] Sent Success! ")
conn.close()
消费者
import pika
conn = pika.BlockingConnection(pika.ConnectionParameters('127.0.0.1'))
channel = conn.channel()
# 重复定义队列
channel.queue_declare(queue='queue004', arguments={"x-dead-letter-exchange": "dlx-queue004",
"x-dead-letter-routing-key": "dlx-004"
})
def callback(ch, method, properties, body):
print(" [x] Received %r" % body)
# ch.basic_ack(delivery_tag=method.delivery_tag)
ch.basic_reject(delivery_tag=method.delivery_tag, requeue=False) #将消息拒绝并将重新放回队列置为False,这样就会放置到之前配置的死信队列中
channel.basic_consume(queue='queue004', on_message_callback=callback, auto_ack=False)
print(' [*] Waiting for messages. To exit press CTRL+C')
channel.start_consuming()
在实际应用上除了死信队列的原始功能(存放各种原因失效的数据)外,还可以配合TTL实现定时任务的功能,例如将消息的TTL设置为10秒,消费者监听死信队列,10s后消息超时,会将数据加入死信队列,这时消费者就收到的消息,实现了延迟消息的功能
5.其他
5.1 ack机制
通过观察上述内容中的消费者代码 可以发现在消费时设置了auto_ack为True,其代表的含义是什么呢?
所谓的ACK确认机制:
自动ACK:消费者接收到消息后自动发送ACK给RabbitMQ。
手动ACK:我们手动控制消费者接收到并成功消息后发送ACK给RabbitMQ。
可以看上图:如果使用自动ACK,当消息者将消息从channel中取出后,RabbitMQ随即将消息给删除。接着不幸的是,消费者没来得及处理消息就挂了。那也就意味着消息其实丢失了。
channel.basic_consume(queue='queue003', on_message_callback=callback, auto_ack=False)
#置为False,再次尝试可以发现,数据已经获取成功,但是在RabbitMQ的后台依旧可以看到数据存在
手动ack
def callback(ch, method, properties, body):
print(" [x] Received %r" % body)
# delivery_tag 是在 channel 中的一个消息计数, 每次消息提取行为都对应一个数字.
ch.basic_ack(delivery_tag=method.delivery_tag)
注意: 手动ack可能会导致数据重复消费,需要特别注意
5.2 basic_reject/basic_nack 的区别
上文介绍死信队列时涉及到reject和nack两种消费者对于消息的响应状态(一种有3种 还有一种ack用于确认消息,在5.1中已经介绍过);reject和nack都代表消息处理失败,那么二者的区别是什么呢?
消息手动除了一次确认一条,也可以一次确认多条。为了减少网络流量,可以批量手动确认。在应答时,设置basic.nack的multiple 字段为true,可以同时对delivery_tag和比delivery_tag值小的投递消息进行确认 例如,假设在通道上没有确认消息的delivery_tag是5,6,7和8,当basic.nack中delivery_tag被设置为8并且multiple 被设置为true时,方法执行成功后,从5到8的所有消息将被确认。 如果multiple 设置为false,那么交货5,6和7仍然是未确认的。
5.3 持久化
MQ默认建立的是临时 queue 和 exchange,如果不声明持久化,一旦 rabbitmq 挂掉,queue、exchange 将会全部丢失。所以我们一般在创建 queue 或者 exchange 的时候会声明 持久化。
queue 声明持久化
# 声明消息队列,消息将在这个队列传递,如不存在,则创建。durable = True 代表消息队列持久化存储,False 非持久化存储
result = channel.queue_declare(queue = 'python-test',durable = True)
exchange 声明持久化
# 声明exchange,由exchange指定消息在哪个队列传递,如不存在,则创建.durable = True 代表exchange持久化存储,False 非持久化存储
channel.exchange_declare(exchange = 'python-test', durable = True)
消息持久化
# 向队列插入数值 routing_key是队列名。delivery_mode = 2 声明消息在队列中持久化,delivery_mod = 1 消息非持久化
channel.basic_publish(exchange = '',routing_key = 'python-test',body = message,
properties=pika.BasicProperties(delivery_mode = 2))
5.4 TTL
5.4 队列常用参数
声明队列queue_declare方法的原型 :
channel.queue_declare(queue='', passive=False, durable=False,
exclusive=False, auto_delete=False,
arguments=None):
1.queue
队列名称
2.durable
是否持久化, 队列的声明默认是False,即存放到内存中的,如果rabbitmq重启会丢失。如果想重启之后还存在就要使队列持久化,保存到Erlang自带的Mnesia数据库中,当rabbitmq重启之后会读取该数据库。
3.exclusive
是否排外的,默认为False,不排外。有两个作用:
一:当连接关闭时connection.close()该队列是否会自动删除;
二:该队列是否是私有的private,如果不是排外的,可以使用两个消费者都访问同一个队列,没有任何问题;
如果是排外的,会对当前队列加锁,只允许当前消费者可以访问,其他通道channel是不能访问的,如果强制访问会报异常:ShutdownSignalException: channel error
一般等于true的话用于一个队列只能有一个消费者来消费的场景 。
4.auto_delete
是否自动删除,当最后一个消费者断开连接之后队列是否自动被删除,默认为False。
可以通过RabbitMQ Management,查看某个队列的消费者数量,当consumers = 0时,即没有任务消费者时,队列就会自动删除
5.arguments
Message TTL消息剩余生存时间
为该队列的所有消息统一设置相同的声明周期:统一设置队列中的所有消息的过期时间,例如设置10秒,10秒后这个队列的消息清零
channel.queue_declare(queue='queue004', arguments={"x-dead-letter-exchange": "dlx-queue004",
"x-dead-letter-routing-key": "dlx-004",
"x-message-ttl": 10000
})
Auto Expire自动过期
x-expires用于当多长时间没有消费者访问该队列的时候,该队列会自动删除,可以设置一个延迟时间,如仅启动一个生产者,10秒之后该队列会删除,或者启动一个生产者,再启动一个消费者,消费者运行结束后10秒,队列也会被删除
Max Length最大长度
x-max-length:用于指定队列的长度,如果不指定,可以认为是无限长,例如指定队列的长度是4,当超过4条消息,前面的消息将被删除,给后面的消息腾位
Max Length Bytes代码片段
x-max-length-bytes: 用于指定队列存储消息的占用空间大小,当达到最大值是会删除之前的数据腾出空间
Maximum priority最大优先级
x-max-priority: 设置消息的优先级,优先级值越大,越被提前消费。
首先设置队列最大优先级,之后设置消息的优先级,示例中将第六条消息的优先级设置为最高
import pika
conn = pika.BlockingConnection(pika.ConnectionParameters('127.0.0.1'))
channel = conn.channel()
channel.queue_delete(queue='queue004')
# 声明队列并最大优先级
channel.queue_declare(queue='queue004', arguments={"x-max-priority":100})
# 声明交换机
channel.exchange_declare(exchange='exchange004', exchange_type='direct')
# 绑定
channel.queue_bind(queue="queue004", exchange="exchange004", routing_key="004")
# 对第六条消息优先级设置为最高
for i in range(0,10):
if i == 5:
p = 100
else:
p = 10
properties = pika.BasicProperties(priority=p)
channel.basic_publish(exchange='exchange004',routing_key='004',body='Send to 004_%s'%(i),properties=properties)
print(" [x] Sent Success! ")
conn.close()
先启动生产者程序,使消息堆积在队列中,再启动消费者,查看读取顺序
[*] Waiting for messages. To exit press CTRL+C
[x] Received b'Send to 004_5'
[x] Received b'Send to 004_0'
[x] Received b'Send to 004_1'
[x] Received b'Send to 004_2'
[x] Received b'Send to 004_3'
[x] Received b'Send to 004_4'
[x] Received b'Send to 004_6'
[x] Received b'Send to 004_7'
[x] Received b'Send to 004_8'
[x] Received b'Send to 004_9'
结果符合期望