基于springboot整合的rabbitmq
概述
详细
RabbitMQ官方解释:
消息系统允许软件、应用相互连接和扩展。这些应用可以相互链接起来组成一个更大的应用,或者将用户设备和数据
进行连接。消息系统通过将消息的发送和接收分离来实现应用程序的异步和解偶。
我们白话文的理解就是:是一个消息代理 - 一个消息系统的媒介。它可以为你的应用提供一个通用的消息发送和接收平台,并且保证消息在传输过程中的安全。
一、RabbitMQ模型简介
AMQP 的工作过程如下图:消息(message)被发布者(publisher)发送给交换机(exchange),交换机常常被比喻成邮局或者邮箱。然后交换机将收到的消息根据路由规则分发给绑定的队列(queue)。最后AMQP代理会将消息投递给订阅了此队列的消费者,或者消费者按照需求自行获取。
二、RabbitMQ 交换机:
Name | Default pre-declared names |
---|---|
Direct exchange | (Empty string) and amq.direct |
Fanout exchange | amq.fanout |
Topic exchange | amq.topic |
Headers exchange | amq.match (and amq.headers in RabbitMQ) |
1. 默认交换机:
default exchange实际上是一个由消息代理预先声明好的没有名字(名字为空字符串)的直连交换机(direct exchange)。它有一个特殊的属性使得它对于简单应用特别有用处:那就是每个新建队列(queue)都会自动绑定到默认交换机上,绑定的路由键(routing key)名称与队列名称相同。
如:当你声明了一个名为"search-indexing-online"的队列,AMQP代理会自动将其绑定到默认交换机上,绑定(binding)的路由键名称也是为"search-indexing-online"。因此,当携带着名为"search-indexing-online"的路由键的消息被发送到默认交换机的时候,此消息会被默认交换机路由至名为"search-indexing-online"的队列中。换句话说,默认交换机看起来貌似能够直接将消息投递给队列,尽管技术上并没有做相关的操作。
2.Direct 直连交换机:
直连型交换机(direct exchange)是根据消息携带的路由键(routing key)将消息投递给对应队列的。直连交换机用来处理消息的单播路由(unicast routing)(尽管它也可以处理多播路由)。
3.fanout扇形交换机:
扇型交换机(funout exchange)将消息路由给绑定到它身上的所有队列,而不理会绑定的路由键。如果N个队列绑定到某个扇型交换机上,当有消息发送给此扇型交换机时,交换机会将消息的拷贝分别发送给这所有的N个队列。
4.topic 主题交换机:
主题交换机(topic exchanges)通过对消息的路由键和队列到交换机的绑定模式之间的匹配,将消息路由给一个或多个队列。主题交换机经常用来实现各种分发/订阅模式及其变种。主题交换机通常用来实现消息的多播路由(multicast routing)。
5.head交换机:
有时消息的路由操作会涉及到多个属性,此时使用消息头就比用路由键更容易表达,头交换机(headers exchange)就是为此而生的。头交换机使用多个消息属性来代替路由键建立路由规则。通过判断消息头的值能否与指定的绑定相匹配来确立路由规则。
二、队列
AMQP中的队列(queue)跟其他消息队列或任务队列中的队列是很相似的:它们存储着即将被应用消费掉的消息。队列跟交换机共享某些属性,但是队列也有一些另外的属性。
-
Name
-
Durable(消息代理重启后,队列依旧存在)
-
Exclusive(只被一个连接(connection)使用,而且当连接关闭后队列即被删除)
-
Auto-delete(当最后一个消费者退订后即被删除)
-
Arguments(一些消息代理用他来完成类似与TTL的某些额外功能)
队列在声明(declare)后才能被使用。如果一个队列尚不存在,声明一个队列会创建它。如果声明的队列已经存在,并且属性完全相同,那么此次声明不会对原有队列产生任何影响。如果声明中的属性与已存在队列的属性有差异,那么一个错误代码为406的通道级异常就会被抛出。
1.队列名称
队列的名字可以由应用(application)来取,也可以让消息代理(broker)直接生成一个。队列的名字可以是最多255字节的一个utf-8字符串。若希望AMQP消息代理生成队列名,需要给队列的name参数赋值一个空字符串:在同一个通道(channel)的后续的方法(method)中,我们可以使用空字符串来表示之前生成的队列名称。之所以之后的方法可以获取正确的队列名是因为通道可以默默地记住消息代理最后一次生成的队列名称。
以"amq."开始的队列名称被预留做消息代理内部使用。如果试图在队列声明时打破这一规则的话,一个通道级的403 (ACCESS_REFUSED)错误会被抛出。
2.队列持久化
持久化队列(Durable queues)会被存储在磁盘上,当消息代理(broker)重启的时候,它依旧存在。没有被持久化的队列称作暂存队列(Transient queues)。并不是所有的场景和案例都需要将队列持久化。
持久化的队列并不会使得路由到它的消息也具有持久性。倘若消息代理挂掉了,重新启动,那么在重启的过程中持久化队列会被重新声明,无论怎样,只有经过持久化的消息才能被重新恢复。
3.绑定
绑定(Binding)是交换机(exchange)将消息(message)路由给队列(queue)所需遵循的规则。如果要指示交换机“E”将消息路由给队列“Q”,那么“Q”就需要与“E”进行绑定。绑定操作需要定义一个可选的路由键(routing key)属性给某些类型的交换机。路由键的意义在于从发送给交换机的众多消息中选择出某些消息,将其路由给绑定的队列。
打个比方:
-
队列(queue)是我们想要去的位于纽约的目的地
-
交换机(exchange)是JFK机场
-
绑定(binding)就是JFK机场到目的地的路线。能够到达目的地的路线可以是一条或者多条
拥有了交换机这个中间层,很多由发布者直接到队列难以实现的路由方案能够得以实现,并且避免了应用开发者的许多重复劳动。
如果AMQP的消息无法路由到队列(例如,发送到的交换机没有绑定队列),消息会被就地销毁或者返还给发布者。如何处理取决于发布者设置的消息属性。
4.消费者
消息如果只是存储在队列里是没有任何用处的。被应用消费掉,消息的价值才能够体现。在AMQP 模型中,有两种途径可以达到此目的:
-
将消息投递给应用 ("push API")
-
应用根据需要主动获取消息 ("pull API")
使用push API,应用(application)需要明确表示出它在某个特定队列里所感兴趣的,想要消费的消息。如是,我们可以说应用注册了一个消费者,或者说订阅了一个队列。一个队列可以注册多个消费者,也可以注册一个独享的消费者(当独享消费者存在时,其他消费者即被排除在外)。
每个消费者(订阅者)都有一个叫做消费者标签的标识符。它可以被用来退订消息。消费者标签实际上是一个字符串。
5.消息确认
消费者应用(Consumer applications) - 用来接受和处理消息的应用 - 在处理消息的时候偶尔会失败或者有时会直接崩溃掉。而且网络原因也有可能引起各种问题。这就给我们出了个难题,AMQP代理在什么时候删除消息才是正确的?AMQP 0-9-1 规范给我们两种建议:
-
当消息代理(broker)将消息发送给应用后立即删除。(使用AMQP方法:basic.deliver或basic.get-ok)
-
待应用(application)发送一个确认回执(acknowledgement)后再删除消息。(使用AMQP方法:basic.ack)
前者被称作自动确认模式(automatic acknowledgement model),后者被称作显式确认模式(explicit acknowledgement model)。在显式模式下,由消费者应用来选择什么时候发送确认回执(acknowledgement)。应用可以在收到消息后立即发送,或将未处理的消息存储后发送,或等到消息被处理完毕后再发送确认回执(例如,成功获取一个网页内容并将其存储之后)。
如果一个消费者在尚未发送确认回执的情况下挂掉了,那AMQP代理会将消息重新投递给另一个消费者。如果当时没有可用的消费者了,消息代理会死等下一个注册到此队列的消费者,然后再次尝试投递。
6.拒绝消息
当一个消费者接收到某条消息后,处理过程有可能成功,有可能失败。应用可以向消息代理表明,本条消息由于“拒绝消息(Rejecting Messages)”的原因处理失败了(或者未能在此时完成)。当拒绝某条消息时,应用可以告诉消息代理如何处理这条消息——销毁它或者重新放入队列。当此队列只有一个消费者时,请确认不要由于拒绝消息并且选择了重新放入队列的行为而引起消息在同一个消费者身上无限循环的情况发生。
Negative Acknowledgements
在AMQP中,basic.reject方法用来执行拒绝消息的操作。但basic.reject有个限制:你不能使用它决绝多个带有确认回执(acknowledgements)的消息。但是如果你使用的是RabbitMQ,那么你可以使用被称作negative acknowledgements(也叫nacks)的AMQP 0-9-1扩展来解决这个问题。更多的信息请参考帮助页面
7.预取消息
在多个消费者共享一个队列的案例中,明确指定在收到下一个确认回执前每个消费者一次可以接受多少条消息是非常有用的。这可以在试图批量发布消息的时候起到简单的负载均衡和提高消息吞吐量的作用。For example, if a producing application sends messages every minute because of the nature of the work it is doing.(???例如,如果生产应用每分钟才发送一条消息,这说明处理工作尚在运行。)
注意,RabbitMQ只支持通道级的预取计数,而不是连接级的或者基于大小的预取。
8.消息属性和有效载荷(消息主体)
AMQP模型中的消息(Message)对象是带有属性(Attributes)的。有些属性及其常见,以至于AMQP明确的定义了它们,并且应用开发者们无需费心思思考这些属性名字所代表的具体含义。例如:
-
Content type(内容类型)
-
Content encoding(内容编码)
-
Routing key(路由键)
-
Delivery mode (persistent or not)
投递模式(持久化 或 非持久化) -
Message priority(消息优先权)
-
Message publishing timestamp(消息发布的时间戳)
-
Expiration period(消息有效期)
-
Publisher application id(发布应用的ID)
有些属性是被AMQP代理所使用的,但是大多数是开放给接收它们的应用解释器用的。有些属性是可选的也被称作消息头(headers)。他们跟HTTP协议的X-Headers很相似。消息属性需要在消息被发布的时候定义。
AMQP的消息除属性外,也含有一个有效载荷 - Payload(消息实际携带的数据),它被AMQP代理当作不透明的字节数组来对待。消息代理不会检查或者修改有效载荷。消息可以只包含属性而不携带有效载荷。它通常会使用类似JSON这种序列化的格式数据,为了节省,协议缓冲器和MessagePack将结构化数据序列化,以便以消息的有效载荷的形式发布。AMQP及其同行者们通常使用"content-type" 和 "content-encoding" 这两个字段来与消息沟通进行有效载荷的辨识工作,但这仅仅是基于约定而已。
消息能够以持久化的方式发布,AMQP代理会将此消息存储在磁盘上。如果服务器重启,系统会确认收到的持久化消息未丢失。简单地将消息发送给一个持久化的交换机或者路由给一个持久化的队列,并不会使得此消息具有持久化性质:它完全取决与消息本身的持久模式(persistence mode)。将消息以持久化方式发布时,会对性能造成一定的影响(就像数据库操作一样,健壮性的存在必定造成一些性能牺牲)。
9.消息确认
由于网络的不确定性和应用失败的可能性,处理确认回执(acknowledgement)就变的十分重要。有时我们确认消费者收到消息就可以了,有时确认回执意味着消息已被验证并且处理完毕,例如对某些数据已经验证完毕并且进行了数据存储或者索引操作。
这种情形很常见,所以 AMQP 内置了一个功能叫做 消息确认(message acknowledgements),消费者用它来确认消息已经被接收或者处理。如果一个应用崩溃掉(此时连接会断掉,所以AMQP代理亦会得知),而且消息的确认回执功能已经被开启,但是消息代理尚未获得确认回执,那么消息会被从新放入队列(并且在还有还有其他消费者存在于此队列的前提下,立即投递给另外一个消费者)。
协议内置的消息确认功能将帮助开发者建立强大的软件。
三、准备工作(windows10环境下的RabbitMQ安装步骤)
第一步:下载并安装erlang
-
原因:RabbitMQ服务端代码是使用并发式语言Erlang编写的,安装Rabbit MQ的前提是安装Erlang。
-
安装完事儿后要记得配置一下系统的环境变量。
此电脑-->鼠标右键“属性”-->高级系统设置-->环境变量-->“新建”系统环境变量
变量名:ERLANG_HOME
变量值就是刚才erlang的安装地址,点击确定。
然后双击系统变量path
点击“新建”,将%ERLANG_HOME%\bin加入到path中。
最后windows键+R键,输入cmd,再输入erl,看到版本号就说明erlang安装成功了。
第二步:下载并安装RabbitMQ
下载地址:http://www.rabbitmq.com/download.html
双击下载后的.exe文件,安装过程与erlang的安装过程相同。
RabbitMQ安装好后接下来安装RabbitMQ-Plugins。打开命令行cd,输入RabbitMQ的sbin目录。
我的目录是:D:\Program Files\RabbitMQ Server\rabbitmq_server-3.7.3\sbin
然后在后面输入rabbitmq-plugins enable rabbitmq_management命令进行安装
打开sbin目录,双击rabbitmq-server.bat
等几秒钟看到这个界面后,访问http://localhost:15672
然后可以看到如下界面
默认用户名和密码都是guest,登陆即可。
四、程序实现
1.创建rabbitmqconfig配置文件类:
package com.zxh.config; import org.springframework.amqp.core.Binding; import org.springframework.amqp.core.BindingBuilder; import org.springframework.amqp.core.DirectExchange; import org.springframework.amqp.core.Queue; import org.springframework.amqp.rabbit.annotation.EnableRabbit; import org.springframework.amqp.rabbit.connection.CachingConnectionFactory; import org.springframework.amqp.rabbit.connection.ConnectionFactory; import org.springframework.amqp.rabbit.core.RabbitTemplate; import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter; import org.springframework.amqp.support.converter.MessageConverter; 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; @EnableRabbit @Configuration public class RabbitMqConfig { public static final String EXCHANGE = "spring.boot.direct"; public static final String ROUTINGKEY_FAIL = "spring.boot.routingKey.failure"; public static final String ROUTINGKEY = "spring.boot.routingKey"; public static final String QUEUE_NAME = "spring.demo"; public static final String QUEUE_NAME_FAIL = "spring.demo.failure"; //RabbitMQ的配置信息 @Value("${spring.rabbitmq.host}") private String host; @Value("${spring.rabbitmq.port}") private Integer port; @Value("${spring.rabbitmq.username}") private String username; @Value("${spring.rabbitmq.password}") private String password; @Value("${spring.rabbitmq.virtual-host}") private String virtualHost; //建立一个连接容器,类型数据库的连接池 @Bean public ConnectionFactory connectionFactory() { CachingConnectionFactory connectionFactory = new CachingConnectionFactory(host, port); connectionFactory.setUsername(username); connectionFactory.setPassword(password); connectionFactory.setVirtualHost(virtualHost); connectionFactory.setPublisherConfirms(true);// 确认机制 // connectionFactory.setPublisherReturns(true); //发布确认,template要求CachingConnectionFactory的publisherConfirms属性设置为true return connectionFactory; } // RabbitMQ的使用入口 @Bean @Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE) //必须是prototype类型 public RabbitTemplate rabbitTemplate() { RabbitTemplate template = new RabbitTemplate(this.connectionFactory()); template.setMessageConverter(this.jsonMessageConverter()); template.setMandatory(true); return template; } /** * 交换机 * 针对消费者配置 * FanoutExchange: 将消息分发到所有的绑定队列,无routingkey的概念 * HeadersExchange :通过添加属性key-value匹配 * DirectExchange:按照routingkey分发到指定队列 * DirectExchange:多关键字匹配 */ @Bean public DirectExchange exchange() { return new DirectExchange(EXCHANGE); } /** * 队列 * * @return */ @Bean public Queue queue() { return new Queue(QUEUE_NAME, true); //队列持久 } @Bean public Queue queueFail() { return new Queue(QUEUE_NAME_FAIL, true); //队列持久 } /** * 绑定 * * @return */ @Bean public Binding binding(Queue queue, DirectExchange exchange) { return BindingBuilder.bind(queue()).to(exchange()).with(RabbitMqConfig.ROUTINGKEY); } @Bean public Binding bindingFail(Queue queue, DirectExchange exchange) { return BindingBuilder.bind(queueFail()).to(exchange()).with(RabbitMqConfig.ROUTINGKEY_FAIL); } @Bean public MessageConverter jsonMessageConverter() { return new Jackson2JsonMessageConverter(); } // @Bean // public CharacterEncodingFilter characterEncodingFilter() { // CharacterEncodingFilter filter = new CharacterEncodingFilter(); // filter.setEncoding("UTF-8"); // filter.setForceEncoding(true); // return filter; // } }
2.生产者推送消息
package com.zxh.service; import com.zxh.config.RabbitMqConfig; import com.zxh.pojo.User; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.amqp.core.Message; import org.springframework.amqp.core.MessageBuilder; import org.springframework.amqp.rabbit.core.RabbitTemplate; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.util.Assert; import java.util.List; import java.util.UUID; @Service public class UserService { private Logger logger = LoggerFactory.getLogger(this.getClass()); @Autowired private RabbitTemplate template; /** * 增加用户 * */ public boolean addPerson(User user) throws Exception { Assert.notNull(user, "添加对象信息不能为空"); Assert.hasText(user.getUserId(), "添加对象信息用户编号不能为空"); Assert.notNull(user.getAge(), "添加对象信息年龄不能为空"); template.convertAndSend(RabbitMqConfig.EXCHANGE, RabbitMqConfig.ROUTINGKEY, user.toString()); // template.setConfirmCallback(new RabbitTemplate.ConfirmCallback() { // @Override // public void confirm(CorrelationData correlationData, boolean ack, String cause) { // if (!ack) { // logger.info("send message failed: " + cause); //+ correlationData.toString()); // throw new RuntimeException("send error " + cause); // } else { // logger.info("send to broke ok" + correlationData.getId()); // } // } // }); return true; } private Message buildMessage(User user) throws Exception { Message message = MessageBuilder.withBody(user.toString().getBytes()) .setMessageId(UUID.randomUUID().toString()).setContentType("application/json").build(); return message; } }
3.消费者订阅消息
package com.zxh.rabbitmq; import org.springframework.amqp.rabbit.annotation.RabbitListener; import org.springframework.stereotype.Component; @Component public class UserTopicRecive { @RabbitListener(queues="spring.demo") public void process(String user) throws InterruptedException { System.out.println("TopicRecive1接受的消息: "+user); } }
五、程序演示
六、项目结构图
七、小结 - RabbitMQ的工作流程介绍
1、建立信息。Publisher定义需要发送消息的结构和内容。
2、建立Conection和Channel。由Publisher和Consumer创建连接,连接到Broker的物理节点上,同时建立Channel。Channel是建立在Connection之上的,一个Connection可以建立多个Channel。Publisher连接Virtual Host 建立Channel,Consumer连接到相应的Queue上建立Channel。
3、声明交换机和队列。声明一个消息交换机(Exchange)和队列(Queue),并设置相关属性。
4、发送消息。由Publisher发送消息到Broker中的Exchange中
5、路由转发。RabbitMQ收到消息后,根据消息指定的Exchange(交换机) 来查找Binding(绑定) 然后根据规则(Routing Key)分发到不同的Queue。这里就是说使用Routing Key在消息交换机(Exchange)和消息队列(Queue)中建立好绑定关系,然后将消息发送到绑定的队列中去。
6、消息接收。Consumer监听相应的Queue,一旦Queue中有可以消费的消息,Queue就将消息发送给Consumer端。
7、消息确认。当Consumer完成某一条消息的处理之后,需要发送一条ACK消息给对应的Queue。
Consumer收到消息时需要显式的向RabbitMQ Broker发送basic.ack消息或者Consumer订阅消息时设置auto_ack参数为true。
在通信过程中,队列对ACK的处理有以下几种情况:
如果Consumer接收了消息,发送ack,RabbitMQ会删除队列中这个消息,发送另一条消息给Consumer。
如果Consumer接收了消息, 但在发送ack之前断开Channel,RabbitMQ会认为这条消息没有被deliver(递送),如果有其他的Channel,会该消息将被发送给另外的Channel。如果没有,当在Consumer再次连接的时候,这条消息会被redeliver(重新递送)。
如果consumer接收了消息,但是忘记了ack,RabbitMQ不会重复发送消息。
新版RabbitMQ还支持Consumer reject某条(类)消息,可以通过设置requeue参数中的reject为true达到目地,那么Consumer将会把消息发送给下一个注册的Consumer。
8、关闭消息通道(channel)以及和服务器的连接。