SpringBoot整合RabbitMQ
Author:jeffrey
Date:2019-04-12
一、环境准备
rabbitMQ是一个在AMQP协议标准基础上完整的,可服用的企业消息系统。它遵循Mozilla Public License开源协议,采用 Erlang 实现的工业级的消息队列(MQ)服务器,Rabbit MQ 是建立在Erlang OTP平台上。
1.1.安装Erlang
所以在安装rabbitMQ之前,需要先安装Erlang 。
官方下载网址:http://www.erlang.org/downloads
下载最新版是otp_win64_21.3,需要其他版本或者32位系统的,可以去官网下载。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-FUyaVuyW-1604366544209)(mq01.png)]
全部点击“下一步”就行。
有的选择其他的安装方式,可能需要添加一下系统环境变量(正常安装的也要检查下):
Set ERLANG_HOME
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-DYWfGhHf-1604366544211)(mq02.png)]
1.2.安装RabbitMQ
官网地址:http://www.rabbitmq.com/download.html
下载运行[rabbitmq-server-3.7.14.exe,需要其他版本或者32位系统的,可以去官网下载。
依旧可以不改变默认进行安装。
需要注意:默认安装的RabbitMQ 监听端口是5672
如果你下载的是zip版,参考:https://www.rabbitmq.com/install-windows-manual.html安装指南
注意:
在搭建RabbitMQ环境过程中,由于版本问题导致环境一直搭建不起来,以下是RabbitMQ与Erlang的版本对应关系
RabbitMQ版本 | Erlang最低要求 | Erlang最高要求 |
---|---|---|
3.7.7 - 3.7.12 | 20.3.x | 21.x |
3.7.0 - 3.7.6 | 19.3 | 20.3.x |
1.3.配置
a、激活 RabbitMQ’s Management Plugin
使用RabbitMQ 管理插件,可以更好的可视化方式查看Rabbit MQ 服务器实例的状态。
打开命令窗口:
输入命令:
"D:\Program Files\RabbitMQ Server\rabbitmq_server-3.7.14\sbin\rabbitmq-plugins.bat" enable rabbitmq_management
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-B4IPGt79-1604366544212)(/mq03.png)]
使用管理员打开cmd再执行此命令:
b、启动RabbitMQ服务
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Bu4uSeTz-1604366544214)(mq04.png)]
c、查看用户列表:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-tRVsexGt-1604366544215)(mq07.png)]
使用浏览器打开 http://localhost:15672 访问Rabbit Mq的管理控制台,使用系统默认的guest账号登陆系统:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-I4SxqwAw-1604366544216)(mq05.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jM7ahhQI-1604366544216)(mq06.png)]
d、创建新用户
用户名:jeffrey 密码:123456
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-AsG7sFdF-1604366544217)(mq08.png)]
注意:如果命令失效
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-EbWBf4v6-1604366544217)(mqe01.png)]
处理如下:
将C:\Windows\system32\config\systemprofile.erlang.cookie中的.erlang.cookie文件,复制一份到错误提示中的地址,我这里是“C:\Users\jeffrey”,该路径下已经有了一个同名文件,覆盖就好了。
e、给新创建用户分配角色:
rabbitmq用户角色可分为五类:超级管理员, 监控者, 策略制定者, 普通管理者以及其他。
(1) 超级管理员(administrator)
可登陆管理控制台(启用management plugin的情况下),可查看所有的信息,并且可以对用户,策略(policy)进行操作。
(2) 监控者(monitoring)
可登陆管理控制台(启用management plugin的情况下),同时可以查看rabbitmq节点的相关信息(进程数,内存使用情况,磁盘使用情况等)
(3) 策略制定者(policymaker)
可登陆管理控制台(启用management plugin的情况下), 同时可以对policy进行管理。
(4) 普通管理者(management)
仅可登陆管理控制台(启用management plugin的情况下),无法看到节点信息,也无法对策略进行管理。
(5) 其他的
无法登陆管理控制台,通常就是普通的生产者和消费者。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-YlNsn5j0-1604366544218)(mq09.png)]
f、使用新用户登录RabbitMQ:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-FIaC0uBR-1604366544219)(mq10.png)]
二、rabbitMQ基本概念**
RabbitMQ是一个开源的AMQP实现,服务器端用Erlang语言编写,支持多种客户端,如:Python、Ruby、.NET、Java、JMS、C、PHP、ActionScript、XMPP、STOMP等,支持AJAX。用于在分布式系统中存储转发消息,在易用性、扩展性、高可用性等方面表现不俗。
了解RabbitMQ,首先学习下AMQP。
AMQP,即Advanced Message Queuing Protocol,高级消息队列协议,是应用层协议的一个开放标准,为面向消息的中间件设计。消息中间件主要用于组件之间的解耦,消息的发送者无需知道消息使用者的存在,反之亦然。
AMQP的主要特征是面向消息、队列、路由(包括点对点和发布/订阅)、可靠性、安全。
2.1 AMQP的协议栈,
AMQP协议本身包含三层,如下:
-
Model Layer,位于协议最高层,主要定义了一些供客户端调用的命令,客户端可以通过这些命令实现自己的业务逻辑,例如,客户端可以通过queue declare声明一个队列,利用consume命令获取队列的消息。
-
Session Layer,主要负责将客户端命令发送给服务器,在将服务器端的应答返回给客户端,主要为客户端与服务器之间通信提供可靠性、同步机制和错误处理。
-
Transport Layer,主要传输二进制数据流,提供帧的处理、信道复用、错误检测和数据表示。
这种分层架构类似于OSI网络协议,可替换各层实现而不影响与其它层的交互。AMQP定义了合适的服务器端域模型,用于规范服务器的行为(AMQP服务器端可称为broker)。在这里Model层决定这些基本域模型所产生的行为,这种行为在AMQP中用command表示。Session层定义客户端与broker之间的通信(通信双方都是一个peer,可互称做partner),为command的可靠传输提供保障。Transport层专注于数据传送,并与Session保持交互,接受上层的数据,组装成二进制流,传送到receiver后再解析数据,交付给Session层。Session层需要Transport层完成网络异常情况的汇报,顺序传送command等工作。
2.2 AMQP当中的概念。
Broker(Server):接受客户端连接,实现AMQP消息队列和路由功能的进程。
Virtual Host:其实是一个虚拟概念,类似于权限控制组,一个Virtual Host里面可以有若干个Exchange和Queue,但是权限控制的最小粒度是Virtual Host。
Exchange:接受生产者发送的消息,并根据Binding规则将消息路由给服务器中的队列。ExchangeType决定了Exchange路由消息的行为,例如,在RabbitMQ中,ExchangeType有direct、Fanout和Topic三种,不同类型的Exchange路由的行为是不一样的。
Message Queue:消息队列,用于存储还未被消费者消费的消息。
Message:由Header和Body组成,Header是由生产者添加的各种属性的集合,包括Message是否被持久化、由哪个Message Queue接受、优先级是多少等。而Body是真正需要传输的APP数据。
Binding:Binding联系了Exchange与Message Queue。Exchange在与多个Message Queue发生Binding后会生成一张路由表,路由表中存储着Message Queue所需消息的限制条件即Binding Key。当Exchange收到Message时会解析其Header得到Routing Key,Exchange根据Routing Key与Exchange Type将Message路由到Message Queue。Binding Key由Consumer在Binding Exchange与Message Queue时指定,而Routing Key由Producer发送Message时指定,两者的匹配方式由Exchange Type决定。
Connection:连接,对于RabbitMQ而言,其实就是一个位于客户端和Broker之间的TCP连接。
Channel:信道,仅仅创建了客户端到Broker之间的连接后,客户端还是不能发送消息的。需要为每一个Connection创建Channel,AMQP协议规定只有通过Channel才能执行AMQP的命令。一个Connection可以包含多个Channel。之所以需要Channel,是因为TCP连接的建立和释放都是十分昂贵的,如果一个客户端每一个线程都需要与Broker交互,如果每一个线程都建立一个TCP连接,暂且不考虑TCP连接是否浪费,就算操作系统也无法承受每秒建立如此多的TCP连接。RabbitMQ建议客户端线程之间不要共用Channel,至少要保证共用Channel的线程发送消息必须是串行的,但是建议尽量共用Connection。
Command:AMQP的命令,客户端通过Command完成与AMQP服务器的交互来实现自身的逻辑。例如在RabbitMQ中,客户端可以通过publish命令发送消息,txSelect开启一个事务,txCommit提交一个事务。
消息中间件的主要功能是消息的路由(Routing)和缓存(Buffering)。在AMQP中提供类似功能的两种域模型:Exchange 和 Message queue。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-lCwP5LVJ-1604366544220)(C:\Users\jeffrey\Desktop\新讲义\11_springboot_rabbitmq\doc\mm01.jpg)]
Exchange接收消息生产者(Message Producer)发送的消息根据不同的路由算法将消息发送往Message queue。Message queue会在消息不能被正常消费时缓存这些消息,具体的缓存策略由实现者决定,当message queue与消息消费者(Message consumer)之间的连接通畅时,Message queue有将消息转发到consumer的责任。
一个Message的处理流程类似于下图:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-F5wp1sKV-1604366544221)(C:\Users\jeffrey\Desktop\新讲义\11_springboot_rabbitmq\doc\mm02.jpg)]
Message是当前模型中所操纵的基本单位,它由Producer产生,经过Broker被Consumer所消费。它的基本结构有两部分: Header和Body。Header是由Producer添加上的各种属性的集合,这些属性有控制Message是否可被缓存,接收的queue是哪个,优先级是多少等。Body是真正需要传送的数据,它是对Broker不可见的二进制数据流,在传输过程中不应该受到影响。
一个broker中会存在多个Message queue,Exchange怎样知道它要把消息发送到哪个Message queue中去呢? 这就是上图中所展示Binding的作用。Message queue的创建是由client application控制的,在创建Message queue后需要确定它来接收并保存哪个Exchange路由的结果。Binding是用来关联Exchange与Message queue的域模型。Client application控制Exchange与某个特定Message queue关联,并将这个queue接受哪种消息的条件绑定到Exchange,这个条件也叫Binding key或是 Criteria。
在与多个Message queue关联后,Exchange中就会存在一个路由表,这个表中存储着每个Message queue所需要消息的限制条件。Exchange就会检查它接受到的每个Message的Header及Body信息,来决定将Message路由到哪个queue中去。Message的Header中应该有个属性叫Routing Key,它由Message发送者产生,提供给Exchange路由这条Message的标准。Exchange根据不同路由算法有不同有Exchange Type。比如有Direct类似,需要Binding key等于Routing key;也有Binding key与Routing key符合一个模式关系;也有根据Message包含的某些属性来判断。一些基础的路由算法由AMQP所提供,client application也可以自定义各种自己的扩展路由算法。
在AMQP中,Client application想要与Broker沟通,就需要建立起与Broker的connection,这种connection其实是与Virtual Host相关联的,也就是说,connection是建立在client与Virtual Host之间。可以在一个connection上并发运行多个channel,每个channel执行与Broker的通信,我们前面提供的session就是依附于channel上的。
这里的Session可以有多种定义,既可以表示AMQP内部提供的command分发机制,也可以说是在宏观上区别与域模型的接口。正常理解就是我们平时所说的交互context,主要作用就是在网络上可靠地传递每一个command。在AMQP的设计中,应当是借鉴了TCP的各种设计,用于保证这种可靠性。
在Session层,为上层所需要交互的每个command分配一个惟一标识符(可以是一个UUID),是为了在传输过程中可以对command做校验和重传。Command发送端也需要记录每个发送出去的command到Replay Buffer,以期得到接收方的回馈,保证这个command被接收方明确地接收或是已执行这个command。对于超时没有收到反馈的command,发送方再次重传。如果接收方已明确地回馈信息想要告知command发送方但这条信息在中途丢失或是其它问题发送方没有收到,那么发送方不断重传会对接收方产生影响,为了降低这种影响,command接收方设置一个过滤器Idempotency Barrier,来拦截那些已接收过的command。 关于这种重传及确认机制,可以参考下TCP的相关设计。
2.3 RabbitMQ核心接口
(1)ConnectionFactory、Connection、Channel
ConnectionFactory、Connection、Channel都是RabbitMQ对外提供的API中最基本的对象。Connection是RabbitMQ的socket链接,它封装了socket协议相关部分逻辑。ConnectionFactory为Connection的制造工厂。Channel是我们与RabbitMQ打交道的最重要的一个接口,我们大部分的业务操作是在Channel这个接口中完成的,包括定义Queue、定义Exchange、绑定Queue与Exchange、发布消息等。
(2)Queue
Queue(队列)是RabbitMQ的内部对象,用于存储消息,用下图表示。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-nk0hHOYT-1604366544221)(C:\Users\jeffrey\Desktop\新讲义\11_springboot_rabbitmq\doc\queue01.png)]
RabbitMQ中的消息都只能存储在Queue中,生产者(下图中的P)生产消息并最终投递到Queue中,消费者(下图中的C)可以从Queue中获取消息并消费。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-HYggaM1t-1604366544222)(C:\Users\jeffrey\Desktop\新讲义\11_springboot_rabbitmq\doc\queue02.png)]
多个消费者可以订阅同一个Queue,这时Queue中的消息会被平均分摊给多个消费者进行处理,而不是每个消费者都收到所有的消息并处理。
(3)Message acknowledgment
在实际应用中,可能会发生消费者收到Queue中的消息,但没有处理完成就宕机(或出现其他意外)的情况,这种情况下就可能会导致消息丢失。为了避免这种情况发生,我们可以要求消费者在消费完消息后发送一个回执给RabbitMQ,RabbitMQ收到消息回执(Message acknowledgment)后才将该消息从Queue中移除;如果RabbitMQ没有收到回执并检测到消费者的RabbitMQ连接断开,则RabbitMQ会将该消息发送给其他消费者(如果存在多个消费者)进行处理。这里不存在timeout概念,一个消费者处理消息时间再长也不会导致该消息被发送给其他消费者,除非它的RabbitMQ连接断开。这里会产生另外一个问题,如果我们的开发人员在处理完业务逻辑后,忘记发送回执给RabbitMQ,这将会导致严重的bug——Queue中堆积的消息会越来越多;消费者重启后会重复消费这些消息并重复执行业务逻辑,另外pub message是没有ack的。
(4)Message durability
如果我们希望即使在RabbitMQ服务重启的情况下,也不会丢失消息,我们可以将Queue与Message都设置为可持久化的(durable),这样可以保证绝大部分情况下我们的RabbitMQ消息不会丢失。但依然解决不了小概率丢失事件的发生(比如RabbitMQ服务器已经接收到生产者的消息,但还没来得及持久化该消息时RabbitMQ服务器就断电了),如果我们需要对这种小概率事件也要管理起来,那么我们要用到事务。
(5)Prefetch count
前面我们讲到如果有多个消费者同时订阅同一个Queue中的消息,Queue中的消息会被平摊给多个消费者。这时如果每个消息的处理时间不同,就有可能会导致某些消费者一直在忙,而另外一些消费者很快就处理完手头工作并一直空闲的情况。我们可以通过设置prefetchCount来限制Queue每次发送给每个消费者的消息数,比如我们设置prefetchCount=1,则Queue每次给每个消费者发送一条消息;消费者处理完这条消息后Queue会再给该消费者发送一条消息。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-RpzA7or3-1604366544223)(C:\Users\jeffrey\Desktop\新讲义\11_springboot_rabbitmq\doc\queue01.png)]
(6)Exchange
在之前我们看到生产者将消息投递到Queue中,实际上这在RabbitMQ中这种事情永远都不会发生。实际的情况是,生产者将消息发送到Exchange(交换器,下图中的X),由Exchange将消息路由到一个或多个Queue中(或者丢弃)。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-i6SBP392-1604366544224)(C:\Users\jeffrey\Desktop\新讲义\11_springboot_rabbitmq\doc\exchange01.png)]
(7)routing key
生产者在将消息发送给Exchange的时候,一般会指定一个routing key,来指定这个消息的路由规则,而这个routing key需要与Exchange Type及binding key联合使用才能最终生效。在Exchange Type与binding key固定的情况下(在正常使用时一般这些内容都是固定配置好的),我们的生产者就可以在发送消息给Exchange时,通过指定routing key来决定消息流向哪里。
RabbitMQ为routing key设定的长度限制为255 bytes。
(8)Binding
RabbitMQ中通过Binding将Exchange与Queue关联起来,这样RabbitMQ就知道如何正确地将消息路由到指定的Queue了。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-49Qkwys9-1604366544225)(binding01.png)]
(9)Binding key
在绑定(Binding)Exchange与Queue的同时,一般会指定一个binding key;消费者将消息发送给Exchange时,一般会指定一个routing key;当binding key与routing key相匹配时,消息将会被路由到对应的Queue中。在绑定多个Queue到同一个Exchange的时候,这些Binding允许使用相同的binding key。binding key 并不是在所有情况下都生效,它依赖于Exchange Type,比如fanout类型的Exchange就会无视binding key,而是将消息路由到所有绑定到该Exchange的Queue。
(10)Exchange Types
RabbitMQ常用的Exchange Type有fanout、direct、topic、headers这四种,下面分别进行介绍。
fanout:fanout类型的Exchange路由规则非常简单,它会把所有发送到该Exchange的消息路由到所有与它绑定的Queue中。下图中,生产者(P)发送到Exchange(X)的所有消息都会路由到图中的两个Queue,并最终被两个消费者(C1与C2)消费。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Gc7zdyYi-1604366544225)(C:\Users\jeffrey\Desktop\新讲义\11_springboot_rabbitmq\doc\Fanout01.png)]
direct:direct类型的Exchange路由规则也很简单,它会把消息路由到那些binding key与routing key完全匹配的Queue中。以上图的配置为例,我们以routingKey=”error”发送消息到Exchange,则消息会路由到Queue1(amqp.gen-S9b…,这是由RabbitMQ自动生成的Queue名称)和Queue2(amqp.gen-Agl…);如果我们以routingKey=”info”或routingKey=”warning”来发送消息,则消息只会路由到Queue2。如果我们以其他routingKey发送消息,则消息不会路由到这两个Queue中。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-EvELVVVT-1604366544226)(direct02.png)]
topic:前面讲到direct类型的Exchange路由规则是完全匹配binding key与routing key,但这种严格的匹配方式在很多情况下不能满足实际业务需求。topic类型的Exchange在匹配规则上进行了扩展,它与direct类型的Exchage相似,也是将消息路由到binding key与routing key相匹配的Queue中,但这里的匹配规则有些不同,它约定:routing key为一个句点号“. ”分隔的字符串(我们将被句点号“. ”分隔开的每一段独立的字符串称为一个单词),如“stock.usd.nyse”、“nyse.vmw”、“quick.orange.rabbit”;binding key与routing key一样也是句点号“. ”分隔的字符串;binding key中可以存在两种特殊字符“”与“#”,用于做模糊匹配,其中“”用于匹配一个单词,“#”用于匹配多个单词(可以是零个)。
以下图中的配置为例,routingKey=”quick.orange.rabbit”的消息会同时路由到Q1与Q2,routingKey=”lazy.orange.fox”的消息会路由到Q1与Q2,routingKey=”lazy.brown.fox”的消息会路由到Q2,routingKey=”lazy.pink.rabbit”的消息会路由到Q2(只会投递给Q2一次,虽然这个routingKey与Q2的两个bindingKey都匹配);routingKey=”quick.brown.fox”、routingKey=”orange”、routingKey=”quick.orange.male.rabbit”的消息将会被丢弃,因为它们没有匹配任何bindingKey。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-X10vWDLa-1604366544226)(C:\Users\jeffrey\Desktop\新讲义\11_springboot_rabbitmq\doc\topic01.png)]
headers:headers类型的Exchange不依赖于routing key与binding key的匹配规则来路由消息,而是根据发送的消息内容中的headers属性进行匹配。在绑定Queue与Exchange时指定一组键值对;当消息发送到Exchange时,RabbitMQ会取到该消息的headers(也是一个键值对的形式),对比其中的键值对是否完全匹配Queue与Exchange绑定时指定的键值对;如果完全匹配则消息会路由到该Queue,否则不会路由到该Queue。
(11)RPC
MQ本身是基于异步的消息处理,前面的示例中所有的生产者(P)将消息发送到RabbitMQ后不会知道消费者(C)处理成功或者失败(甚至连有没有消费者来处理这条消息都不知道)。但实际的应用场景中,我们很可能需要一些同步处理,需要同步等待服务端将我的消息处理完成后再进行下一步处理。这相当于RPC(Remote Procedure Call,远程过程调用)。在RabbitMQ中也支持RPC。如下图
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-h6KxLGNz-1604366544227)(rpc01.png)]
RabbitMQ中实现RPC的机制是:客户端发送请求(消息)时,在消息的属性(MessageProperties,在AMQP协议中定义了14中properties,这些属性会随着消息一起发送)中设置两个值replyTo(一个Queue名称,用于告诉服务器处理完成后将通知我的消息发送到这个Queue中)和correlationId(此次请求的标识号,服务器处理完成后需要将此属性返还,客户端将根据这个id了解哪条请求被成功执行了或执行失败)。服务器端收到消息并处理。服务器端处理完消息后,将生成一条应答消息到replyTo指定的Queue,同时带上correlationId属性。客户端之前已订阅replyTo指定的Queue,从中收到服务器的应答消息后,根据其中的correlationId属性分析哪条请求被执行了,根据执行结果进行后续业务处理
三、常用命令
创建一个用户为mytest,密码为mytest
rabbitmqctl add_user mytest mytest
删除一个用户
rabbitmqctl delete_user username
修改用户的密码
rabbitmqctl change_password username newpassword
查看当前用户列表
rabbitmqctl list_users
设置用户角色(user为用户名, tag为角色名(对应administrator,monitoring,policymaker,management,或其他自定义名称)
rabbitmqctl set_user_tags user tag tag tag
设置用户权限(给用户mytest 设置所有资源都可以读写权限)
rabbitmqctl set_permissions -p / mytest ‘.’ '.’ ‘.*’(配置权限的正则,写权限的正则,读全新的正则)
查看(指定vhostpath)所有用户的权限信息
rabbitmqctl list_permissions -p /
查看某个指定用户的权限信息
rabbitmqctl list_user_permissions
清除某个用户的权限信息
rabbitmqctl clear_permissions [-p vhostpath] username
四、springboot整合rabbitmq开发
4.1 创建maven工程并添加rabbitmq包依赖
pom.xml
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!--rabbit依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
<version>2.1.3.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
4.2 配置rabbitmq
application.properties
spring.application.name=springboot-rabbitmq
spring.rabbitmq.host=127.0.0.1
spring.rabbitmq.port=5672
spring.rabbitmq.username=guest
spring.rabbitmq.password=guest
spring.rabbitmq.publisher-confirms=true
spring.rabbitmq.virtual-host=/
4.3 初始化创建队列、转发器,并把队列绑定到转发器
RabbitMQConfig.java
package com.jeffrey.config;
import com.rabbitmq.client.AMQP;
import org.springframework.amqp.core.*;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* Created by jeffrey on 2019/4/13.
*/
@Configuration
public class RabbitMQConfig {
@Bean
public Queue helloQueue(){
return new Queue("helloQueue");
}
@Bean
public Queue userQueue(){
return new Queue("user");
}
/*-------验证topic Exchange的队列-------*/
@Bean
public Queue queueMessage(){
return new Queue("topic.message");
}
@Bean
public Queue queueMessages(){
return new Queue("topic.messages");
}
/*---------验证fanout Exchange的队列----------*/
@Bean
public Queue AMessage(){
return new Queue("fanout.A");
}
@Bean
public Queue BMessage(){
return new Queue("fanout.B");
}
@Bean
public Queue CMessage(){
return new Queue("fanout.C");
}
@Bean
public TopicExchange exchange(){
return new TopicExchange("exchange");
}
@Bean
public FanoutExchange fanoutExchange(){
return new FanoutExchange("fanoutExchange");
}
/**
* 将队列topic.message与exchange绑定,bingding_key为topic.message,就是完全匹配
* @param queueMessage
* @param exchange
* @return
*/
@Bean
public Binding bindingExchangeMessage(Queue queueMessage,TopicExchange exchange){
return BindingBuilder.bind(queueMessage).to(exchange).with("topic.message");
}
/**
* 将队列topic.messages与exchange绑定,binding_key为topic.#,模糊匹配
* @param queueMessage
* @param exchange
* @return
*/
@Bean
public Binding bindingExchangeMessages(Queue queueMessage,TopicExchange exchange){
return BindingBuilder.bind(queueMessage).to(exchange).with("topic.#");
}
@Bean
public Binding bindingExchangeA(Queue AMessage ,FanoutExchange fanoutExchange){
return BindingBuilder.bind(AMessage).to(fanoutExchange);
}
@Bean
public Binding bindingExchangeB(Queue BMessage ,FanoutExchange fanoutExchange){
return BindingBuilder.bind(BMessage).to(fanoutExchange);
}
@Bean
public Binding bindingExchange(Queue CMessage ,FanoutExchange fanoutExchange){
return BindingBuilder.bind(CMessage).to(fanoutExchange);
}
}
4.4、各种情景实现
1)、最简单的hello生产和消费实现(单生产者和单消费者)
生产者:
package com.jeffrey.hello;
import org.springframework.amqp.core.AmqpTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.Date;
/**
* Created by jeffrey on 2019/4/13.
*/
@Component
public class HelloSender1 {
@Autowired
private AmqpTemplate rabbitTemplate;
public void send(){
String sendMsg = "hello"+ new Date();
System.out.println("sender1:"+sendMsg);
rabbitTemplate.convertAndSend("helloQueue",sendMsg);
}
}
消费者:
package com.jeffrey.hello;
import org.springframework.amqp.rabbit.annotation.RabbitHandler;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;
/**
* Created by jeffrey on 2019/4/13.
*/
@Component
@RabbitListener(queues="helloQueue")
public class HelloReceiver1 {
@RabbitHandler
public void process(String hello){
System.out.println("Receiver1:"+hello);
}
}
控制器:
package com.jeffrey.controller;
import com.jeffrey.hello.HelloSender1;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* Created by jeffrey on 2019/4/13.
*/
@RestController
@RequestMapping("/rabbit")
public class HelloRabbitController {
@Autowired
private HelloSender1 helloSender1;
@RequestMapping("/hello")
public void hello(){
helloSender1.send();
}
}
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jbjXH03O-1604366544228)(mq11.png)]
2)、单生产者-多消费者
生产者:
package com.jeffrey.hello;
import org.springframework.amqp.core.AmqpTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.Date;
/**
* Created by jeffrey on 2019/4/13.
*/
@Component
public class HelloSender1 {
@Autowired
private AmqpTemplate rabbitTemplate;
public void send(String msg){
// String sendMsg = "hello"+ new Date();
String sendMsg = msg + new Date();
System.out.println("sender1:"+sendMsg);
rabbitTemplate.convertAndSend("helloQueue",sendMsg);
}
}
消费者1:
package com.jeffrey.hello;
import org.springframework.amqp.rabbit.annotation.RabbitHandler;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;
/**
* Created by jeffrey on 2019/4/13.
*/
@Component
@RabbitListener(queues="helloQueue")
public class HelloReceiver1 {
@RabbitHandler
public void process(String hello){
System.out.println("Receiver1:"+hello);
}
}
消费者2:
package com.jeffrey.hello;
import org.springframework.amqp.rabbit.annotation.RabbitHandler;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;
/**
* Created by jeffrey on 2019/4/13.
*/
@Component
@RabbitListener(queues = "helloQueue")
public class HelloReceiver2 {
@RabbitHandler
public void process(String hello){
System.out.println("Receiver2:"+hello);
}
}
controller:
package com.jeffrey.controller;
import com.jeffrey.hello.HelloSender1;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* Created by jeffrey on 2019/4/13.
*/
@RestController
@RequestMapping("/rabbit")
public class HelloRabbitController {
@Autowired
private HelloSender1 helloSender1;
@RequestMapping("/hello")
public void hello(){
helloSender1.send("hello1");
}
@RequestMapping("/one2many")
public void one2many(){
for (int i=0;i<10;i++){
helloSender1.send("helloMsg:"+i);
}
}
}
用get方式执行:
http://localhost:8080/rabbit/one2many
结果如下:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-WOhH9Va7-1604366544228)(mq12.png)]
3)、多生产者-多消费者
生产者2:
package com.jeffrey.hello;
import org.springframework.amqp.core.AmqpTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Controller;
import java.util.Date;
/**
* Created by jeffrey on 2019/4/13.
*/
@Component
public class HelloSender2 {
@Autowired
private AmqpTemplate rabbitTemplate;
public void send(String msg){
String sendMsg = msg + new Date();
System.out.println("sender2:"+sendMsg);
rabbitTemplate.convertAndSend("helloQueue",sendMsg);
}
}
controller:
package com.jeffrey.controller;
import com.jeffrey.hello.HelloReceiver2;
import com.jeffrey.hello.HelloSender1;
import com.jeffrey.hello.HelloSender2;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* Created by jeffrey on 2019/4/13.
*/
@RestController
@RequestMapping("/rabbit")
public class HelloRabbitController {
@Autowired
private HelloSender1 helloSender1;
@RequestMapping("/hello")
public void hello(){
helloSender1.send("hello1");
}
@RequestMapping("/one2many")
public void one2many(){
for (int i=0;i<10;i++){
helloSender1.send("helloMsg:"+i);
}
}
@Autowired
private HelloSender2 helloSender2;
@RequestMapping("/many2many")
public void many2many(){
for (int i=0;i<10;i++){
helloSender1.send("helloMsg:"+i);
helloSender2.send("helloMsg:"+i);
}
}
}
用get方式执行:
http://127.0.0.1:8080/rabbit/many2many
结果如下:
sender1:helloMsg:0Sat Apr 13 21:47:43 CST 2019
sender2:helloMsg:0Sat Apr 13 21:47:43 CST 2019
sender1:helloMsg:1Sat Apr 13 21:47:43 CST 2019
sender2:helloMsg:1Sat Apr 13 21:47:43 CST 2019
sender1:helloMsg:2Sat Apr 13 21:47:43 CST 2019
sender2:helloMsg:2Sat Apr 13 21:47:43 CST 2019
sender1:helloMsg:3Sat Apr 13 21:47:43 CST 2019
sender2:helloMsg:3Sat Apr 13 21:47:43 CST 2019
sender1:helloMsg:4Sat Apr 13 21:47:43 CST 2019
Receiver1:helloMsg:0Sat Apr 13 21:47:43 CST 2019
Receiver2:helloMsg:0Sat Apr 13 21:47:43 CST 2019
sender2:helloMsg:4Sat Apr 13 21:47:43 CST 2019
sender1:helloMsg:5Sat Apr 13 21:47:43 CST 2019
sender2:helloMsg:5Sat Apr 13 21:47:43 CST 2019
Receiver2:helloMsg:1Sat Apr 13 21:47:43 CST 2019
Receiver1:helloMsg:1Sat Apr 13 21:47:43 CST 2019
sender1:helloMsg:6Sat Apr 13 21:47:43 CST 2019
Receiver2:helloMsg:2Sat Apr 13 21:47:43 CST 2019
Receiver1:helloMsg:2Sat Apr 13 21:47:43 CST 2019
Receiver2:helloMsg:3Sat Apr 13 21:47:43 CST 2019
Receiver1:helloMsg:3Sat Apr 13 21:47:43 CST 2019
sender2:helloMsg:6Sat Apr 13 21:47:43 CST 2019
Receiver1:helloMsg:4Sat Apr 13 21:47:43 CST 2019
sender1:helloMsg:7Sat Apr 13 21:47:43 CST 2019
Receiver1:helloMsg:5Sat Apr 13 21:47:43 CST 2019
Receiver2:helloMsg:4Sat Apr 13 21:47:43 CST 2019
sender2:helloMsg:7Sat Apr 13 21:47:43 CST 2019
Receiver1:helloMsg:6Sat Apr 13 21:47:43 CST 2019
Receiver2:helloMsg:5Sat Apr 13 21:47:43 CST 2019
Receiver2:helloMsg:6Sat Apr 13 21:47:43 CST 2019
sender1:helloMsg:8Sat Apr 13 21:47:43 CST 2019
Receiver1:helloMsg:7Sat Apr 13 21:47:43 CST 2019
Receiver2:helloMsg:7Sat Apr 13 21:47:43 CST 2019
sender2:helloMsg:8Sat Apr 13 21:47:43 CST 2019
sender1:helloMsg:9Sat Apr 13 21:47:43 CST 2019
sender2:helloMsg:9Sat Apr 13 21:47:43 CST 2019
Receiver1:helloMsg:8Sat Apr 13 21:47:43 CST 2019
Receiver1:helloMsg:9Sat Apr 13 21:47:43 CST 2019
Receiver2:helloMsg:8Sat Apr 13 21:47:43 CST 2019
Receiver2:helloMsg:9Sat Apr 13 21:47:43 CST 2019
4)、实体类传输
springboot完美的支持对象的发送和接收,勿需要格外的配置。
实体类(必须实现序列化接口):
package com.jeffrey.user;
import lombok.Data;
import java.io.Serializable;
/**
* Created by jeffrey on 2019/4/13.
*/
@Data
public class User implements Serializable{
private String username;
private String password;
}
生产者:
package com.jeffrey.user;
import org.springframework.amqp.core.AmqpTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
/**
* Created by jeffrey on 2019/4/13.
*/
@Component
public class UserSender {
@Autowired
private AmqpTemplate rabbitTempate;
public void send(){
User user = new User();
user.setUsername("jeffrey");
user.setPassword("123456");
System.out.println("User sender :"+ user);
rabbitTempate.convertAndSend("userQueue",user);
}
}
消费者:
package com.jeffrey.user;
import org.springframework.amqp.rabbit.annotation.RabbitHandler;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;
/**
* Created by jeffrey on 2019/4/13.
*/
@Component
@RabbitListener(queues = "userQueue")
public class UserReceiver {
@RabbitHandler
public void process(User user){
System.out.println(user);
}
}
controller:
@Autowired
private UserSender userSender;
@RequestMapping("/user")
public void user(){
userSender.send();
}
用get方式执行:
http://127.0.0.1:8080/rabbit/user
结果如下:
User sender :User(username=jeffrey, password=123456)
UserReceiver:User(username=jeffrey, password=123456)
5)、topic ExChange示例
topic 是RabbitMQ中最灵活的一种方式,可以根据binding_key自由的绑定不同的队列
任何发送到Topic Exchange的消息都会被转发到所有关心RouteKey中指定话题的Queue上
1、这种模式较为复杂,简单来说,就是每个队列都有其关心的主题,所有的消息都带有一个“标题”(RouteKey),Exchange会将消息转发到所有关注主题能与RouteKey模糊匹配的队列。
2、这种模式需要RouteKey,也许要提前绑定Exchange与Queue。
3、在进行绑定时,要提供一个该队列关心的主题,如“#.log.#”表示该队列关心所有涉及log的消息(一个RouteKey为”MQ.log.error”的消息会被转发到该队列)。
4、“#”表示0个或若干个关键字,“”表示一个关键字。如“log.”能与“log.warn”匹配,无法与“log.warn.timeout”匹配;但是“log.#”能与上述两者匹配。
5、同样,如果Exchange没有发现能够与RouteKey匹配的Queue,则会抛弃此消息。
首先对topic规则配置,这里使用两个队列来测试(也就是在Application类中创建和绑定的topic.message和topic.messages两个队列),其中topic.message的bindting_key为
“topic.message”,topic.messages的binding_key为“topic.#”;
生产者:
6)、fanout ExChange示例
Fanout 就是我们熟悉的广播模式或者订阅模式,给Fanout转发器发送消息,绑定了这个转发器的所有队列都收到这个消息。
任何发送到Fanout Exchange的消息都会被转发到与该Exchange绑定(Binding)的所有Queue上。
1、可以理解为路由表的模式
2、这种模式不需要RouteKey
3、这种模式需要提前将Exchange与Queue进行绑定,一个Exchange可以绑定多个Queue,一个Queue可以同多个Exchange进行绑定。
4、一个发送到交换机的消息都会被转发到与该交换机绑定的所有队列上。很像子网广播,每台子网内的主机都获得了一份复制的消息。
5、如果接受到消息的Exchange没有与任何Queue绑定,则消息会被抛弃。
6、Fanout交换机转发消息是最快的。
这里使用三个队列来测试(也就是在Application类中创建和绑定的fanout.A、fanout.B、fanout.C)这三个队列都和Application中创建的fanoutExchange转发器绑定。
生产者:
package com.jeffrey.fanout;
import org.springframework.amqp.core.AmqpTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
/**
* Created by jeffrey on 2019/4/14.
*/
@Component
public class FanoutSender {
@Autowired
private AmqpTemplate amqpTemplate;
public void send(){
String msg = "fanoutSender: hello I am fanout";
System.out.println(msg);
amqpTemplate.convertAndSend("fanoutExchange","abcd.ee",msg);
}
}
消费者A:
package com.jeffrey.fanout;
import org.springframework.amqp.rabbit.annotation.RabbitHandler;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;
/**
* Created by jeffrey on 2019/4/14.
*/
@Component
@RabbitListener(queues = "fanout.A")
public class FanoutReceiverA {
@RabbitHandler
public void process(String msg){
System.out.println("fanoutReceiverA:"+ msg);
}
}
消费者B、消费者C与消费者A写法一样,只是侦听的队列不同。
在HelloRabbitController.java中添加:
@Autowired
private FanoutSender fanoutSender;
@RequestMapping("/fanout")
public void famout(){
fanoutSender.send();
}
用get方式执行:
http://127.0.0.1:8080/rabbit/fanout
结果如下:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-TDyk9h7N-1604366544229)(/mq13.png)]
7)、消息发送确认( 带callback的消息发送)
增加回调处理,这里不再使用application.properties默认配置的方式,会在程序中显示的使用文件中的配置信息。该示例中没有新建队列和exchange,用的是第5节中的topic.messages队列和exchange转发器。消费者也是第5节中的topicMessagesReceiver
rabbitmq配置类:
package com.jeffrey.callback;
import org.springframework.amqp.core.Binding;
import org.springframework.amqp.core.BindingBuilder;
import org.springframework.amqp.core.Queue;
import org.springframework.amqp.core.TopicExchange;
import org.springframework.amqp.rabbit.connection.CachingConnectionFactory;
import org.springframework.amqp.rabbit.connection.ConnectionFactory;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.beans.factory.config.ConfigurableBeanFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Scope;
/**
* Created by jeffrey on 2019/10/22.
*/
@Configuration
public class RabbbitConfig {
@Value("${spring.rabbitmq.host}")
private String host;
@Value("${spring.rabbitmq.port}")
private String port;
@Value("${spring.rabbitmq.username}")
private String username;
@Value("${spring.rabbitmq.password}")
private String password;
@Value("${spring.rabbitmq.virtual-host}")
private String virtualHost;
@Value("${spring.rabbitmq.publisher-confirms}")
private boolean publisherConfirms;
// 定义一个消息队列
@Bean
public Queue queueMessages(){
return new Queue("topic.messages");
}
// 定义一个TopicExchange
@Bean
public TopicExchange exchange(){
return new TopicExchange("exchange");
}
@Bean
public Binding bindingExcahngeMessages(Queue queueMessages,TopicExchange exchange){
return BindingBuilder.bind(queueMessages).to(exchange).with("topic.#");
}
@Bean
public ConnectionFactory connectionFactory(){
CachingConnectionFactory connectionFactory = new CachingConnectionFactory();
connectionFactory.setAddresses(host+":"+port);
connectionFactory.setUsername(username);
connectionFactory.setPassword(password);
connectionFactory.setVirtualHost(virtualHost);
connectionFactory.setPublisherConfirms(publisherConfirms);
return connectionFactory;
}
@Bean
//因为要设置回调类,所以应是prototype类型,如果是singleton类型,则回调类为最后一次设置
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public RabbitTemplate rabbitTemplatenew(){
RabbitTemplate rabbitTemplate = new RabbitTemplate(connectionFactory());
return rabbitTemplate;
}
}
消息发送者:
package com.jeffrey.callback;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.amqp.rabbit.connection.CorrelationData;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.UUID;
/**
* Created by jeffrey on 2019/10/22.
*/
@Component
public class CallbackSender implements RabbitTemplate.ConfirmCallback {
Logger log = LoggerFactory.getLogger(CallbackSender.class);
@Autowired
private RabbitTemplate rabbitTemplate;
public void send(){
rabbitTemplate.setConfirmCallback(this);
String msg = "Callback sender: I am callback sender";
log.info(msg);
CorrelationData correlationData = new CorrelationData(UUID.randomUUID().toString());
log.info("callbacksender UUID:"+correlationData);
this.rabbitTemplate.convertAndSend("exchange","topic.messages",msg,correlationData);
}
@Override
public void confirm(CorrelationData correlationData, boolean ack, String cause) {
log.error("callback confirm:"+correlationData.getId());
}
}
消息接收者:
package com.jeffrey.callback;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;
/**
* Created by jeffrey on 2019/10/22.
*/
@Component
public class CallbackReceiver {
Logger log =LoggerFactory.getLogger(CallbackReceiver.class);
@RabbitListener(queues = "topic.messages")
public void process(String msg){
log.info("callback receicer:"+msg);
}
}