MQ的使用场景大概包括解耦,提高峰值处理能力,送达和排序保证,缓冲等。
消息队列技术是分布式应用间交换信息的一种技术。
消息队列可驻留在内存或磁盘上,队列存储消息直到它们被应用程序读走。
通过消息队列,应用程序可独立地执行--它们不需要知道彼此的位置、或在继续执行前不需要等待接收程序接收此消息。
MQ主要作用是接受和转发消息。你可以想想在生活中的一种场景:当你把信件的投进邮筒,邮递员肯定最终会将信件送给收件人。我们可以把MQ比作 邮局和邮递员。
MQ和邮局的主要区别是,它不处理消息,但是,它会接受数据、存储消息数据、转发消息。
RabbitMQ 基本概念
上面只是最简单抽象的描述,具体到 RabbitMQ 则有更详细的概念需要解释。上面介绍过 RabbitMQ 是 AMQP 协议的一个开源实现,所以其内部实际上也是 AMQP 中的基本概念:
RabbitMQ 内部结构
1,Message
消息,消息是不具名的,它由消息头和消息体组成。消息体是不透明的,而消息头则由一系列的可选属性组成,这些属性包括routing-key(路由键)、priority(相对于其他消息的优先权)、delivery-mode(指出该消息可能需要持久性存储)等。
2,Publisher
消息的生产者,也是一个向交换器发布消息的客户端应用程序。
3,Exchange
交换器,用来接收生产者发送的消息并将这些消息路由给服务器中的队列。
4,Binding
绑定,用于消息队列和交换器之间的关联。一个绑定就是基于路由键将交换器和消息队列连接起来的路由规则,所以可以将交换器理解成一个由绑定构成的路由表。
5,Queue
消息队列,用来保存消息直到发送给消费者。它是消息的容器,也是消息的终点。一个消息可投入一个或多个队列。消息一直在队列里面,等待消费者连接到这个队列将其取走。
6,Connection
网络连接,比如一个TCP连接。
7,Channel
信道,多路复用连接中的一条独立的双向数据流通道。信道是建立在真实的TCP连接内地虚拟连接,AMQP 命令都是通过信道发出去的,不管是发布消息、订阅队列还是接收消息,这些动作都是通过信道完成。因为对于操作系统来说建立和销毁 TCP 都是非常昂贵的开销,所以引入了信道的概念,以复用一条 TCP 连接。
8,Consumer
消息的消费者,表示一个从消息队列中取得消息的客户端应用程序。
9,Virtual Host
虚拟主机,表示一批交换器、消息队列和相关对象。虚拟主机是共享相同的身份认证和加密环境的独立服务器域。每个 vhost 本质上就是一个 mini 版的 RabbitMQ 服务器,拥有自己的队列、交换器、绑定和权限机制。vhost 是 AMQP 概念的基础,必须在连接时指定,RabbitMQ 默认的 vhost 是 / 。
10,Broker
表示消息队列服务器实体。
AMQP 中的消息路由
AMQP 中消息的路由过程和 Java 开发者熟悉的 JMS 存在一些差别,AMQP 中增加了 Exchange 和 Binding 的角色。生产者把消息发布到 Exchange 上,消息最终到达队列并被消费者接收,而 Binding 决定交换器的消息应该发送到那个队列。
AMQP 的消息路由过程
Exchange 类型
Exchange分发消息时根据类型的不同分发策略有区别,目前共四种类型:direct、fanout、topic、headers 。headers 匹配 AMQP 消息的 header 而不是路由键,此外 headers 交换器和 direct 交换器完全一致,但性能差很多,目前几乎用不到了,所以直接看另外三种类型:
1,direct
direct 交换器
消息中的路由键(routing key)如果和 Binding 中的 绑定键(binding key) 一致, 交换器就将消息发到对应的队列中。路由键与队列名完全匹配,如果一个队列绑定到交换机要求路由键为“dog”,则只转发 routing key 标记为“dog”的消息,不会转发“dog.puppy”,也不会转发“dog.guard”等等。它是完全匹配、单播的模式。
2,fanout
fanout 交换器
可以看到,作为交换机,当消息从生产者传递至交换机(Exchange)时,交换机会将消息复制,按照绑定规则(Bindings),分别将消息推送至绑定在该交换机(Exchange)上的队列(Queues)中。最终队列拿到交换机传递的消息后,消费者就可以通过消息队列获取生产推送的消息了,而且保证了每个连接不同消息队列的消费者拿到的数据是相同的。要注意,交换机的作用仅仅是用于信息交换,它本身是不具有消息存储的能力的。每个发到 fanout 类型交换器的消息都会分到所有绑定的队列上去。fanout 交换器不处理路由键,只是简单的将队列绑定到交换器上,每个发送到交换器的消息都会被转发到与该交换器绑定的所有队列上。很像子网广播,每台子网内的主机都获得了一份复制的消息。fanout 类型转发消息是最快的。
3,topic
topic 交换器
topic 交换器通过模式匹配分配消息的路由键属性,将路由键和某个模式进行匹配,此时队列需要绑定到一个模式上。它将路由键和绑定键的字符串切分成单词,这些单词之间用点隔开。它同样也会识别两个通配符:符号“ # ” 和符号“ * ”。#匹配0个或多个单词,* 匹配不多不少,只是一个单词。
稍微解释一下上面的交换机制类似于一个国际新闻讯息网站的机制。
可以看到,队列绑定交换机时指定的key为“usa.#”的时候,可以匹配到以“usa.”开头的所有key的消息,所以其匹配到了key为“usa.news”和“usa.weather”,即获取美国的新闻和天气信息。
第二个队列绑定交换机时指定的key为“#.news”的时候,可以匹配到以“.news”结尾的所有key的消息,所以其匹配到了key为“usa.news”和“europe.news”,即获取美国的新闻和欧洲的新闻。
第三个队列绑定交换机时指定的key为“#.weather”的时候,可以匹配到以“.weather”结尾的所有key的消息,所以其匹配到了key为“usa.weather”和“europe.weather”,即获取美国的天气和欧洲的天气。
第四个队列绑定交换机时指定的key为“europe.#”的时候,可以匹配到以“europe。#”开头的所有key的消息,所以其匹配到了key为“europe.news”和“europe.weather”,即获取欧洲的新闻和欧洲的天气。
最后,通配符模式相对于路由模式的好处是,在路由模式中,如果我们要获取某几种类型key的信息,我们要一个一个的指定,而在通配符模式中,我们可以使用通配符,来更加简洁的指定想获取信息的key类型。
简单的总结一下前三种:fanout——往每家每户都发送邮件;direct——往某一户人家发送邮件;topic——往姓张的家里发送邮件。
学习RabbitMQ主要是学习它的6种队列:
这里最后一种我们暂时不学习,因为RPC是远程调用的模式,严格意义上来讲已经脱离了
消息队列的范畴了,所以这里不对该队列模式进行讲解。(想要学习RPC,推荐学习阿里的Dubbo)
1,简单模式
所有 MQ 产品从模型抽象上来说都是一样的过程:
消费者(consumer)订阅某个队列。生产者(producer)创建消息,然后发布到队列(queue)中,最后将消息发送到监听的消费者。
消息流
2,work模式
work的队列模式图如下所示:
可以看到,该模式下有一个生产者,一个队列和多个消费者。
一个生产者将一个消息发送至队列,此时对于多个消费者,只能有一个消费者获取到消息,即是消费者谁先抢到谁拿到该消息。这里就牵扯到了work模式的“能者多劳”机制。对于“能者多劳”机制,自然就是我们一开始所想的,获取信息速度快的人就会拿到的信息多一些。
work模式能够用来做什么呢?即是它的应用场景是什么呢?其实,work模式之所以叫做“工作”模式,就好像是老板给员工分配任务一样,每个人拿到的任务是不同的,谁领到什么任务就做什么任务。
例如某系统作用是写数据到数据库,如果其它系统都去访问它压力会比较大,于是乎就会做一个集群,再部署一个相同的系统,也做写数据这个事情。要求它们写数据的时候是不能写重复的数据的,那么其它系统去调用它的时候,相当于再给它下发任务(发消息),通过work模式,此时集群中两台服务器拿到的任务(消息)不一样,则插入的数据也会不一样,避免了重复插入数据的情况。
3,消息的确认模式
RabbitMQ的消息确认机制是为了确保消息发送者知道自己发布的消息被正确接收,如果没有收到确认时就会认为消息发送过程发送了错误,此时就会马上采取措施,以保证消息能正确送达(类似于HTTP的建立连接时的确认答复)。
具体做法如下:
当RabbitMQ发送消息以后,如果收到消息确认,才将该消息从Quque中移除。如果RabbitMQ没有收到确认,如果检测到消费者的RabbitMQ链接断开,则RabbitMQ 会将该消息发送给其他消费者;否则就会重新再次发送一个消息给消费者。
当消费者从队列中获取消息后,服务端是如何知道自己被消费的呢?在RabbitMQ中服务端确认消息是否被消费成功,有两种确认模式:
(1)自动确认
只要消息从队列中获取,无论消费者获取到消息后是否有成功接收的反馈,都认为是消息已经被成功消费。
(2)手动模式
消费者从队列中获取消息后,服务器会将消息标记为不可用状态,等待消费者的反馈,如果消费者一直没有反馈,那么消息将一直处于不可用状态。
“手动模式”与“自动模式”的区别就是,当我们创建消费者对象并且设置消费者对队列进行监听时,设置的第二个参数的不同,对于自动模式:
//定义队列的消费者
QueueingConsumer consumer = new QueueingConsumer(channel);
//监听队列
channel.basicConsume(QUEUE_NAME, true,consumer);
对于手动模式:
// 定义队列的消费者
QueueingConsumer consumer = new QueueingConsumer(channel);
// 监听队列,手动返回完成
channel.basicConsume(QUEUE_NAME, false, consumer);
上面的两段代码中,basicConsume方法的三个参数分别代表:队列名称、是否自动确认(否将需要手动确认)、接收消息的消费者对象。
所以在定义消费者监听队列时,设置basicConsume方法的第二个参数为true,则使用的是自动模式,下面消费者接收完信息后无需确认。而当第二个参数为false时,下面的消费者接收完信息后需要进行手动编码确认(channel.basicAck...)。
4,订阅模式
在实际开发中,通常会遇到以下需求:
一个生产者,多个消费者,同一个消息被多个消费者获取。
对于“订阅模式”,其模式图如下所示:
上图中,“P”为消息的生产者,而“X”是交换机(Exchange),即消息生产者将消息发送到“交换机”而不再是“队列”,然后需要承载成产者消息的队列,与该交换机进行绑定。余下就是消费者监听相关队列来获取服务端推送的消息,满足多个消费者共同获取同一个生产者的所有消息的情况。
当消费者不需要从生产者获取消息了,将相关消息队列与交换机的绑定关系解除即可。
所以上面的“订阅模式”具有以下特点:
(1)一个生产者,多个消费者
(2)每个消费者都有自己的一个队列
(3)生产者没有将消息直接发送至队列,而是发送到了交换机
(4)每个队列都要绑定到交换机
(5)生产者发送的消息,经过交换机,到达队列,实现一个消息被多个消费者获取的目的。
5,路由模式
其实严格来讲,RabbitMQ只有三种模式:“简单模式”、“work模式”以及“交换机模式”。
对于交换机模式来说,又分三种:“订阅模式”、“路由模式”、“通配符模式”,而他们之间的不同就是交换机类型的不同。
目前在实际开发中我们可能会遇到这种问题,生产者发布到消息队列的信息,消费者不一定全部需要,但是使用订阅模式的话,所有消费者都能够拿到,不满足只拿到自己所需信息的需求。
对于以上的需求,“路由模式”就可以进行实现。
路由模式的图示如下:
还是一个生产者、一个交换机,多个绑定交换机的队列,和多个连接不同队列的消费者。与之前的订阅模式不同的是,这里指定了交换机的类型是“direct”;而消费者将队列绑定到交换机时,指定了一个路由Key(error/info/warning等类型)。
如果生产者发送的消息中的路由key是“error”,那么消费者c1和c2都可以接收到,而如果生产者发送的消息中的路由key是“info”或者“warning”,则只有消费者c2能够收到。
由此可见。“路由模式”的作用就是可以让消费者有选择性的接收消息。
6,通配符模式
而“通配符交换机”与之前的路由模式相比,它将信息的传输类型的key更加细化,以“key1.key2.keyN....”的模式来指定信息传输的key的大类型和大类型下面的小类型,让消费者可以更加精细的确认自己想要获取的信息类型。而在消费者一段,不用精确的指定具体到哪一个大类型下的小类型的key,而是可以使用类似正则表达式(但与正则表达式规则完全不同)的通配符在指定一定范围或符合某一个字符串匹配规则的key,来获取想要的信息。
“通配符交换机”(Topic Exchange)将路由键和某模式进行匹配。此时队列需要绑定在一个模式上。符号“#”匹配一个或多个词,符号“*”仅匹配一个词。因此“audit.#”能够匹配到“audit.irs.corporate”,但是“audit.*”只会匹配到“audit.irs”。(这里与我们一般的正则表达式的“*”和“#”刚好相反,这里我们需要注意一下。)
RabbitMQ的一大特性就是支持消息持久化。但是Rabbit MQ默认是不持久队列、Exchange、Binding以及队列中的消息的,这意味着一旦消息服务器重启,所有已声明的队列,Exchange,Binding以及队列中的消息都会丢失,这是因为支持持久化会对性能造成较大的影响。
什么时候需要持久化?
1.我们根据自己的需求对它们进行持久化(具体方法可以参考官方的API)。
注意:消息是存在队列里的,如果要使得消息能持久化,就必须先使队列持久化。
2.内存紧张时,需要将部分内存中的消息转移到磁盘中。
消息如何刷到磁盘?
1.写入文件前会有一个Buffer,大小为1M,数据在写入文件时,首先会写入到这个Buffer,如果Buffer已满,则会将Buffer写入到文件(未必刷到磁盘)。
2.有个固定的刷盘时间:25ms,也就是不管Buffer满不满,每间隔25ms,Buffer里的数据及未刷新到磁盘的文件内容必定会刷到磁盘。
3.每次消息写入后,如果没有后续写入请求,则会直接将已写入的消息刷到磁盘:使用Erlang的receive x after 0实现,只要进程的信箱里没有消息,则产生一个timeout消息,而timeout会触发刷盘操作。
RabbitMQ中也支持RPC,具体实现如下:
1.客户端发送请求(消息)时,在消息的属性中设置两个值replyTo(用于告诉服务器处理完成后将通知我的消息发送到这个Queue中)和correlationId(此次请求的标识号,服务器处理完成后需要将此属性返还,客户端将根据这个id了解哪条请求被成功执行了或执行失败)
2.服务器端收到消息并处理
3.服务器端处理完消息后,将生成一条应答消息到replyTo指定的Queue,同时带上correlationId属性
4.客户端之前已订阅replyTo指定的Queue,从中收到服务器的应答消息后,根据其中的correlationId属性分析哪条请求被执行了,根据执行结果进行后续业务处理
老板让秘书去买东西,告诉秘书将买到的东西送到他家门口保卫处,并写上自己的名字。
他家门口保卫处——replyTo。
自己的名字——correlationId。
RabbitMQ 最优秀的功能之一就是内建集群,这个功能设计的目的是允许消费者和生产者在节点崩溃的情况下继续运行,以及通过添加更多的节点来线性扩展消息通信吞吐量。RabbitMQ 内部利用 Erlang 提供的分布式通信框架 OTP 来满足上述需求,使客户端在失去一个 RabbitMQ 节点连接的情况下,还是能够重新连接到集群中的任何其他节点继续生产、消费消息。
RabbitMQ 集群中的一些概念
RabbitMQ 会始终记录以下四种类型的内部元数据:
1队列元数据
包括队列名称和它们的属性,比如是否可持久化,是否自动删除
2交换器元数据
交换器名称、类型、属性
3绑定元数据
内部是一张表格记录如何将消息路由到队列
4vhost 元数据
为 vhost 内部的队列、交换器、绑定提供命名空间和安全属性
在单一节点中,RabbitMQ 会将所有这些信息存储在内存中,同时将标记为可持久化的队列、交换器、绑定存储到硬盘上。存到硬盘上可以确保队列和交换器在节点重启后能够重建。而在集群模式下同样也提供两种选择:存到硬盘上(独立节点的默认设置),存在内存中。
如果在集群中创建队列,集群只会在单个节点而不是所有节点上创建完整的队列信息(元数据、状态、内容)。结果是只有队列的所有者节点知道有关队列的所有信息,因此当集群节点崩溃时,该节点的队列和绑定就消失了,并且任何匹配该队列的绑定的新消息也丢失了。还好RabbitMQ 2.6.0之后提供了镜像队列以避免集群节点故障导致的队列内容不可用。
RabbitMQ 集群中可以共享 user、vhost、exchange等,所有的数据和状态都是必须在所有节点上复制的,例外就是上面所说的消息队列。RabbitMQ 节点可以动态的加入到集群中。
当在集群中声明队列、交换器、绑定的时候,这些操作会直到所有集群节点都成功提交元数据变更后才返回。集群中有内存节点和磁盘节点两种类型,内存节点虽然不写入磁盘,但是它的执行比磁盘节点要好。内存节点可以提供出色的性能,磁盘节点能保障配置信息在节点重启后仍然可用,那集群中如何平衡这两者呢?
RabbitMQ 只要求集群中至少有一个磁盘节点,所有其他节点可以是内存节点,当节点加入火离开集群时,它们必须要将该变更通知到至少一个磁盘节点。如果只有一个磁盘节点,刚好又是该节点崩溃了,那么集群可以继续路由消息,但不能创建队列、创建交换器、创建绑定、添加用户、更改权限、添加或删除集群节点。换句话说集群中的唯一磁盘节点崩溃的话,集群仍然可以运行,但知道该节点恢复,否则无法更改任何东西。
首先确保RabbitMQ的服务是启动状态,可以在RabbitMQ的安装目录sbin下使用“rabbitmqctl status”查看RabbitMQ服务的状态:
找到你安装rabbitMQ的路径,然后切换到sbin的文件夹
输入rabbitmq-plugins enable rabbitmq_management命令来启动监控管理器
然后在浏览器输入http:localhost:15672 用户名和密码默认都为guest。
然后下面的“Ports and contexts”展示的是端口信息:
这里一共有三个端口,其中5672是amqp协议的端口,15672是RabbitMQ的管理工具端口(这就是我们现在操作的),25672是做集群的端口。
如果我们现在的Java程序要与RabbitMQ进行交互,需要和哪个端口交互呢?是5672。因为Java客户端需要与RabbitMQ服务进行数据交互,必须要遵循amqp协议协议,所以要走5672端口。
而15672是RabbitMQ的管理工具的端口,与服务无关,仅仅是管理工具运行的端口。
启动监控管理器:rabbitmq-plugins enable rabbitmq_management
关闭监控管理器:rabbitmq-plugins disable rabbitmq_management
启动rabbitmq:rabbitmq-service start
关闭rabbitmq:rabbitmq-service stop
查看所有的队列:rabbitmqctl list_queues
清除所有的队列:rabbitmqctl reset
关闭应用:rabbitmqctl stop_app
启动应用:rabbitmqctl start_app
用户和权限设置(后面用处)
添加用户:rabbitmqctl add_user username password
分配角色:rabbitmqctl set_user_tags username administrator
新增虚拟主机:rabbitmqctl add_vhost vhost_name
将新虚拟主机授权给新用户:rabbitmqctl set_permissions -p vhost_name username '.*' '.*' '.*'
角色说明
none 最小权限角色
management 管理员角色
policymaker 决策者
monitoring 监控
administrator 超级管理员