消息队列
引子
队列的作用:
存储消息、数据
保证消息的顺序
保证数据的正确交付
python中的队列:
- threading Queue: 用于多个线程之间进行数据同步和交互
from threading import Thread
from queue import Queue
def produce(q):
q.put('hello, consume1')
q.put('hello,consume2')
def consume1(q):
print(q.get())
def consume2(q):
print(q.get())
if __name__ == '__main__':
q = Queue()
t = Thread(target=produce, args=(q,))
t.start()
t1 = Thread(target=consume1, args=(q,))
t2 = Thread(target=consume2, args=(q, ))
t1.start()
t2.start()
- 进程 Queue: 父进程和子进程进行交互, 或者同属于同一父进程下的多个子进程进行交互
from multiprocessing import Queue,Process
from queue import Queue
def produce(q):
q.put('hello')
def consume(q):
print(q.get())
if __name__ == '__main__':
q = Queue()
p = Process(target=produce, args=(q,))
p.start()
c = Process(target=consume, args=(q,))
c.start()
不管是多进程之间通信也好, 多线程之间通信也好, 都无法实现不同进程之间的通信
所以有一种软件来做代理, 与所有需要通信的软件建立socket连接, 然后再转发到响应的软件
MQ的介绍
MQ
全称为Message Queue
, 消息队列(MQ
)是一种应用程序对应用程序的通信方法。MQ
是消费-生产者模型的一个典型的代表,一端往消息队列中不断写入消息,而另一端则可以读取队列中的消息。消息发布者只管把消息发布到 MQ 中而不用管谁来取,消息使用者只管从 MQ 中取消息而不管是谁发布的。这样发布者和使用者都不用知道对方的存在。
你可以想想在生活中的一种场景:当你把信件的投进邮筒,邮递员肯定最终会将信件送给收件人。我们可以把MQ比作 邮局和邮递员。
MQ和邮局的主要区别是,它不处理消息,但是,它会接受数据、存储消息数据、转发消息
为什么要用MQ
以常见的订单系统为例,用户点击【下单】按钮之后的业务逻辑可能包括:扣减库存、生成相应单据、发红包、发短信通知。在业务发展初期这些逻辑可能放在一起同步执行,随着业务的发展订单量增长,需要提升系统服务的性能,这时可以将一些不需要立即生效的操作拆分出来异步执行,比如发放红包、发短信通知等。这种场景下就可以用 MQ ,在下单的主流程(比如扣减库存、生成相应单据)完成之后发送一条消息到 MQ 让主流程快速完结,而由另外的单独线程拉取MQ的消息(或者由 MQ 推送消息),当发现 MQ 中有发红包或发短信之类的消息时,执行相应的业务逻辑
RabbitMQ
了解RabbitMQ
队列,生产者,消费者
队列是RabbitMQ的内部对象,用于存储消息。生产者(下图中的P)生产消息并投递到队列中,消费者(下图中的C)可以从队列中获取消息并消费
多个消费者可以订阅同一个队列,这时队列中的消息会被平均分摊给多个消费者进行处理,而不是每个消费者都收到所有的消息并处理。
概念
RabbitMQ 是一个由 Erlang 语言开发的 AMQP 的开源实现。
AMQP :Advanced Message Queue,高级消息队列协议。它是应用层协议的一个开放标准,为面向消息的中间件设计,基于此协议的客户端与消息中间件可传递消息,并不受产品、开发语言等条件的限制。
RabbitMQ 最初起源于金融系统,用于在分布式系统中存储转发消息,在易用性、扩展性、高可用性等方面表现不俗。具体特点包括:
可靠性(Reliability):RabbitMQ 使用一些机制来保证可靠性,如持久化、传输确认、发布 确认。
灵活的路由(Flexible Routing):在消息进入队列之前,通过 Exchange 来路由消息的。对 于典型的路由功能,RabbitMQ 已经提供了一些内置的 Exchange 来实现。针对更复杂的路由功能,可以将多个 Exchange 绑定在一起,也通过插件机制实现自己的 Exchange 。
消息集群(Clustering):多个 RabbitMQ 服务器可以组成一个集群,形成一个逻辑 Broker。
高可用(Highly Available Queues):队列可以在集群中的机器上进行镜像,使得在部分节 点出问题的情况下队列仍然可用。
多种协议(Multi-protocol):RabbitMQ 支持多种消息队列协议,比如 STOMP、MQTT等等。
多语言客户端(Many Clients):RabbitMQ 几乎支持所有常用语言,比如 Java、.NET、 Ruby 等等。
管理界面(Management UI):RabbitMQ 提供了一个易用的用户界面,使得用户可以监控 和管理消息 Broker 的许多方面。
跟踪机制(Tracing):如果消息异常,RabbitMQ 提供了消息跟踪机制,使用者可以找出发生 了什么。
插件机制(Plugin System):RabbitMQ 提供了许多插件,来从多方面进行扩展,也可以编 写自己的插件。
RabbitMQ架构
使用场景
在我们秒杀抢购商品的时候,系统会提醒我们稍等排队中,而不是像几年前一样页面卡死或报错给用户。
像这种排队结算就用到了消息队列机制,放入通道里面一个一个结算处理,而不是某个时间断突然涌入大批量的查询新增把数据库给搞宕机,所以RabbitMQ本质上起到的作用就是削峰填谷,为业务保驾护航
为什么选择RabbitMQ
现在的市面上有很多MQ可以选择,比如ActiveMQ、ZeroMQ、Appche Qpid,那问题来了为什么要选择RabbitMQ?
除了Qpid,RabbitMQ是唯一一个实现了AMQP标准的消息服务器;
可靠性,RabbitMQ的持久化支持,保证了消息的稳定性;
高并发,RabbitMQ使用了Erlang开发语言,Erlang是为电话交换机开发的语言,天生自带高并发光环,和高可用特性;
集群部署简单,正是应为Erlang使得RabbitMQ集群部署变的超级简单;
社区活跃度高,根据网上资料来看,RabbitMQ也是首选
工作机制
生产者、消费者和代理
在了解消息通讯之前首先要了解3个概念:生产者、消费者和代理。
生产者:消息的创建者,负责创建和推送数据到消息服务器;
消费者:消息的接收方,用于处理数据和确认消息;
代理:就是RabbitMQ本身,用于扮演“快递”的角色,本身不生产消息,只是扮演“快递”的角色。
消息发送原理
首先你必须连接到Rabbit才能发布和消费消息,那怎么连接和发送消息的呢?
你的应用程序和Rabbit Server之间会创建一个TCP连接,一旦TCP打开,并通过了认证,认证就是你试图连接Rabbit之前发送的Rabbit服务器连接信息和用户名和密码,有点像程序连接数据库,使用Java有两种连接认证的方式,后面代码会详细介绍,一旦认证通过你的应用程序和Rabbit就创建了一条AMQP信道(Channel)。
信道是创建在“真实”TCP上的虚拟连接,AMQP命令都是通过信道发送出去的,每个信道都会有一个唯一的ID,不论是发布消息,订阅队列或者介绍消息都是通过信道完成的
为什么不通过TCP直接发送命令?
对于操作系统来说创建和销毁TCP会话是非常昂贵的开销,假设高峰期每秒有成千上万条连接,每个连接都要创建一条TCP会话,这就造成了TCP连接的巨大浪费,而且操作系统每秒能创建的TCP也是有限的,因此很快就会遇到系统瓶颈。
如果我们每个请求都使用一条TCP连接,既满足了性能的需要,又能确保每个连接的私密性,这就是引入信道概念的原因
RabbitMQ 必须了解的名词
ConnectionFactory(连接管理器):应用程序与Rabbit之间建立连接的管理器,程序代码中使用
Channel(信道):消息推送使用的通道
Exchange(交换器):用于接受、分配消息
Queue(队列):用于存储生产者的消息
RoutingKey(路由键):用于把生成者的数据分配到交换器上
BindingKey(绑定键):用于把交换器的消息绑定到队列上
持久化
消息持久化
Rabbit队列和交换器有一个不可告人的秘密,就是默认情况下重启服务器会导致消息丢失,那么怎么保证Rabbit在重启的时候不丢失呢?答案就是消息持久化。
当你把消息发送到Rabbit服务器的时候,你需要选择你是否要进行持久化,但这并不能保证Rabbit能从崩溃中恢复,想要Rabbit消息能恢复必须满足3个条件:
投递消息的时候durable设置为true,消息持久化,代码:channel.queueDeclare(x, true, false, false, null),参数2设置为true持久化;
设置投递模式deliveryMode设置为2(持久),代码:channel.basicPublish(x, x, MessageProperties.PERSISTENT_TEXT_PLAIN,x),参数3设置为存储纯文本到磁盘;
消息已经到达持久化交换器上;
消息已经到达持久化的队列;
持久化工作原理
Rabbit会将你的持久化消息写入磁盘上的持久化日志文件,等消息被消费之后,Rabbit会把这条消息标识为等待垃圾回收。
持久化的缺点
消息持久化的优点显而易见,但缺点也很明显,那就是性能,因为要写入硬盘要比写入内存性能较低很多,从而降低了服务器的吞吐量,尽管使用SSD硬盘可以使事情得到缓解,但他仍然吸干了Rabbit的性能,当消息成千上万条要写入磁盘的时候,性能是很低的。
所以使用者要根据自己的情况,选择适合自己的方式。
怎么保证 MQ 消息不丢失?
使用了 MQ 之后,还要关心消息丢失的问题。这里我们挑 RabbitMQ 来说明一下吧。
生产者弄丢了数据
RabbitMQ 生产者将数据发送到 RabbitMQ 的时候,可能数据在网络传输中搞丢了,这个时候 RabbitMQ 收不到消息,消息就丢了。
RabbitMQ 提供了两种方式来解决这个问题:
事务方式:在生产者发送消息之前,通过channel.txSelect
开启一个事务,接着发送消息。
如果消息没有成功被 RabbitMQ 接收到,生产者会收到异常,此时就可以进行事务回滚channel.txRollback
,然后重新发送。假如 RabbitMQ 收到了这个消息,就可以提交事务channel.txCommit
。
但是这样一来,生产者的吞吐量和性能都会降低很多,现在一般不这么干。
另外一种方式就是通过 Confirm 机制:这个 Confirm 模式是在生产者那里设置的,就是每次写消息的时候会分配一个唯一的 ID,然后 RabbitMQ 收到之后会回传一个 ACK,告诉生产者这个消息 OK 了。
如果 RabbitMQ 没有处理到这个消息,那么就回调一个 Nack 的接口,这个时候生产者就可以重发。
事务机制和 Confirm 机制最大的不同在于事务机制是同步的,提交一个事务之后会阻塞在那儿。
但是 Confirm 机制是异步的,发送一个消息之后就可以发送下一个消息,然后那个消息 RabbitMQ 接收了之后会异步回调你一个接口通知你这个消息接收到了。
所以一般在生产者这块避免数据丢失,都是用 Confirm 机制的。
RabbitMQ 弄丢了数据
RabbitMQ 集群也会弄丢消息,这个问题在官方文档的教程中也提到过,就是说在消息发送到 RabbitMQ 之后,默认是没有落地磁盘的,万一 RabbitMQ 宕机了,这个时候消息就丢失了。
所以为了解决这个问题,RabbitMQ 提供了一个持久化的机制,消息写入之后会持久化到磁盘。
这样哪怕是宕机了,恢复之后也会自动恢复之前存储的数据,这样的机制可以确保消息不会丢失。
设置持久化有两个步骤:
第一个是创建 Queue 的时候将其设置为持久化的,这样就可以保证 RabbitMQ 持久化 Queue 的元数据,但是不会持久化 Queue 里的数据。
第二个是发送消息的时候将消息的 deliveryMode 设置为 2,就是将消息设置为持久化的,此时 RabbitMQ 就会将消息持久化到磁盘上去。
但是这样一来可能会有人说:万一消息发送到 RabbitMQ 之后,还没来得及持久化到磁盘就挂掉了,数据也丢失了,怎么办?
对于这个问题,其实是配合上面的 Confirm 机制一起来保证的,就是在消息持久化到磁盘之后才会给生产者发送 ACK 消息。
万一真的遇到了那种极端的情况,生产者是可以感知到的,此时生产者可以通过重试发送消息给别的 RabbitMQ 节点。
消费端弄丢了数据
RabbitMQ 消费端弄丢了数据的情况是这样的:在消费消息的时候,刚拿到消息,结果进程挂了,这个时候 RabbitMQ 就会认为你已经消费成功了,这条数据就丢了。
对于这个问题,要先说明一下 RabbitMQ 消费消息的机制:在消费者收到消息的时候,会发送一个 ACK 给 RabbitMQ,告诉 RabbitMQ 这条消息被消费到了,这样 RabbitMQ 就会把消息删除。
但是默认情况下这个发送 ACK 的操作是自动提交的,也就是说消费者一收到这个消息就会自动返回 ACK 给 RabbitMQ,所以会出现丢消息的问题。
所以针对这个问题的解决方案就是:关闭 RabbitMQ 消费者的自动提交 ACK,在消费者处理完这条消息之后再手动提交 ACK。
这样即使遇到了上面的情况,RabbitMQ 也不会把这条消息删除,会在你程序重启之后,重新下发这条消息过来。
安装
mac
https://www.rabbitmq.com/install-homebrew.html
在Mac下安装RabbitMQ一般默认RabbitMQ服务器依赖的Erlang已经安装,只需要用下面两个命令就可以完成RabbitMQ的安装(前提是homebrew已经被安装)
$ brew update
$ brew install rabbitmq
>>>> 加入环境变量
$ vim ~.bash_profile
export PATH=$PATH:/usr/local/opt/rabbitmq/sbin
$ python3 -m pip install pika --upgrade
>>>> 启动服务
$ rabbitmq-server
## ##
## ## RabbitMQ 3.7.16. Copyright (C) 2007-2019 Pivotal Software, Inc.
########## Licensed under the MPL. See https://www.rabbitmq.com/
###### ##
########## Logs: /usr/local/var/log/rabbitmq/[email protected]
/usr/local/var/log/rabbitmq/rabbit@localhost_upgrade.log
Starting broker...
completed with 6 plugins.
输入URL http://localhost:15672/
登录web界面, 用户名和密码输入 guest
windows
整体思路
-
- 安装
Erlang
- 安装
-
- 安装
RabbitMQ
- 安装
-
- 激活
RabbitMQ's Management Plugin
可视化插件
- 激活
具体步骤
安装Erlang
- 1.1 下载地址: https://www.erlang.org/downloads, 选择
OTP 22.0 Windows 64-bit Binary File
(注: 官网下载太慢, 可以通过网盘下载: 链接) - 1.2 运行
otp_win64_22.0.exe
, 安装Erlang - 1.3 设置环境变量
ERLANG_HOME
:此电脑 -> 右键属性 -> 高级系统设置 -> 高级 -> 环境变量 -> 系统变量 -> 新建ERLANG_HOME
, 变量名:ERLANG_HOME
, 变量值:D:\erlang\erl10.4
(注:D:\erlang\erl10.4
即安装目录) - 1.4 修改环境变量
Path
: 末尾追加:;%ERLANG_HOME%\bin
, 保存
(注: 分号视情况而定, 若原Path
末尾有分号则不加, 无则加) - 1.5 打开命令行, 输入
erl
, 提示版本信息:Eshell V10.4 (abort with ^G)
, 说明Erlang安装 成功
安装RabbitMQ
2.1 下载地址: https://www.rabbitmq.com/install-windows.html, 选择
rabbitmq-server-3.7.15.exe
2.2 运行
rabbitmq-server-3.7.15.exe
, 安装RabbitMQ2.3 设置环境变量, 同上, 新建
RABBITMQ_SERVER
, 变量名:RABBITMQ_SERVER
, 变量值:D:\rabbitmq\rabbitmq_server-3.7.15
2.4 修改
Path
: 追加:;%RABBITMQ_SERVER%\sbin
2.5 命令行输入:
rabbitmqctl status
, 出现
说明: RabbitMQ
已安装成功, 且已启动, 但此时访问http://localhost:15672/发现无法访问, 因为还未激活管理插件
激活RabbitMQ's Management Plugin可视化插件
3.1 进入到RabbitMQ安装目录, 我的目录是:
D:\rabbitmq\rabbitmq_server-3.7.15
, 命令行先切换到D盘:d:
, 然后cd rabbitmq\rabbitmq_server-3.7.15\sbin
, 运行命令:rabbitmq-plugins.bat enable rabbitmq_management
, 出现说明插件安装成功
3.2 验证: 浏览器访问: http://localhost:15672/
用户名 密码均为: guest
, 登录
至此, windows安装RabbitMQ已全部完成
linux
安装思路
1. 安装erlang
2. 安装socat
3. 安装rabbitmq
4. 相关配置
说明:
- 由于RabbitMQ是基于Erlang语言开发, 所以在安装RabbitMQ之前, 需要先安装Erlang
- rabbitmq需要socat依赖, 所以需要先安装socat
具体步骤
1. 安装erlang
下载: wget http://www.rabbitmq.com/releases/erlang/erlang-18.3-1.el7.centos.x86_64.rpm
安装: rpm -ivh erlang-18.3-1.el7.centos.x86_64.rpm
2. 安装socat
下载: wget http://repo.iotti.biz/CentOS/7/x86_64/socat-1.7.3.2-5.el7.lux.x86_64.rpm
安装: rpm -ivh socat-1.7.3.2-5.el7.lux.x86_64.rpm
3. 安装rabbitmq
下载: wget http://www.rabbitmq.com/releases/rabbitmq-server/v3.6.5/rabbitmq-server-3.6.5-1.noarch.rpm
安装: rpm -ivh rabbitmq-server-3.6.5-1.noarch.rpm
安装成功, 如下图所示:
4. 启动rabbitmq服务
安装完成后, 还没启动服务, 先查看rabbitmq服务状态, 命令: service rabbitmq-server status
, 提示:
Error: unable to connect to node rabbit@wangzaiplus: nodedown
OK, 启动rabbitmq服务, 命令: service rabbitmq-server start
, 检查是否启动成功: ps -ef | grep rabbitmq
5. 启用后台管理插件
此时, 服务已经启动, 但通过浏览器访问http://localhost:15672
或ip:15672
还是不能访问管理界面, 因为还没启用插件, 启用命令: rabbitmq-plugins enable rabbitmq_management
, 提示Applying plugin configuration to rabbit@wangzaiplus... started 6 plugins.
即表示启用成功
浏览器远程访问http://192.168.1.123:15672
, 出现登录界面
注意:
如果服务器开启了防火墙, 则访问不了, 可以先关闭防火墙或者暴露端口
centos7查看防火墙状态:firewall-cmd --state
, 关闭后显示not running
,开启后显示running
关闭防火墙:systemctl stop firewalld.service
6. 开启用户远程访问
rabbitmq从3.3.0开始, 默认用户guest
只允许本机访问, 即: http://localhost:15672
, 如果通过ip:port
访问, 会发现Login failed
, 为了让guest
用户能够远程访问, 只需新建配置文件配置loopback_users
即可(rabbitmq.config
配置文件需手动创建), 步骤如下:
cd /etc/rabbitmq
新建配置文件: touch rabbitmq.config
vim rabbitmq.config
写入并保存: [{rabbit, [{loopback_users, []}]}].
重启服务: service rabbitmq-server restart
重新以guest
登录, OK
rabbitmq服务常用命令
- 查看rabbitmq服务状态:
service rabbitmq-server status
- 启动服务:
service rabbitmq-server start
- 停止服务:
service rabbitmq-server stop
- 重启服务:
service rabbitmq-server restart
- 设置开机启动:
chkconfig rabbitmq-server on
- 开启管控台:
rabbitmq-plugins enable rabbitmq_management
- 关闭管控台:
rabbitmq-plugins disable rabbitmq_management
安装步骤总结
- 下载erlang:
wget http://www.rabbitmq.com/releases/erlang/erlang-18.3-1.el7.centos.x86_64.rpm
- 安装erlang:
rpm -ivh erlang-18.3-1.el7.centos.x86_64.rpm
- 下载socat:
wget http://repo.iotti.biz/CentOS/7/x86_64/socat-1.7.3.2-5.el7.lux.x86_64.rpm
- 安装socat:
rpm -ivh socat-1.7.3.2-5.el7.lux.x86_64.rpm
- 下载rabbitmq:
wget http://www.rabbitmq.com/releases/rabbitmq-server/v3.6.5/rabbitmq-server-3.6.5-1.noarch.rpm
- 安装rabbitmq:
rpm -ivh rabbitmq-server-3.6.5-1.noarch.rpm
- 设置开机启动:
chkconfig rabbitmq-server on
- 开启rabbitmq服务:
service rabbitmq-server start
- 启用web管理插件:
rabbitmq-plugins enable rabbitmq_management
- 设置远程访问登录:
cd /etc/rabbitmq
touch rabbitmq.config
vim rabbitmq.config
写入:[{rabbit, [{loopback_users, []}]}].
service rabbitmq-server restart
常见错误
- 无法远程访问管理界面, rabbitmq服务安装成功并启动后, 浏览器访问
http://192.168.1.123:15672
发现访问不了, 有以下几个原因:
没有启用管理插件, 解决办法:
rabbitmq-plugins enable rabbitmq_management
服务器开启了防火墙且未开放
5672
、15672
端口, 解决办法: 关闭防火墙或暴露端口, 关闭防火墙命令:systemctl stop firewalld.service
很重要: 如果服务器使用的是第三方云服务(如腾讯云服务器), 设置了安全组策略, 入站规则没有开放
5672
、15672
端口, 那么也会出现这个问题, 解决办法: 登录云服务器管控台, 配置安全组策略并关联实例即可, 具体配置自行百度
如图:
代码实现
实现rabbit的通信
(test1为centos 7 虚拟机)
发送端
'''生产者(发送端)'''
import pika
connection = pika.BlockingConnection(pika.ConnectionParameters('test1'))
channel = connection.channel() # 声明一个管道
# 声明queue
channel.queue_declare(queue='hello2', durable=True) # 发送数据到队列, 代表一个进程
# 发消息
channel.basic_publish(exchange='',
routing_key='hello2',
body='hello world!',
properties=pika.BasicProperties(
delivery_mode=2,
))
print("[x] Sent 'hello world!'")
connection.close()
接收端
'''消费者(接收端)'''
'''消费者(接收端)'''
import time
import pika
connection = pika.BlockingConnection(pika.ConnectionParameters('test1'))
channel = connection.channel()
channel.queue_declare(queue='hello2', durable=True) #从队列接收数据, 代表一个进程
'''
如果确认发送端已经发送数据, 则不用再声明这个队列;
如果声明了这个队列, 即使发送端不发送消息, 这里也不会报错
'''
def callback(ch, method, properties, body):
# time.sleep(30)
print(" [x] Received %r" % body)
# ch.basic_ack(delivery_tag=method.delivery_tag)
channel.basic_qos(prefetch_count=1)
channel.basic_consume(on_message_callback=callback, # 如果收到消息, 就调用callback函数来处理消息
queue='hello2', # 从哪个队列收消息
auto_ack=True
)
print(' [*] Waiting for messages. To exit press CTRL+C')
channel.start_consuming() # 开始收消息, 一启动就永远运行
callback函数的参数
ch
管道的内存对象地址
method
处理数据的方法
properties
主要用来配置消息持久化
body
消息
b'hello world!'
轮询机制
发送端会轮流发送给连接发送端队列的接收端, 发送 顺序根据接收端的启动顺序来
相关命令
查看队列
$ rabbitmqctl list_queues
Timeout: 60.0 seconds ...
Listing queues for vhost / ...
name messages
hello 15
hello2 1
持久化
队列持久化
channel.queue_declare(queue='hello2', durable=True)
消息持久化
channel.basic_publish(exchange='', routing_key='hello2', body='hello world!', properties=pika.BasicProperties( delivery_mode=2, ))
消息分发
channel.basic_qos(prefetch_count=1)
处理完第一条才继续接收下一条