文章目录
1-1 消息队列概述
1. 队列
从v
2. 消息队列
消息(Message)是指在应用间传送的数据。消息可以非常简单,比如只包含文本字符串,JSON,也可以很复杂,比如内嵌对象。
消息队列(Messaeg Queue
)是一种使用队列(Queue)作为底层存储数据结构,可用于解决不同进程与应用之间通讯的分布式消息容器,也称为消息中间件。
从本质上说消息队列就是一个队列结构的中间件,也就是说消息放入这个中间件之后就可以直接返回,并不需要系统立即处理,而另外会有一个程序读取这些数据,并按顺序进行逐次处理。
目前使用得比较多的消息队列有ActiveMQ
,RabbitMQ
,Kafka
,RocketMQ
等。
3. 应用场景
消息队列常用的有五个场景:
- 消息通讯
- 异步处理
- 服务解耦
- 流量削峰
A. 消息通讯
消息队列最主要功能收发消息,其内部有高效的通讯机制,因此非常适合用于消息通讯。
可以基于消息队列开发点对点聊天系统,也可以开发广播系统,用于将消息广播给大量接收者。
B. 异步处理
一般写的程序都是顺序执行(同步执行),比如一个用户注册函数,其执行顺序如下:
- 写入用户注册数据
- 发送注册邮件
- 发送注册成功的短信通知
- 更新统计数据
按照上面的执行顺序,要全部执行完毕,才能返回成功,但其实在第1步执行成功后,其他的步骤完全可以异步执行,可以将后面的逻辑发给消息队列,再由其他程序异步执行,如下所示:
使用消息队列进行异步处理,可以更快地返回结果,加快服务器的响应速度,提升了服务器的性能。
C. 服务解耦
在系统中,应用与应用之间的通讯是很常见的,一般应用之间直接调用,比如说应用A调用应用B的接口,这时候应用之间的关系是强耦合的。
如果应用B处于不可用的状态,那么应用A也会受影响。
在应用A与应用B之间引入消息队列进行服务解耦,如果应用B挂掉,也不会影响应用A的使用。
使用消息队列之后,生产者并不关心消费者是谁,消费者同样不关注发送者是谁,这就是解耦,消息队列常用来解决服务之间的调用依赖关系。
D. 流量削峰
对于高并发的系统来说,在访问高峰时,突发的流量就像洪水般向应用系统涌过来,尤其是一些高并发写操作,随时会导致数据库服务器瘫痪,无法继续提供服务。
而引入消息队列则可以减少突发流量对应用系统的冲击。消息队列就像“水库”一样,拦蓄上游的洪水,削减进入下游河道的洪峰流量,从而达到减免洪水灾害的目的。
这方面最常见的例子就是秒杀系统,一般秒杀活动瞬间流量很高,如果流量全部涌向秒杀系统,会压垮秒杀系统,通过引入消息队列,可以有效缓冲突发流量,达到“削峰填谷”的作用。
1-2. RabbitMQ概述
1. 概述
RabbitMQ
是用Erlang
语言开发的一个实现了AMQP协议
的消息队列服务器,相比其他同类型的消息队列,最大的特点在保证可观的单机吞吐量的同时,延时方面非常出色。
RabbitMQ
支持多种客户端,比如:Python
、Ruby
、.NET
、Java
、JMS
、C
、PHP
、ActionScript
、XMPP
、STOMP
等。
RabbitMQ
最初起源于进入系统,用于在分布式系统中存储转发消息。
RabbitMQ的特点:
- 可靠性:RabbitMQ提供了多种技术可以在性能和可靠性之间进行权衡。这些技术包括持久性机制、投递确认、发布者证实和高可用性机制
- 灵活的路由:消息在到达队列前是通过交换器进行路由的。RabbitMQ为典型的路由逻辑提供了多种内置交换器类型
- 集群:在相同局域网中的多个RabbitMQ服务器可以聚合在一起,作为一个独立的逻辑代理来使用
- 联合:对于服务器来说,它比集群需要更多的松散和非可靠链接。为此RabbitMQ提供了联合模型
- 高可用队列:在同一个集群里,队列可以被镜像到多个机器中,以确保当其中某些节点出现故障后仍然可用
- 多协议:支持多种消息协议的消息传递,比如AMQP、STOMP、MQTT等
- 广泛的客户端:RabbitMQ 几乎支持所有常用语言,比如Java、.NET、Ruby、PHP、C#、JavaScript 等
- 可视化管理工具:RabbitMQ附带了一个易于使用的可视化管理工具,它可以帮助你监控和管理消息、集群中的节点
- 追踪:RabbitMQ提供了对异常行为的追踪的支持,能够发现问题所在
- 插件系统:RabbitMQ附带了各种各样的插件来进行扩展,甚至可以写插件来使用
2. AMQP协议
RabbitMQ是一个实现了AMQP协议的消息队列服务器,这里先来介绍以下AMQP。
AMQP,即Advanced Message Queuing Protocol,高级消息队列协议,是应用层协议的一个开放标准,为面向消息的中间件设计,它支持符合要求的客户端应用(application)和消息中间件代理(messaging middleware broker)之间进行通信。
消息代理(message brokers)从发布者(publishers)亦称生产者(producers)那儿接收消息,并根据既定的路由规则把接收到的消息发送给处理消息的消费者(consumers)。
由于AMQP是一个网络协议,所以这个过程中的发布者,消费者,消息代理可以存在于不同的设备上。
AMQP 0-9-1的工作过程如下图:
消息(message)被发布者(publisher)发送给交换器(exchange),交换器常常被比喻成邮局或者邮箱。然后交换器将收到的消息根据路由规则分发给绑定的队列(queue)。最后AMQP代理会将消息投递给订阅了此队列的消费者,或者消费者按照需求自行获取。
发布者(publisher)发布消息时可以给消息指定各种消息属性(message meta-data)。有些属性有可能会被消息代理(brokers)使用,然而其他的属性则是完全不透明的,它们只能被接收消息的应用所使用。
从安全角度考虑,网络是不可靠的,接收消息的应用也有可能在处理消息的时候失败。
基于此原因,AMQP模块包含了一个消息确认(message acknowledgements)的概念:当一个消息从队列中投递给消费者后(consumer),消费者会通知一下消息代理(broker),这个可以是自动的也可以由处理消息的应用的开发者执行。当“消息确认”被启用的时候,消息代理不会完全将消息从队列中删除,直到它收到来自消费者的确认回执(acknowledgement)。
在某些情况下,例如当一个消息无法被成功路由时,消息或许会被返回给发布者并被丢弃。或者,如果消息代理执行了延期操作,消息会被放入一个所谓的死信队列中。此时,消息发布者可以选择某些参数来处理这些特殊情况。
队列,交换器和绑定统称为AMQP实体(AMQP entities)。
AMQP协议的设计模型如下:
3. 相关概念
RabbitMQ
有属于自己的一套核心概念,对这些概念的理解很重要,只有理解了这些核心概念,才有可能建立对RabbitMQ
的全面理解。
A. 生产者,Producer
生产者连接到RabbitMQ服务器,然后将消息发送到RabbitMQ服务器的队列,是消息的发送方。
消息一般可以包含2个部分:
- 消息体:消息体也可以称之为payload,在实际应用中,消息体一般是一个带有业务逻辑结构的数据,可以很简单,比如文本字符串、json等,也可以是内嵌对象等。
- 标签(Label)
生产者不需要知道消费者是谁。
另外,生产者不是直接将消息发送到队列的,而是将消息发送到交换器的,再由交换器转发到队列去。
B. 消费者,Consumer
消费者连接到RabbitMQ服务器,并订阅到队列上,是消息的接收方。
当消费者消费一条消息时,只是消费消息的消息体(payload)。
在消息路由的过程中,消息的标签会丢弃,存入到队列中的消息只有消息体,消费者也只会消费到消息体,也就不知道消息的生产者是谁,当然消费者也不需要知道。
C. Broker
Broker,是消息中间件的服务节点。
对于RabbitMQ来说,一个RabbitMQ Broker可以简单地看作一个RabbitMQ服务节点,或者 RabbitMQ服务实例 。
D. Queue,队列
队列,Queue,是RabbitMQ内部用于存储消息的对象,是真正用存储消息的结构。
在生产端,生产者的消息最终发送到指定队列,而消费者也是通过订阅某个队列,达到获取消息的目的。
RabbitMQ中消息都只能存储在队列中,队列的特性是先进先出。
队列收到的消息,必须是由Exchange转发过来的。
一个队列中的消息数据可能来自于多个Exchange,一个Exchange中的消息数据也可能推送给多个队列,它们之间的关系是多对多的。
多个消费者可以订阅同一个Queue,此时Queue中的消息会被平均分摊(即轮询)给多个消费者进行处理,而不是每个消息者都收到所有的消息并处理。
如图,红色的表示队列
E. Exchange
Exchange,消息交换器,作用是接收来自生产者的消息,并根据路由键转发消息到所绑定的队列。
生产者发送的消息,每个消息都会带有一个路由键(RoutingKey),就是一个简单的字符串,消息先通过Exchange按照绑定(binding)规则转发到队列的。
一个队列中的消息数据可能来自于多个Exchange,一个Exchange中的消息数据也可能推送给多个队列,它们之间的关系是多对多的。
交换器拿到一个消息之后将它路由给一个或多个队列。它使用哪种路由算法是由交换器类型和被称作绑定(bindings)的规则所决定的。
如图:
交换器类型(Exchange Type)共有4种:
- fanout:扇型交换器,这种类型不处理路由键(RoutingKey),类似于广播,把所有发送到交换器上的消息都会发送给与该交换器绑定的所有队列上。该类型下发送消息是最快的。
- direct:直连交换器,模式处理路由键(RoutingKey),需要路由键(RoutingKey)和BindingKey完全匹配的队列才能收到交换器的消息。这种模式使用最多。
- topic:主题交换器,将路由键和某模式进行匹配。
- headers:头交换器,一般很少用。
注意:交换器(Exchange)只负责转发消息,不具备存储消息的能力,因此如果没有任何队列于交换器绑定,或者是没有符合路由规则的队列,那么消息会丢失。
F. Binding
Binding
是一种操作,其作用是建立消息从Exchange
转发到Queue
的规则,在进行Exchange
与Queue
的绑定时,需要指定一个路由键BindingKey
,Binding操作一般用于RabbitMQ
的路由工作模式和主题工作模式。
如下图:
G. vhosts
vhosts,虚拟主机(Virtual Host),Virutal host
也叫虚拟主机,一个Virtual Host
下面有一组不同Exchnage
与Queue
,不同的Virtual host
的Exchnage
与Queue
之间互相不影响。
每个vhosts本质上就是一个mini版的RabbitMQ服务器,拥有自己的队列、交换器、绑定和权限机制。
应用隔离与权限划分,Virtual host
是RabbitMQ中最小颗粒的权限单位划分。
如果要类比的话,可以把Virtual host
比作MySQL
中的数据库,通常我们在使用MySQL
时,会为不同的项目指定不同的数据库,同样的,在使用RabbitMQ
时,可以为不同的应用程序指定不同的Virtual host
。
RabbitMQ 当中,用户只能在虚拟主机的粒度进行权限控制。因此,如果需要禁止 A 组访问 B 组的交换器 / 队列 / 绑定,必须为 A 和 B 分别创建一个虚拟主机。
每一个 RabbitMQ 服务器都有一个默认的虚拟主机 “/” 。
一个RabbitMQ的Server上可以有多个vhosts,用户与权限设置就是依附于vhosts。
在同一个vhosts下的exchange和queue才能相互绑定。
建议:一般有多个项目需要使用RabbitMQ的时候,不需要每个项目都去部署RabbitMQ,这样非常浪费资源,只需要每个项目对应一个vhost即可,vhost 之间是绝对隔离的,不同的 vhost 对应不同的项目,互不影响。
H. Connection
Connection
是RabbitMQ
内部对象之一,偏物理的概念,是一个TCP连接,用于管理每个到RabbitMQ
的TCP
网络连接。
生产者、消费者和Broker之间就是通过Connection进行连接的。
I. Channel
Channel
,信道,是与RabbitMQ
打交道的最重要的一个接口,是偏逻辑上的概念,在一个连接(Connection)中可以创建多个Channel。
大部分与RabbitMQ的相关操作是在Channel
这个接口中完成的,包括定义Queue
、定义Exchange
、绑定Queue
与Exchange
、发布消息等。
一旦连接(Connection)建立起来,客户端紧接着可以创建一个AMQP信道(Channel),每个信道都会被指派一个唯一的ID。信道是建立在Connection之上的虚拟连接,RabbitMQ处理的每条AMQP指令都是通过信道完成的。
为什么要引入Channel呢?
在某个场景下一个应用程序中有很多个线程需要从RabbitMQ中消费消息,或者生产消息,那么必然需要建立很多个Connection,也就是许多个TCP连接。然而对于操作系统而言,建立和销毁TCP连接是非常昂贵的开销,如果遇到使用高峰,性能瓶颈也随之显现。
RabbitMQ采用类似NIO(Non-blocking I/O)的做法,选择TCP连接复用,不仅可以减少性能开销,同时也便于管理。
每个线程把持一个信道,所以信道复用了Connection的TCP连接。同时RabbitMQ可以确保每个线程的私密性,就像拥有独立的连接一样。当每个信道的流量不是很大时,复用单一的Connection可以在产生性能瓶颈的情况下有效地节省TCP连接资源。
但是当信道本身的流量很大时,这时候多个信道复用一个Connection就会产生性能瓶颈,进而使整体的流量被限制了。此时就需要开辟多个Connection,将这些信道均摊到这些Connection中。
4. 运转流程
可以参考如图:
消息的运转过程如下图:
生产者发送消息:
- 生产者连接到Broker,建立一个连接(Connection),开启一个信道(Channel)
- 生产者声明一个交换器,并设置相关属性,比如交换器类型、是否持久化等
- 生产者声明一个队列并设置相关属性,比如是否排他性、是否持久化、是否自动删除等
- 生产者通过路由键将交换器和队列绑定起来
- 生产者发送消息到Broker,其中包含路由键、交换器等信息
- 相应的交换器根据接收到的路由键查找相匹配的队列
- 如果找到则将从生产者发送来的消息存入对应的队列中
- 如果没找到则根据生产者配置的属性选择丢弃还是回退给生产者
- 关闭信道
- 关闭连接
消费者接收消息:
- 消费者连接到Broker,建立一个连接(Connection),开启一个信道(Channel)
- 消费者向Broker请求消费相应队列中的消息,可能会设置相应的回调函数,以及做一些准备工作
- 等待Broker回应并投递相应队列中的消息,消费者接收消息
- 消费者确认(ack)接收到的消息
- RabbitMQ从队列中删除相应已经被确认的消息
- 关闭信道
- 关闭连接
架构设计如下:
2-1. 安装
这里只介绍Windows的安装和CentOS7下的安装,官网还提供了Ubuntu、Mac甚至docker下的安装等。
RabbitMQ服务器的代码是使用erlang语言编写的,所以是需要先安装erlang语言的。
注意:RabbitMQ的版本依赖于Erlang的版本,两者之间是有版本的兼容性要求的,一定要选择兼容的版本,具体可参考:https://www.rabbitmq.com/which-erlang.html
1. Windows安装
A. 安装erlang
先通过官网下载erlang:官网下载地址
下载exe文件然后安装
安装好了之后需要设置环境变量:
我的电脑 - 右击属性 - 高级系统设置 - 环境变量 - 用户变量/系统变量新建一个变量:
变量名为:ERLANG_HOME,变量值为erlang的安装目录
还需要加入到Path中:%ERLANG_HOME%\bin
然后打开命令行,输入erl,如果显示erlang的版本信息即表示安装成功:
B. 安装RabbitMQ
通过官网下载RabbitMQ:官网下载地址
双击安装即可
C. 安装Web管理【非必须】
该步骤非必须,RabbitMQ还提供了Web管理工具,而Web管理工具作为RabbitMQ的插件,相当于是一个后台管理页面,方便在浏览器中查看
进入到sbin目录下,打开命令行输入:
./rabbitmq-plugins.bat enable rabbitmq_management
安装成功之后,浏览器输入http://localhost:15672即可访问管理页面
默认的账号和密码都是guest
注意:一般会创建个新的管理员用户,不使用默认的guest,guest用户只能在localhost下访问,如果在内网的其他机器访问的话,登录的时候会报错:
Web管理页面的使用操作可见 6-1
2. Centos 7安装
A. 安装erlang
关于安装erlang,RabbitMQ官网提供了4种方式安装:
我这里使用第一种方式,按照Github上的安装方式
新建一个文件:/etc/yum.repos.d/rabbitmq_erlang.repo
# vim /etc/yum.repos.d/rabbitmq_erlang.repo
[rabbitmq_erlang]
name=rabbitmq_erlang
baseurl=https://packagecloud.io/rabbitmq/erlang/el/7/$basearch
repo_gpgcheck=1
gpgcheck=1
enabled=1
# PackageCloud's repository key and RabbitMQ package signing key
gpgkey=https://packagecloud.io/rabbitmq/erlang/gpgkey
https://dl.bintray.com/rabbitmq/Keys/rabbitmq-release-signing-key.asc
sslverify=1
sslcacert=/etc/pki/tls/certs/ca-bundle.crt
metadata_expire=300
[rabbitmq_erlang-source]
name=rabbitmq_erlang-source
baseurl=https://packagecloud.io/rabbitmq/erlang/el/7/SRPMS
repo_gpgcheck=1
gpgcheck=0
enabled=1
# PackageCloud's repository key and RabbitMQ package signing key
gpgkey=https://packagecloud.io/rabbitmq/erlang/gpgkey
https://dl.bintray.com/rabbitmq/Keys/rabbitmq-release-signing-key.asc
sslverify=1
sslcacert=/etc/pki/tls/certs/ca-bundle.crt
metadata_expire=300
保存之后,进行yum安装
yum install erlang
然后打开命令行,输入erl,如果显示erlang的版本信息即表示安装成功:
注意:不能直接yum install erlang
,会导致erlang的版本很低,后面安装rabbitMQ的时候会有版本冲突的
B. 安装其他依赖
除了erlang,RabbitMQ还需要安装:socat和logrotate
yum install -y socat logrotate
C. 安装RabbitMQ
这里有两种方式进行安装:
- rpm安装
- yum安装
第一种方式:rpm安装
下载RabbitMQ,具体的rpm包链接可参考:官网下载页面
wget https://github.com/rabbitmq/rabbitmq-server/releases/download/v3.8.14/rabbitmq-server-3.8.14-1.el7.noarch.rpm
然后rpm安装:
rpm -ivh rabbitmq-server-3.8.14-1.el7.noarch.rpm
第二种方式:yum安装
新建一个文件:/etc/yum.repos.d/rabbitmq_server.repo
# vim /etc/yum.repos.d/rabbitmq_server.repo
[rabbitmq_server]
name=rabbitmq_server
baseurl=https://packagecloud.io/rabbitmq/rabbitmq-server/el/7/$basearch
repo_gpgcheck=1
gpgcheck=0
enabled=1
gpgkey=https://packagecloud.io/rabbitmq/rabbitmq-server/gpgkey
sslverify=1
sslcacert=/etc/pki/tls/certs/ca-bundle.crt
metadata_expire=300
[rabbitmq_server-source]
name=rabbitmq_server-source
baseurl=https://packagecloud.io/rabbitmq/rabbitmq-server/el/7/SRPMS
repo_gpgcheck=1
gpgcheck=0
enabled=1
gpgkey=https://packagecloud.io/rabbitmq/rabbitmq-server/gpgkey
sslverify=1
sslcacert=/etc/pki/tls/certs/ca-bundle.crt
metadata_expire=300
然后yum安装:
yum install -y rabbitmq_server rabbitmq-server
通过rabbitmqctl来验证是否安装成功:
>>> rabbitmqctl version
3.8.14
D. 启动
以守护进程的方式来启动RabbitMQ:
# -detached为可选参数,表示后台开启
rabbitmq-server -detached
可以通过查看状态来验证是否启动:
rabbitmqctl status
如果要关闭的话,可以使用:
rabbitmqctl stop
E. 安装Web管理【非必须】
该步骤非必须,RabbitMQ还提供了Web管理工具,而Web管理工具作为RabbitMQ的插件,相当于是一个后台管理页面,方便在浏览器中查看
rabbitmq-plugins enable rabbitmq_management
安装完成之后即可通过localhost:15672来进行访问
默认的账号和密码都是guest
注意:一般会创建个新的管理员用户,不使用默认的guest,guest用户只能在本地(localhost)下访问,如果在内网的其他机器访问的话,登录的时候会报错:
以下是添加管理员用户(这里是root):
# 添加用户
rabbitmqctl add_user root 123456
# 赋予管理员权限
rabbitmqctl set_user_tags root administrator
# 设置所有权限
rabbitmqctl set_permissions -p / root ".*" ".*" ".*"
# 查看用户列表
rabbitmqctl list_users
2-2 RabbitMQ常见命令
以下是RabbitMQ的场景命令:
# 查看版本
rabbitmqctl version
# 查看状态
rabbitmqctl status
# 停止
rabbitmqctl stop
# 添加用户
rabbitmqctl add_user root 123456
# 赋予管理员权限
rabbitmqctl set_user_tags root administrator
# 设置所有权限
rabbitmqctl set_permissions -p / root ".*" ".*" ".*"
# 查看用户列表
rabbitmqctl list_users
# 查看所有的交换器
rabbitmqctl list_exchanges
# 查看所有的队列
rabbitmqctl list_queues
# 查看未确认的消息
rabbitmqctl list_queues name messages_ready messages_unacknowledged
# 查看所有的绑定
rabbitmqctl list_bindings
3-1. 交换器类型
交换器类型(Exchange Type)共有4种:
- fanout:扇型交换器,这种类型不处理路由键(RoutingKey),类似于广播,把所有发送到交换器上的消息都会发送给与该交换器绑定的所有队列上。该类型下发送消息是最快的。
- direct:直连交换器,模式处理路由键(RoutingKey),需要路由键完全匹配的队列才能收到交换器的消息。这种模式使用最多。
- topic:主题交换器,将路由键和某模式进行匹配。
- headers:头交换器,一般很少用。
1. Fanout
扇型交换器(fanout exchange)将消息路由给绑定到它身上的所有队列,而不理会绑定的路由键,也就是说在Fanout模式下是不需要RoutingKey的。
如果N个队列绑定到某个扇型交换器上,当有消息发送给此扇型交换器时,交换器会将消息的拷贝分别发送给这所有的N个队列。扇型用来交换器处理消息的广播路由(broadcast routing)。
因为扇型交换器投递消息的拷贝到所有绑定到它的队列,所以应用场景都极其相似:
- 大规模多用户在线(MMO)游戏可以使用它来处理排行榜更新等全局事件
- 体育新闻网站可以用它来近乎实时地将比分更新分发给移动客户端
- 分发系统使用它来广播各种状态和配置更新
- 在群聊的时候,它被用来分发消息给参与群聊的用户
扇型交换器图例:
2. Direct
直连型交换器(direct exchange)是根据消息携带的路由键(RoutingKey)将消息投递给对应队列的。直连交换器用来处理消息的单播路由(unicast routing)。下边介绍它是如何工作的:
- 将一个队列绑定到某个交换器上,同时赋予该绑定一个路由键(RoutingKey)
- 当一个携带着路由键为
R
的消息被发送给直连交换器时,交换器会把它路由给绑定值同样为R
的队列。
直连交换器经常用来循环分发任务给多个工作者(workers)。当这样做的时候,我们需要明白一点,在AMQP 0-9-1中,消息的负载均衡是发生在消费者(consumer)之间的,而不是队列(queue)之间。
直连交换器是完全匹配、单播的模式,同时也是RabbitMQ默认的交换器模式,也是最简单的模式。
直连型交换器图例:
3. Topic
主题交换器(topic exchanges)通过对消息的路由键和队列到交换器的绑定模式之间的匹配,将消息路由给一个或多个队列。主题交换器经常用来实现各种分发/订阅模式及其变种。主题交换器通常用来实现消息的多播路由(multicast routing)。
主题交换器拥有非常广泛的使用场景。无论何时,当一个问题涉及到那些想要有针对性的选择需要接收消息的 多消费者/多应用(multiple consumers/applications) 的时候,主题交换器都可以被列入考虑范围。
使用场景:
- 分发有关于特定地理位置的数据,例如销售点
- 由多个工作者(workers)完成的后台任务,每个工作者负责处理某些特定的任务
- 股票价格更新(以及其他类型的金融数据更新)
- 涉及到分类或者标签的新闻更新(例如,针对特定的运动项目或者队伍)
- 云端的不同种类服务的协调
- 分布式架构/基于系统的软件封装,其中每个构建者仅能处理一个特定的架构或者系统。
主题交换器在流程上和直连模式类似,但比直连模式更优化的地方在于支持了RoutingKey的通配符,通配符也并不是按照正则表达式的那种方式,只是简单支持了*和#,并且RoutingKey有严格的规划,单词之间必须用星号符(.)隔开
- * 代表一个单词
- # 代表零个或多个单词
如图:
- 路由键为“com.orange.rabbit”的消息会同时路由到Q1和Q2
- 路由键为“lazy.orange.rabbit”的消息会同时路由到Q1和Q2
- 路由键为“com.hidden.rabbit”的消息只会路由到Q2
- 路由键为“com.orange.demo”的消息只会路由到Q1
- 路由键为“java.hidden.rabbit”的消息只会路由到Q2
- 路由键为“java.hidden.demo”的消息因为没有匹配任何路由键将会被丢弃或者是返回给生产者(需要mandatory参数)
4. Headers
有时消息的路由操作会涉及到多个属性,此时使用消息头就比用路由键更容易表达,头headers 交换器就是为此而生的。头交换器使用多个消息属性来代替路由键建立路由规则。通过判断消息头的值能否与指定的绑定相匹配来确立路由规则。
我们可以绑定一个队列到头交换器上,并给他们之间的绑定使用多个用于匹配的头(header)。这个案例中,消息代理得从应用开发者那儿取到更多一段信息,换句话说,它需要考虑某条消息(message)是需要部分匹配还是全部匹配。上边说的“更多一段消息”就是"x-match"参数。当"x-match"设置为“any”时,消息头的任意一个值被匹配就可以满足条件,而当"x-match"设置为“all”的时候,就需要消息头的所有值都匹配成功。
头交换器可以视为直连交换器的另一种表现形式。头交换器能够像直连交换器一样工作,不同之处在于头交换器的路由规则是建立在头属性值之上,而不是路由键。路由键必须是一个字符串,而头属性值则没有这个约束,它们甚至可以是整数或者哈希值(字典)等。
Headers交换器提供了另一种不同于主题交换器的策略,在发送消息的时候,给消息设置header,header是一系列的键值对,可以设置多个,配置绑定关系有两种选择:
- match-any:header中的任意一个键值对能够匹配上就会被路由到对应的Queue
- match-all:header中的所有键值对能够匹配上才会被路由到对应的Queue
注意:Headers类型的交换器性能会很差,而且也不实用,基本上不会看到它的存在。
5. 总结
从性能的角度来说,Fanout > Direct > Topic > Headers
在实际应用中,在满足工作场景的前提下,选择性能最高的那种模式,一般会使用Direct类型。
3-2. 消息确认
消费者在消费消息的时候,可能是几秒钟,也可能是耗时很长比如几十分钟,这时候有可能消费者执行到了一半结果崩溃了或者是连接断开了或者是该消费者被人工kill掉等,那么该消息就有可能会丢失。
为了保证消息能够从队列可靠地到达消费者,RabbitMQ 提供了消息确认机制。
消息确认机制可分为两种:
- 消费者确认
- 生产者确认
如图,RabbitMQ的消息确认流程:
消费者确认
消费者确认机制:
- 消费者在处理完消息之后,发回一个 ack 来告诉 RabbitMQ 消息已经被接收、处理,此时 RabbitMQ 才去可以删除它
- 消费者在处理消息的时候如果处理失败,也可以发回一个ack来告诉RabbitMQ拒绝接收,让RabbitMQ重新发送消息
- 如果消费者在没有发送 ack 的情况下死亡(其通道关闭、连接关闭或 TCP 连接丢失),RabbitMQ 将理解消息未完全处理并将重新排队。如果有其他消费者同时在线,RabbitMQ会将消息重新分配给另一个消费者,这样就可以确保不会丢失任何消息
消费者订阅队列的时候,可以指定autoAck参数:
- 当autoAck为true的时候,RabbitMQ采用自动确认模式,RabbitMQ自动把发送出去的消息设置为确认,然后从内存或者硬盘中删除,而不管消费者是否真正消费到了这些消息。
- 当autoAck为false的时候,RabbitMQ会等待消费者回复的确认信号,收到确认信号之后才从内存或者磁盘中删除消息。
默认情况下autoAck为false,即不自动确认,以Python为示例:
# 定义队列的消费回调,将消息传递给回调函数,消费完成手动进行消息确认
channel.basic_consume(queue=队列名, on_message_callback=callback, auto_ack=False)
消息确认机制是RabbitMQ消息可靠性投递的基础,只要设置autoAck参数为false,消费者就有足够的时间处理消息,不用担心处理消息的过程中消费者进程挂掉后消息丢失的问题。
注意:在实际应用中很容易忘记消息确认,这会导致堆积越来越多的未确认的消息,这种消息无法自动释放,可以通过以下命令来查看未确认的消息:
rabbitmqctl list_queues name messages_ready messages_unacknowledged
注意:实际项目中是会关闭自动确认的,但无论如何消费者必须发送ack响应,否则会导致堆积的未确认消息越来越多。
生产者确认
同样的,生产者发送消息到Broker的时候,如果消息由于网络原因无法达到,而生产者也不知道消息到底有没有到Broker,这有可能会造成问题,比如重复消费的问题等。
所以RabbitMQ同样的提供了生产者确认机制:
- 消息到达Exchange:Exchange向生产者发送Confirm确认,成功或失败都会返回一个confirmCallback
- 消息成功达到Exchange,但是从Exchange投递Queue失败:向生产者返回一个returnCallback。只有失败才会返回
3-3. 持久化
虽然可以通过消息确认机制来避免消费者一旦死亡而导致消息丢失,但还是存在消息丢失的可能性。
当RabbitMQ崩溃挂掉的时候,交换器和队列是会全部丢失:
- 交换器丢失,消息不会丢失,但不能将消息发送到该交换器
- 队列丢失,队列中的消息会丢失
所以为了保证消息不丢失,都会在建立交换器和队列的时候声明持久化存储,持久化之后即使RabbitMQ崩溃挂掉,那么在重启之后交换器和队列依然还是存在的不会丢失。
RabbitMQ默认都是不开启持久化的,默认建立的是临时交换器和队列。
交换器的持久化是通过durable=True来实现的:
# 声明exchange,由exchange指定消息在哪个队列传递,如不存在,则创建。durable=True 代表exchange持久化存储,False 非持久化存储
channel.exchange_declare(exchange=交换器名, durable=True)
如果不设置交换器的持久化,那么在RabbitMQ服务重启之后,相关的交换器元数据会丢失,但消息不会丢失,只是不能将消息发送到该交换器中。
对一个长期使用的交换器来说,建议将其设置为持久化。
队列的持久化是通过durable=True来实现的:
# 声明消息队列,消息将在这个队列传递,如不存在,则创建。durable=True 代表消息队列持久化存储,False 非持久化存储
channel.queue_declare(queue=队列名, durable=True)
注意:如果已存在一个非持久化的队列或交换器 ,执行上述代码会报错,因为RabbitMQ不允许使用不同的参数重新声明一个队列或交换器,需要删除重建。另外如果队列和交换器中一个声明了持久化,另一个没有声明持久化,则不允许绑定。
如果队列不设置为持久化,那么在RabbitMQ服务重启之后,相关的队列元数据会丢失,此时数据也会丢失,即队列中的消息也会丢失的。
队列的持久化能保证其本身的元数据不因异常情况而丢失,但并不能保证内部所存储的消息不会丢失。要想保证消息不会丢失,需要将消息也设置持久化。
消息的持久化是通过在BasicProperties中设置deliveryMode设置为2来实现的:
# 向队列插入消息,delivery_mode=2:消息持久化,delivery_mode=1:消息非持久化
channel.basic_publish(exchange=交换器名, routing_key=路由键, body = message, properties=pika.BasicProperties(delivery_mode=2))
设置了队列和消息的持久化之后,当RabbitMQ服务重启之后,消息依旧存在。
只设置队列持久化,重启之后队列里面的消息会丢失。
只设置消息的持久化,重启之后队列消失,既而消息也丢失,设置消息持久化而不设置队列的持久化显得毫无意义。
注意:如果将所有的消息都进行持久化操作会严重影响RabbitMQ的性能,因为写入磁盘的速度比写入内存的速度慢很多,对于可靠性不是那么高的消息可以不采用持久化处理以提高整体的吞吐量。鱼和熊掌不可兼得,关键在于选择和取舍。在实际中,需要根据实际情况在可靠性和吞吐量之间做一个权衡。
注意:将队列、交换器和消息都设置了持久化之后也并不能保证100%的消息不会丢失
- 首先,如果开启了自动消息确认,那么消息传递给消费者的时候就已经从队列中删除,而消费者自身崩溃挂掉的话会导致该消息丢失,这可以通过手动消息确认机制来解决
- 其次,消息在正确存入RabbitMQ之后,还需要有一段时间(这个时间很短,但不可忽视)才能存入磁盘之中,RabbitMQ并不是为每条消息都做fsync的处理,可能仅仅保存到cache中而不是物理磁盘上,在这段时间内RabbitMQ broker发生crash, 消息保存到cache但是还没来得及落盘,那么这些消息将会丢失。可引入RabbitMQ镜像队列机制来解决,具体可参考:《RabbitMQ实战指南》4.7章持久化
总结:
- RabbitMQ默认都是不开启持久化的
- 交换器不持久化,重启之后交换器丢失,消息不会丢失,但不能将消息发送到该交换器
- 队列不持久化,重启之后队列丢失,队列中的消息会丢失
- 只设置消息持久化而队列不持久化的做法无意义
- 队列、交换器和消息都设置了持久化之后也并不能保证100%的消息不会丢失
- 一般会设置交换器和队列持久化,而消息是否持久化则根据实际场景来
3-4. 公平调度
当RabbitMQ拥有多个消费者时,队列收到的消息将以轮询(round-robin)的分发方式发送给消费者。每条消息只会发送给订阅列表里的一个消费者。这种方式非常适合扩展,而且它是专门为并发程序设计的。如果现在负载加重,那么只需创建更多的消费者来消费处理消息即可。
但很多时候轮询的分发机制也不是那么优雅。默认情况下,如果有n个消费者,那么rabbitmq会将第m条消息分发给第m%n(取余的方式)个消费者,RabbitMQ不管消费者是否消费并已经确认(Basic.Ack)了消息。
试想一下,如果某些消费者任务繁重,来不及消费那么多消息,而某些其他消费者由于某些原因很快处理完了所分配到的消息,进而空闲,这样就会造成整体应用吞吐量的下降。
RabbitMQ是可以设置公平分配消息任务,不会给某个消费者同时分配多个消息处理任务,换句话说,RabbitMQ在处理和确认消息之前,不会向消费者发送新的消息,而是将消息分发给下一个不忙的消费者。
RabbitMQ是使用channel.basic_qos(num)
来保证公平调度的,该方法允许限制信道上的消费者所能保持的最大未确认消息的数量。
# 设置消费者允许的最大未确认消息数量为1
channel.basic_qos(prefetch_count=1)
举例说明,在订阅消费队列之前,消费端程序调用了channel.basic_qos(3)
,之后定义了某个队列进行消费。RabbitMQ会保存一个消费者的列表,每发送一条消息都会为对应的消费者计数,当达到了所设定的上限,那么RabbitMQ将不会向这个消费者再发送任何消息,直到消费者确认了某条消息之后,RabbitMQ会将相应的计数减1,之后消费者可以继续接收消息,直到再次达到计数上线。这种机制可以类比于TCP/IP的滑动窗口。
注意:如果channel.basic_qos(num)
的num设置为0则表示没有上限。
4-1. 开发语言支持
RabbitMQ
支持多种客户端,比如:Python
、Ruby
、.NET
、Java
、JMS
、C
、PHP
、ActionScript
、XMPP
、STOMP
等。
这里主要是以Python和PHP为例进行演示。
Python
注意:Python是使用3.7版本,并且需要安装好pika
pip install pika
PHP
注意:需要PHP:7.x+,并且安装php-amqplib,可通过composer进行安装
composer.json(项目根目录下)
{
"require": {
"php-amqplib/php-amqplib": ">=3.0"
}
}
项目目录下进行composer install
即可
或者是直接不需要上面的composer.json,直接进行命令:
composer require php-amqplib/php-amqplib
4-2. 工作模式
RabbitMQ
一共有7种工作模式,具体可见:RabbitMQ Tutorials
- 简单模式
- 工作模式
- 发布/订阅模式
- 路由模式
- 主题模式
RPC
模式- 生产者确认模式
1. 简单模式
简单(simple)模式,是几种工作模式中最简单的一种模式了,如下图:
有以下特点:
- 只有一个生产者、一个消费者和一个队列
- 生产者和消费者在发送和接收消息时,只需要指定队列名,而不需要指定发送到哪个交换器,RabbitMQ会自动使用vhosts的默认交换器,默认交换器的type为直连(direct)
应用场景:将发送的电子邮件放到消息队列,然后邮件服务在队列中获取邮件并发送给收件人
生产者发送消息步骤:
- 连接RabbitMQ服务器,建立信道
- 创建队列,如果不创建队列的话,发送消息的时候队列不存在那么RabbitMQ会抛弃该消息,注意:重复创建队列并不会重复创建队列,建议生产端和消费端都需要创建队列
- 发送消息,注意:这里只是发送到交换器而已,并不会直接到队列,空交换器是默认交换器,这里只是测试所以用默认交换器
- 关闭连接
消费者接收消息步骤:
- 同发送消息,连接RabbitMQ服务器,建立信道
- 同发送消息,创建队列,如果不创建队列的话,发送消息的时候队列不存在那么RabbitMQ会抛弃该消息,注意:重复创建队列并不会重复创建队列,建议生产端和消费端都需要创建队列
- 定义回调函数,每接收到一条消息就会把消息传递给回调函数
- 定义队列的消费回调,将消息传递给回调函数,同时进行消息确认
- 循环消费/接收消息
Python
注意:Python是使用3.7版本,并且需要安装好pika
发送端,发送消息:1-simple-send.py
#!/usr/bin/env python
import pika
# 连接RabbitMQ服务器
connection = pika.BlockingConnection(pika.ConnectionParameters(host='localhost'))
channel = connection.channel()
# 创建队列
channel.queue_declare(queue='hello')
# 发送消息,消息体为hello world,交换器为默认交换器(空交换器),路由键为hello
channel.basic_publish(exchange='', routing_key='hello', body='Hello World!')
print(" [x] Sent 'Hello World!'")
# 关闭连接
connection.close()
接收端,接收消息:1-simple-receive.py
#!/usr/bin/env python
import pika, sys, os
def main():
# 连接RabbitMQ服务器
connection = pika.BlockingConnection(pika.ConnectionParameters(host='localhost'))
channel = connection.channel()
# 创建队列
channel.queue_declare(queue='hello')
# 定义回调函数
def callback(ch, method