消息队列MQ
概念
- 消息队列(Message Queue,简称MQ),从字面意思上看,本质是个队列,FIFO先入先出,只不过队列中存放的内容是message而已。
- MQ是消费-生产者模型的一个典型的代表,一端往消息队列中不断写入消息,而另一端则可以读取队列中的消息。这样发布者和使用者都不用知道对方的存在。
用途
- 不同进程Process / 线程Thread之间通信。
作用
生产者消费者模式
- 生产者消费者模式是通过一个容器来解决生产者和消费者的强耦合问题。生产者和消费者彼此之间不直接通讯,而通过阻塞队列来进行通讯,所以生产者生产完数据之后不用等待消费者处理,直接扔给阻塞队列,消费者不找生产者要数据,而是直接从阻塞队列里取,阻塞队列就相当于一个缓冲区,平衡了生产者和消费者的处理能力
MQ
- 消息队列中间件是分布式系统中重要的组件,主要解决应用解耦,异步消息,流量削锋等问题,实现高性能,高可用,可伸缩和最终一致性架构。
- 不同进程(process)之间传递消息时,两个进程之间耦合程度过高,改动一个进程,引发必须修改另一个进程。为了隔离这两个进程,在两进程间抽离出一层(一个模块),所有两进程之间传递的消息,都必须通过消息队列来传递,单独修改某一个进程,不会影响另一个;
- 不同进程(process)之间传递消息时,为了实现标准化,将消息的格式规范化了,并且,某一个进程接受的消息太多,一下子无法处理完,并且也有先后顺序,必须对收到的消息进行排队,因此诞生了事实上的消息队列;
- 关于消息队列的详细介绍请参阅:
类型
- 目前使用较多的消息队列有ActiveMQ,RabbitMQ,ZeroMQ,Kafka,MetaMQ,RocketMQ。
RabbitMQ
概念
- rabbitMQ是一款基于AMQP协议的消息中间件,它能够在应用之间提供可靠的消息传输。在易用性,扩展性,高可用性上表现优秀。使用消息中间件利于应用之间的解耦,生产者(客户端)无需知道消费者(服务端)的存在。而且两端可以使用不同的语言编写,大大提供了灵活性。
特点
- RabbitMQ是一个由Erlang开发、开源的、在AMQP基础上完整的、可复用的企业消息系统。
- Erlang -- 面向并发的编程语言
- AMQP -- 消息队列的协议
- 支持主流的操作系统,Linux、Windows、MacOX等。
- 多种开发语言支持,Java、Python、Ruby、.NET、PHP、C/C++、node.js等
- 所有队列、数据默认均存储在内存中!断电消失
使用存储库安装
RabbitMQ必须要在Erlang的基础上才能下载使用
添加存储库条目
要将Erlang Solutions存储库(包括用于apt-secure的公共密钥)添加到您的系统,请调用以下命令:
wget https://packages.erlang-solutions.com/erlang-solutions_2.0_all.deb sudo dpkg -i erlang-solutions_2.0_all.deb
安装Erlang
刷新存储库缓存并安装“ erlang”软件包:
sudo apt-get update sudo apt-get install erlang
或“ esl-erlang”软件包:
sudo apt-get update sudo apt-get install esl-erlang
区别:
- “esl-erlang”软件包是一个包含完整安装的文件:它包含Erlang / OTP平台及其所有应用程序。
- “erlang”软件包是许多较小软件包的前端,足够我们使用。
-
GitHub资源:Ubuntu
安装
在Ubuntu上双击打开然后点击下载,就算安装完成了
操作指令
启动、停止、重启
service rabbitmq-server start
service rabbitmq-server stop
service rabbitmq-server restart
设置开机启动
chkconfig rabbitmq-server on
下载pika模块
pip install pika
python中通过pika模块实现与RabbitMQ软件的对接,并可以对其进行操作(类似于python通过pymysql模块来与数据库连接,并进行各种操作)
简单模式
生产者producer
import pika # python中用来连接rabbitmq应用的模块,类似于pymysql的作用 # 建立连接,并拿到操作对象 connection = pika.BlockingConnection(pika.ConnectionParameters('localhost')) channel = connection.channel() # 创建名为'hello'的队列 channel.queue_declare(queue='hello') # 向指定'hello'队列中添加数据'Hello nb!' channel.basic_publish(exchange='', # 这个属性是交换机的名字,这里使用的不是交换机,所以为空 routing_key='hello', # 我们使用的是hello队列作为中介进行数据交换 body='Hello nb!') # 数据内容 # 提示代码执行完毕 print(" [x] Sent 'Hello World!'")
消费者customer
import pika # 建立连接,并拿到操作对象 connection = pika.BlockingConnection(pika.ConnectionParameters('localhost')) channel = connection.channel() # 为什么生产者已经创建过队列了,这里还要创建队列呢?因为你并不知道消费者、生产者的客户端谁会先运行! # 想想,如果你消费者只是写上了接收数据的代码,而此时这个队列还没创建(生产者客户端还没运行),那么你去监听谁呢?他找不到监听对象,是不是就直接报错了。所以两个客户端都要创建,记住是同名的,因为我们并不是要两个,而是同一个 channel.queue_declare(queue='hello') # 回调函数,这里能拿到你要监听的数据,你可以进行逻辑操作,以及对数据进行保存 def callback(ch, method, properties, body): print(" [x] Received %r" % body) # 监听配置,注意这里只是配置,并没有开始监听 channel.basic_consume(queue='hello', # 监听队列的名称 auto_ack=True, # 自动应答,调用回调函数的同时自动回复队列, on_message_callback=callback) # 回调函数,队列中有新数据产生后自动调用 print(' [*] Waiting for messages. To exit press CTRL+C') channel.start_consuming() # 开始监听! # 要注意,这一步是持续拿数据,如果队列数据为空,则阻塞等待
应答参数
自动应答
- 概念:
- 默认情况(即
auto_ack=True
自动应答)下,当消费者拿到队列中的数据后,会自动向队列发送一个应答,表示我已经拿到数据了,你那边可以把数据删除掉了,于是,这条数据就从内存中删除掉了。
- 默认情况(即
- 但是自动应答模式有一种bug:
- 当消费者拿到队列数据,并调用回调函数时,回调函数内部出现错误,导致程序运行终止,数据被清空(内存,临时存储),而此时队列已经收到消费者的自动应答,把刚才那条数据删除掉了,这时,这条数据就永久丢失了。
- 表现在实际生活中就是,你在饿了么下了一单外卖,应用提示你下单成功,但是你没有收到外卖,然后你就联系客服,最后发现外卖小哥、饭店肯定有个人没收到你的订单信息,因为他们的客户端可能报错了
- 如何避免这种情况呢?
- 改变应答模式为:主动应答
主动应答
概念:
- 当消费者拿到数据后,我不要他自动应答,我将主动应答语句放到回调函数的最后,即我的逻辑操作、数据保存全部处理完后,再跟队列说,我拿到数据了,你可以删除数据了(其实你早已经拿到数据,并且操作都执行完了,但是没办法,谁让他不智能呢,这么操作可以保证我们的数据被保存后我们再将源数据进行删除)
要注意:
- 这种情况下,当你的回调函数内部出现bug时,你的进程被终止,没有发送应答,队列会在等待一段时间未果后,将这条数据发送给下一个消费者
如何操作:
def callback(ch, method, properties, body): print(" [x] Received %r" % body) ch.basic_ack(delivery_tag=method.delivery_tag) # 主动应答语句 channel.basic_consume(queue='hello', auto_ack=False, # 关闭自动应答 on_message_callback=callback)
简单总结
- 自动应答机制 --> 只要调用callback,就自动应答,标志取走数据了,队列删除数据;
- 手动应答 --> 取消自动应答,把应答语句加到callback函数最下面,直到函数代码运行到我的应答语句为止,才标志取走数据,这样可以避免回调函数出现bug数据没保存,结果数据池还把数据丢弃了
- 其实两者的区别就是:
- 应答时间的不同,一个是跟调用函数同时,另一个是在函数内部(逻辑操作、数据保存之后)应答
分发参数
情景
假设一种情况:两个消费者同时监听一个队列,其中一个线程sleep(7)秒,另一个消费者线程sleep(3)秒
轮询分发
- 轮询分发(round-robin)是默认模式,轮询分发模式下两个消费者处理的消息是一样多的,跟个人效率无关,不管谁快,都不会多给消息,总是你一个我一个。
- 想要实现这种情景,要先将下面的“双开”配置好,并且处于自动应答模式,然后两个进程都处于阻塞状态,这时生产者插入数据,就可以看到两个平均分配数据,一人一个,但是3s的很快就执行完了,7s的很久才好。且由于7s的文件先执行,所以它对应的执行结果是
0 2 4 6
,3s的结果是1 3 5 7
- 如果上述情况是主动应答,结果一样,必须一人一个,不管你有多慢,且7s的2还没执行完,3s的1,3执行完后直接执行5,还给他预留了4
- 结论:只要没配置公平分发的代码,无论你怎么搞都是一人一个
- 想要实现这种情景,要先将下面的“双开”配置好,并且处于自动应答模式,然后两个进程都处于阻塞状态,这时生产者插入数据,就可以看到两个平均分配数据,一人一个,但是3s的很快就执行完了,7s的很久才好。且由于7s的文件先执行,所以它对应的执行结果是
公平分发
公平分发(fair dispatch)前提条件必须关闭自动应答ack,改成手动应答。公平是谁快谁拿,只要谁有空,谁就拿数据。
- 公平模式必须建立在手动应答基础上的原因:自动应答不会考虑函数的执行时间,调用的同时就说我拿到数据了,再给我一个;而手动应答才可以跟函数执行时间挂钩,执行完一个才可以再要一个
- 代码:
channel.basic_qos(prefetch_count=1)
,放置在全局即可,位置只要在channel定义完了之后就行
import time import pika connection = pika.BlockingConnection(pika.ConnectionParameters('localhost')) channel = connection.channel() channel.queue_declare(queue='hello') def callback(ch, method, properties, body): time.sleep(7) # 这个文件执行过后,然后改成3再执行,不过需要配置一下pycharm,看代码框下面 print(" [x] Received %r" % body) ch.basic_ack(delivery_tag=method.delivery_tag) # 1.主动应答语句 # 3.将轮询分配改为公平分配 channel.basic_qos(prefetch_count=1) channel.basic_consume(queue='hello', auto_ack=False, # 2.主动应答 on_message_callback=callback) print(' [*] Waiting for messages. To exit press CTRL+C') channel.start_consuming() # 开始监听!
生产者
import pika connection = pika.BlockingConnection(pika.ConnectionParameters('localhost')) channel = connection.channel() channel.queue_declare(queue='hello') # 循环添加数据 for i in range(8): channel.basic_publish(exchange='', routing_key='hello', body=f'Hello {i}!') print(" [x] Sent 'Hello World!'")
执行结果
7s: [x] Received b'Hello 0!' [x] Received b'Hello 4!' [x] Received b'Hello 7!' 3s: # 与时间比成反比,合理 [x] Received b'Hello 1!' [x] Received b'Hello 2!' [x] Received b'Hello 3!' [x] Received b'Hello 5!' [x] Received b'Hello 6!'
有一个知识点,就是单个文件多个执行:
- 按照下图配置,然后就可以在7s基础上第一次运行customer文件,然后将源文件中的7改成3,再次运行同一个文件,但是是两个进程
持久化参数
上面我们提到过,RabbitMQ中的数据是存储在内存上的,断电消失,但是我们必然有一些重要数据是想存储起来的,或者不想因为断电而损失的,这就引出了持久化存储的方法。
# 声明queue,若声明过,则换一个名字 channel.queue_declare(queue='hello2', durable=True) # durable=True声明这个队列可以持久化存储数据,但不是每条数据都会持久化存储。只有创建的时候这么声明了,这个队列内部添加的数据才有资格标记自己为持久化存储 channel.basic_publish(exchange='', routing_key='hello2', body='Hello World!', properties=pika.BasicProperties( # 这就是标记数据持久化存储的配置 delivery_mode=2, # make message persistent ) )
交换机
结构:一个生产者 <--> 一个交换机 <--> 多个消息队列 <--> 多个消费者(要注意:消费者与队列可一对一,也可多对一)
关键结论:同一个消息通过交换机被多个消息队列获取,而每个消息队列可以有多个消费者实例,但只有其中一个消费者实例会获取到消息。
交换机之发布订阅
类型:fanout
发布订阅和简单模式消息队列区别在于,发布订阅会将消息发送给所有的订阅者,而消息队列中的数据被消费一次便消失。所以,RabbitMQ实现发布和订阅时,会为每一个订阅者创建一个队列,而发布者发布消息时,交换机会将消息放置在所有相关队列中。
这里生产者的数据就交给交换机,而不是之前的对列了,但是消费者还是直接从队列中拿数据
代码:
生产者:
import pika connection = pika.BlockingConnection(pika.ConnectionParameters( host='localhost')) channel = connection.channel() # 这里生产者的数据就交给交换机,而不是之前的对列了,所以声明的是交换机对象 channel.exchange_declare(exchange='logs', # 交换机name exchange_type='fanout') # 交换机类型 message = "info: Hello World!" channel.basic_publish(exchange='logs', # 由于数据交给的是交换机,所以这里参数就要选择交换机,而不是下面的参数 routing_key='', # 上面声明了交换机之后,这里routing_key就是交换机中关键词的参数,后面再说 body=message) print(" [x] Sent %r" % message) connection.close()
消费者:
- 这里就不能跟之前一样同一个文件运行两次了,因为内容没有变化。只能再复制一份,运行两个文件进行监听
import pika connection = pika.BlockingConnection(pika.ConnectionParameters(host='localhost')) channel = connection.channel() # 看到这里聪明的孩子就会问了,为什么消费者跟队列进行交互,反而声明交换机呢? # 你的理解没问题,但是还是之前的问题,你怎么知道生产者、消费者哪个客户端会先运行呢? # 如果是消费者,那你不声明交换机的话,你监听的队列绑定交换机的时候会不会报错呢 channel.exchange_declare(exchange='logs', # 这里跟生产者一样就行 exchange_type='fanout') # 这里跟生产者一样就行 # 创建名为随机字符串的队列,并且拿到创建出来的队列对象 result = channel.queue_declare("", exclusive=True) # 得到其随即创建的名字,后面绑定交换机,监听要用 queue_name = result.method.queue # 绑定队列与交换机 channel.queue_bind(exchange='logs', queue=queue_name) print(' [*] Waiting for logs. To exit press CTRL+C') # 下面的配置上面都说过了,按照需求配置就行 def callback(ch, method, properties, body): print(" [x] %r" % body) channel.basic_consume(queue=queue_name, auto_ack=True, on_message_callback=callback) channel.start_consuming()
交换机之关键字
类型:direct
感觉是在订阅的基础上延伸出来的,通过使用info / warning / error三个词汇进行关键字匹配。
这样的话就不是所有绑定交换机的队列我都推送,而是每个队列都有自己的关键字,而每个数据也有关键词,只有两者互相匹配时,才会给匹配上的队列推送。
代码:
生产者:
import pika connection = pika.BlockingConnection(pika.ConnectionParameters( host='localhost')) channel = connection.channel() channel.exchange_declare(exchange='logs2', exchange_type='direct') # 交换机类型 message = "info: Hello gx!" # 这里面的info与关键次无任何关系,只是做个提示 channel.basic_publish(exchange='logs2', routing_key='warning', # 本次数据的关键词,一个数据只能有一个关键词 body=message) print(" [x] Sent %r" % message) connection.close()
消费者:
import pika import sys connection = pika.BlockingConnection(pika.ConnectionParameters( host='localhost')) channel = connection.channel() channel.exchange_declare(exchange='logs2', exchange_type='direct') 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_bind只能绑定一个交换机关键字 # channel.queue_bind(exchange='logs2', # queue=queue_name, # routing_key=severity) # 将队列标记为某种标签,并绑定到某个交换机上,可以接收对应的关键字 # 一个队列可以绑定多个关键字,但是一次queue_bind只能绑定一个关键字,cv多个绑定或者for+format循环绑定 channel.queue_bind(exchange='logs2', queue=queue_name, routing_key="info" ) print(' [*] Waiting for logs. To exit press CTRL+C') def callback(ch, method, properties, body): print(" [x] %r" % body) channel.basic_consume(queue=queue_name, auto_ack=True, on_message_callback=callback) channel.start_consuming()
总结:
- 一条数据只能有一个关键字;
- 一个队列可以绑定多个关键字,但是一次queue_bind只能绑定一个关键字,cv多个绑定或者for+format循环绑定
交换机之通配符
- 类型:topic
- 不再使用固定的三个词汇进行匹配,而是类似于正则的模糊匹配,其中,'#'可以匹配多个词,'*'只能匹配一个词。
- 用法:队列的routing_key参数值为:'#.weather','europe.#' --> 都可以匹配到数据的关键字:'europe.weather'
代码:
生产者:
import pika connection = pika.BlockingConnection(pika.ConnectionParameters( host='localhost')) channel = connection.channel() channel.exchange_declare(exchange='logs3', exchange_type='topic') # 交换机类型 message = "info: Hello ERU!" channel.basic_publish(exchange='logs3', routing_key='europe.weather', # 数据关键字 body=message) print(" [x] Sent %r" % message) connection.close()
消费者:
import pika import sys connection = pika.BlockingConnection(pika.ConnectionParameters( host='localhost')) channel = connection.channel() channel.exchange_declare(exchange='logs3', exchange_type='topic') 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_bind只能绑定一个交换机关键字 # channel.queue_bind(exchange='logs2', # queue=queue_name, # routing_key=severity) # 将队列绑定到某个交换机上 channel.queue_bind(exchange='logs3', queue=queue_name, routing_key="#.weather" ) # 另一个文件routing_key="europe.#",两者同时监听、同时能匹配到数据 print(' [*] Waiting for logs. To exit press CTRL+C') def callback(ch, method, properties, body): print(" [x] %r" % body) channel.basic_consume(queue=queue_name, auto_ack=True, on_message_callback=callback) channel.start_consuming()