一.RabbitMQ之生产者,消费者代码实现
rabbitmq安装之前有讲,虚拟机设置,账号添加设置请看前一节
rabbitmq进行生产者/消费者模型通信有很多中方式,消费者可以直接声明rabbitmq队列,然后生成者直接将队列消息发送到指定消费者队列,然后进行消费,生产者也可以先将消息发给交换机然后通过bind的routing_key分发到指定rabbitmq队列,前者属于一对一的word模型,后者属于exchange模型,不同之处在于交换机exchange模型通过交换机这一角色进行
1.1 简单work模型
生产者和消费者通过rabbitmq队列直接生产消费
直接上代码
消费者端
# _*_ coding: utf-8 _*_
# !/usr/bin/python
"""
Author:maoyaomin
Create Date: -11:12
User: EDZ
description:
"""
# 一对应模式 生产者------> 消费者 简单模式
import pika
class MqRecv:
def __init__(self, host, port, user, password):
self.host = host
self.port = port
self.user = user
self.password = password
self.rmq_obj = None
self.param_config = None
self.channel = None
def connection(self):
self.param_config = pika.ConnectionParameters(
virtual_host='mymTest',
host=self.host,
port=self.port,
credentials=pika.PlainCredentials(self.user, self.password),
)
self.rmq_obj = pika.BlockingConnection(self.param_config)
def callback(self, channel, methods, properties, body):
print("消费者接收来自消息队列中的{}成功".format(body))
channel.basic_ack(delivery_tag=methods.delivery_tag)
# channel.basic_reject(delivery_tag=methods.delivery_tag, requeue=False)
def recv_listen(self):
print("start")
self.connection()
self.channel = self.rmq_obj.channel()
self.channel.queue_declare(queue='changeMessage')
self.channel.basic_qos(prefetch_count=1)
self.channel.basic_consume(
self.callback,
queue='changeMessage',
no_ack=False,
)
print("当前MQ简单模式正在等待生产者往消息队列中放入消息")
self.channel.start_consuming()
**生产者端代码**
# 一对应模式 生产者------> 消费者 简单模式
import pika
class MqSend:
def __init__(self, host, port, user, password):
self.host = host
self.port = port
self.user = user
self.password = password
self.rmq_obj = None
self.param_config = None
self.channel = None
def connection(self):
self.param_config = pika.ConnectionParameters(
virtual_host='mymTest',
host=self.host,
port=self.port,
credentials=pika.PlainCredentials(self.user, self.password)
)
self.rmq_obj = pika.BlockingConnection(self.param_config)
def send(self):
self.connection()
self.channel = self.rmq_obj.channel()
self.channel.basic_publish(
exchange='', # exchange为空表示简单模式
routing_key='changeMessage', # 路由键直接通过queue的名称
body='{"name": "jack"}'
)
print('发送{}成功'.format('send name jack'))
注意点
其中 self.channel.basic_qos(prefetch_count=1)是用来给每个信道设置队列中分配的消息在没有进行确认(basic_ack)之前的最大数量
1.2 Exchange交换机模型
通过交换机exchange进行消息分发,实现多进程接收等,设置死信队列,业务队列处理错误对消息进行basic_reject后进入到死信队列进行处理
直接上代码
Exchange模式消费者代码
import pika
import threading
import multiprocessing
class MqRecv:
def __init__(self, host, port, user, password):
self.host = host
self.port = port
self.user = user
self.password = password
self.rmq_obj = None
self.param_config = None
self.channel = None
def connection(self):
self.param_config = pika.ConnectionParameters(
virtual_host='mymTest',
host=self.host,
port=self.port,
credentials=pika.PlainCredentials(self.user, self.password),
)
self.rmq_obj = pika.BlockingConnection(self.param_config)
@staticmethod
def callback_business(channel, methods, properties, body):
print("业务队列的日志为%s" % properties)
print("业务队列,消费者接收来自消息队列中的{}成功".format(body))
# channel.basic_ack(delivery_tag=methods.delivery_tag)
channel.basic_reject(delivery_tag=methods.delivery_tag, requeue=False)
@staticmethod
def callback_dead(channel, methods, properties, body):
print("死信队列的日志为%s" % properties)
print("死信队列,消费者接收来自消息队列中的{}成功".format(body))
# channel.basic_reject(delivery_tag=methods.delivery_tag, requeue=False)
channel.basic_ack(delivery_tag=methods.delivery_tag)
def exchange_declare_method(self):
self.channel = self.rmq_obj.channel()
self.channel.exchange_declare(exchange='deadExchange', exchange_type='direct', durable=True)
self.channel.exchange_declare(exchange='businessExchange', exchange_type='direct', durable=True)
def work(self, i):
self.connection()
self.exchange_declare_method()
# self.channel = self.rmq_obj.channel()
if i == 0:
self.channel = self.rmq_obj.channel()
dead_exchange_argument = {
'x-dead-letter-exchange': "deadExchange", # 延迟结束后指向交换机(死信收容交换机)
'x-dead-letter-routing-key': "deadKey", # routing_key指向队列(死信收容队列),可直接设置queue name也可以设置routing-key
'x - queue - type': "classic", # 延迟结束后指向交换机(死信收容交换机)
}
self.channel.queue_declare(queue='businessMessage', durable=True, arguments=dead_exchange_argument)
self.channel.queue_bind(exchange='businessExchange', queue='businessMessage', routing_key='needKey')
self.channel.basic_qos(prefetch_count=1)
self.channel.basic_consume(
self.callback_business,
queue='businessMessage',
no_ack=False,
)
print("进入业务交换机逻辑,当前MQ简单模式正在等待生产者往消息队列中放入消息")
self.channel.start_consuming()
elif i == 1:
self.channel = self.rmq_obj.channel()
dead_exchange_argument = {
'x-dead-letter-exchange': "deadExchange", # 延迟结束后指向交换机(死信收容交换机)
'x-dead-letter-routing-key': "deadKey", # 延迟结束后指向队列(死信收容队列),可直接设置queue name也可以设置routing-key
'x - queue - type': "classic", # 延迟结束后指向交换机(死信收容交换机)
}
self.channel.queue_declare(queue='deadMessage', durable=True, arguments=dead_exchange_argument)
self.channel.queue_bind(exchange='deadExchange', queue='deadMessage', routing_key='deadKey')
self.channel.basic_consume(
self.callback_dead,
queue='deadMessage',
no_ack=False,
)
print("进入死信交换机逻辑,当前MQ简单模式正在等待生产者往消息队列中放入消息")
self.channel.start_consuming()
print("启动成功")
def recv_listen(self):
print("start")
th_l = [multiprocessing.Process(target=self.work, args=(i,)) for i in range(2)]
for th in th_l:
th.start()
for th in th_l:
th.join()
Exchange生产者端代码
import pika
class MqSend:
def __init__(self, host, port, user, password):
self.host = host
self.port = port
self.user = user
self.password = password
self.rmq_obj = None
self.param_config = None
self.channel = None
def connection(self):
self.param_config = pika.ConnectionParameters(
virtual_host='mymTest',
host=self.host,
port=self.port,
credentials=pika.PlainCredentials(self.user, self.password)
)
self.rmq_obj = pika.BlockingConnection(self.param_config)
def send(self):
self.connection()
self.channel = self.rmq_obj.channel()
self.channel.basic_publish(
exchange='businessExchange', # exchange为空表示简单模式
routing_key='needKey',
body='{"name": "jack"}',
properties=pika.BasicProperties(delivery_mode=2,)
)
print('发送{}成功'.format('send name jack'))
注意点
1.多进程消费者端,主要是线程实现不了并行,通过多进程实现多cpu调度
2.通过properties=pika.BasicProperties(delivery_mode=2,)设置消息持久化
3.死信交换机创建及传参
二.RabbitMQ死信队列介绍
2.1死信队列是什么
死信,在官网中对应的单词为“Dead Letter”,可以看出翻译确实非常的简单粗暴。那么死信是个什么东西呢?
“死信”是RabbitMQ中的一种消息机制,当你在消费消息时,如果队列里的消息出现以下情况:
消息被否定确认,使用 channel.basicNack 或 channel.basicReject ,并且此时requeue 属性被设置为false。
消息在队列的存活时间超过设置的TTL时间。
消息队列的消息数量已经超过最大队列长度。
那么该消息将成为“死信”。
“死信”消息会被RabbitMQ进行特殊处理,如果配置了死信队列信息,那么该消息将会被丢进死信队列中,如果没有配置,则该消息将会被丢弃
2.2 死信队列如何配置
这一部分将是本文的关键,如何配置死信队列呢?其实很简单,大概可以分为以下步骤:
配置业务队列,绑定到业务交换机上
为业务队列配置死信交换机和路由key
为死信交换机配置死信队列
注意,并不是直接声明一个公共的死信队列,然后所以死信消息就自己跑到死信队列里去了。而是为每个需要使用死信的业务队列配置一个死信交换机,这里同一个项目的死信交换机可以共用一个,然后为每个业务队列分配一个单独的路由key。
有了死信交换机和路由key后,接下来,就像配置业务队列一样,配置死信队列,然后绑定在死信交换机上。也就是说,死信队列并不是什么特殊的队列,只不过是绑定在死信交换机上的队列。死信交换机也不是什么特殊的交换机,只不过是用来接受死信的交换机,所以可以为任何类型【Direct、Fanout、Topic】。一般来说,会为每个业务队列分配一个独有的路由key,并对应的配置一个死信队列进行监听,也就是说,一般会为每个重要的业务队列配置一个死信队列。
有了前文这些陈述后,接下来就是惊险刺激的实战环节,这里省略了RabbitMQ环境的部署和搭建环节
2.3 死信队列的消息变化
那么“死信”被丢到死信队列中后,会发生什么变化呢?
如果队列配置了参数 x-dead-letter-routing-key 的话,“死信”的路由key将会被替换成该参数对应的值。如果没有设置,则保留该消息原有的路由key。
举个栗子:
如果原有消息的路由key是testA,被发送到业务Exchage中,然后被投递到业务队列QueueA中,如果该队列没有配置参数x-dead-letter-routing-key,则该消息成为死信后,将保留原有的路由keytestA,如果配置了该参数,并且值设置为testB,那么该消息成为死信后,路由key将会被替换为testB,然后被抛到死信交换机中。
另外,由于被抛到了死信交换机,所以消息的Exchange Name也会被替换为死信交换机的名称。
消息的Header中,也会添加很多奇奇怪怪的字段,修改一下上面的代码,在死信队列的消费者中添加一行日志输出:
log.info(“死信消息properties:{}”, message.getMessageProperties());
然后重新运行一次,即可得到死信消息Header中被添加的信息:
死信消息:
properties:MessageProperties [headers={x-first-death-exchange=dead.letter.demo.simple.business.exchange, x-death=[{reason=rejected, count=1, exchange=dead.letter.demo.simple.business.exchange, time=Sun Jul 14 16:48:16 CST 2019, routing-keys=[], queue=dead.letter.demo.simple.business.queuea}], x-first-death-reason=rejected, x-first-death-queue=dead.letter.demo.simple.business.queuea}, correlationId=1, replyTo=amq.rabbitmq.reply-to.g2dkABZyYWJiaXRAREVTS1RPUC1DUlZGUzBOAAAPQAAAAAAB.bLbsdR1DnuRSwiKKmtdOGw==, contentType=text/plain, contentEncoding=UTF-8, contentLength=0, receivedDeliveryMode=PERSISTENT, priority=0, redelivered=false, receivedExchange=dead.letter.demo.simple.deadletter.exchange, receivedRoutingKey=dead.letter.demo.simple.deadletter.queuea.routingkey, deliveryTag=1, consumerTag=amq.ctag-NSp18SUPoCNvQcoYoS2lPg, consumerQueue=dead.letter.demo.simple.deadletter.queuea]
Header中看起来有很多信息,实际上并不多,只是值比较长而已。下面就简单说明一下Header中的值:
字段名 含义
x-first-death-exchange 第一次被抛入的死信交换机的名称
x-first-death-reason 第一次成为死信的原因,rejected:消息在重新进入队列时被队列拒绝,由于default-requeue-rejected 参数被设置为false。expired :消息过期。maxlen : 队列内消息数量超过队列最大容量
x-first-death-queue 第一次成为死信前所在队列名称
x-death 历次被投入死信交换机的信息列表,同一个消息每次进入一个死信交换机,这个数组的信息就会被更新
2.4死信队列使用场景
通过上面的信息,我们已经知道如何使用死信队列了,那么死信队列一般在什么场景下使用呢?
一般用在较为重要的业务队列中,确保未被正确消费的消息不被丢弃,一般发生消费异常可能原因主要有由于消息信息本身存在错误导致处理异常,处理过程中参数校验异常,或者因网络波动导致的查询异常等等,当发生异常时,当然不能每次通过日志来获取原消息,然后让运维帮忙重新投递消息(没错,以前就是这么干的= =)。通过配置死信队列,可以让未正确处理的消息暂存到另一个队列中,待后续排查清楚问题后,编写相应的处理代码来处理死信消息,这样比手工恢复数据要好太多了。
死信队列其实并没有什么神秘的地方,不过是绑定在死信交换机上的普通队列,而死信交换机也只是一个普通的交换机,不过是用来专门处理死信的交换机。
总结一下死信消息的生命周期:
业务消息被投入业务队列
消费者消费业务队列的消息,由于处理过程中发生异常,于是进行了nck或者reject操作
被nck或reject的消息由RabbitMQ投递到死信交换机中
死信交换机将消息投入相应的死信队列
死信队列的消费者消费死信消息
死信消息是RabbitMQ为我们做的一层保证,其实我们也可以不使用死信队列,而是在消息消费异常时,将消息主动投递到另一个交换机中,当你明白了这些之后,这些Exchange和Queue想怎样配合就能怎么配合。比如从死信队列拉取消息,然后发送邮件、短信、钉钉通知来通知开发人员关注。或者将消息重新投递到一个队列然后设置过期时间,来进行延时消费