关于RabbitMQ的原理,找的他人的博客,已经很完善了,直接就拿过来了。
原文地址:http://blog.csdn.net/whycold/article/details/41119807
一、简介
MQ全称为Message Queue, 消息队列(MQ)是一种应用程序对应用程序的通信方法。应用程序通过读写出入队列的消息(针对应用程序的数据)来通信,而无需专用连接来链接它们。消息传递指的是程序之间通过在消息中发送数据进行通信,而不是通过直接调用彼此来通信,直接调用通常是用于诸如远程过程调用的技术。排队指的是应用程序通过 队列来通信。队列的使用除去了接收和发送应用程序同时执行的要求。其中较为成熟的MQ产品有IBM WEBSPHERE MQ等等...
二、使用场景
在项目中,将一些无需即时返回且耗时的操作提取出来,进行了异步处理,而这种异步处理的方式大大的节省了服务器的请求响应时间,从而提高了系统的吞吐量。
三、相关名称介绍
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的内部对象,用于存储消息,用下图表示。
RabbitMQ中的消息都只能存储在Queue中,生产者(下图中的P)生产消息并最终投递到Queue中,消费者(下图中的C)可以从Queue中获取消息并消费。
多个消费者可以订阅同一个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服务器就断电了),如果我们需要对这种小概率事件也要管理起来,那么我们要用到事务。由于这里仅为RabbitMQ的简单介绍,所以这里将不讲解RabbitMQ相关的事务。
5、Prefetch count
前面我们讲到如果有多个消费者同时订阅同一个Queue中的消息,Queue中的消息会被平摊给多个消费者。这时如果每个消息的处理时间不同,就有可能会导致某些消费者一直在忙,而另外一些消费者很快就处理完手头工作并一直空闲的情况。我们可以通过设置prefetchCount来限制Queue每次发送给每个消费者的消息数,比如我们设置prefetchCount=1,则Queue每次给每个消费者发送一条消息;消费者处理完这条消息后Queue会再给该消费者发送一条消息。
6、Exchange
在上一节我们看到生产者将消息投递到Queue中,实际上这在RabbitMQ中这种事情永远都不会发生。实际的情况是,生产者将消息发送到Exchange(交换器,下图中的X),由Exchange将消息路由到一个或多个Queue中(或者丢弃)。
Exchange是按照什么逻辑将消息路由到Queue的?这个将在下面的8、Binding中介绍。
RabbitMQ中的Exchange有四种类型,不同的类型有着不同的路由策略,这将在下面的10、Exchange Types中介绍。
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了。
9、Binding key
在绑定(Binding)Exchange与Queue的同时,一般会指定一个binding key;消费者将消息发送给Exchange时,一般会指定一个routing key;当binding key与routing key相匹配时,消息将会被路由到对应的Queue中。这个将在Exchange Types章节会列举实际的例子加以说明。
在绑定多个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这四种(AMQP规范里还提到两种Exchange Type,分别为system与自定义,这里不予以描述),下面分别进行介绍。
fanout(又叫广播式)
fanout类型的Exchange路由规则非常简单,它会把所有发送到该Exchange的消息路由到所有与它绑定的Queue中。
上图中,生产者(P)发送到Exchange(X)的所有消息都会路由到图中的两个Queue,并最终被两个消费者(C1与C2)消费。
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中。
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。
headers
headers类型的Exchange不依赖于routing key与binding key的匹配规则来路由消息,而是根据发送的消息内容中的headers属性进行匹配。
在绑定Queue与Exchange时指定一组键值对;当消息发送到Exchange时,RabbitMQ会取到该消息的headers(也是一个键值对的形式),对比其中的键值对是否完全匹配Queue与Exchange绑定时指定的键值对;如果完全匹配则消息会路由到该Queue,否则不会路由到该Queue。
该类型的Exchange没有用到过(不过也应该很有用武之地),所以不做介绍。
11、RPC
MQ本身是基于异步的消息处理,前面的示例中所有的生产者(P)将消息发送到RabbitMQ后不会知道消费者(C)处理成功或者失败(甚至连有没有消费者来处理这条消息都不知道)。
但实际的应用场景中,我们很可能需要一些同步处理,需要同步等待服务端将我的消息处理完成后再进行下一步处理。这相当于RPC(Remote Procedure Call,远程过程调用)。在RabbitMQ中也支持RPC。
RabbitMQ中实现RPC的机制是:
客户端发送请求(消息)时,在消息的属性(MessageProperties,在AMQP协议中定义了14中properties,这些属性会随着消息一起发送)中设置两个值replyTo(一个Queue名称,用于告诉服务器处理完成后将通知我的消息发送到这个Queue中)和correlationId(此次请求的标识号,服务器处理完成后需要将此属性返还,客户端将根据这个id了解哪条请求被成功执行了或执行失败);
服务器端收到消息并处理;
服务器端处理完消息后,将生成一条应答消息到replyTo指定的Queue,同时带上correlationId属性;
客户端之前已订阅replyTo指定的Queue,从中收到服务器的应答消息后,根据其中的correlationId属性分析哪条请求被执行了,根据执行结果进行后续业务处理。
四、总结
本文介绍了RabbitMQ中个人认为最重要的概念,充分利用RabbitMQ提供的这些功能就可以处理我们绝大部分的异步业务了。
五、实战
下面开始在springboot中使用RabbitMQ
在我的项目中使用的Exchage type使用的是topic,简单描述使用场景,生产者订单系统,订单处理成功发送成功消息,消费者接受到订单成功消息,做业务处理,例如发送推送等。因为订单系统和消费者是两个不同的服务,所以在两个服务都需要配置RabbitMQ。
首先在pom中引入mq
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
<version>1.5.2.RELEASE</version>
</dependency>
在生产者也就是订单系统中配置config
@Configuration
public class RabbitConfig {
//默认交换机
public static final String EXCHANGE_NAME_DEFAULT = "spring-boot-exchange-default";
@Value("${rabbitmq.hostname}")
private String hostname;
@Value("${rabbitmq.port}")
private int port;
@Value("${rabbitmq.username}")
private String username;
@Value("${rabbitmq.password}")
private String password;
@Bean
@Primary
public ConnectionFactory connectionFactory() {
CachingConnectionFactory connectionFactory = new CachingConnectionFactory(hostname, port);
if (StringUtils.isNotBlank(username)) {
connectionFactory.setUsername(username);
}
if (StringUtils.isNotBlank(password)) {
connectionFactory.setPassword(password);
}
connectionFactory.setVirtualHost(MessageConstant.VIRTUAL_HOST);
return connectionFactory;
}
@Bean
public AmqpTemplate amqpTemplate() {
return new RabbitTemplate(asyncTaskConnectionFactory());
}
@Bean
public AmqpAdmin amqpAdmin() {
RabbitAdmin amqpAdmin = new RabbitAdmin(asyncTaskConnectionFactory());
amqpAdmin.declareExchange(asyncTaskExchange());
return amqpAdmin;
}
@Bean
public TopicExchange exchange() {
TopicExchange topicExchange = new TopicExchange(Constant.EXCHANGE_NAME_DEFAULT);
topicExchange.setShouldDeclare(false);
return topicExchange;
}
}
mq配好后,就可以发送消息
public class OrderServiceImpl{
@Autowired
@Qualifier("amqpTemplate")
private AmqpTemplate amqpTemplate;
public static final String EXCHANGE_NAME_DEFAULT = "spring-boot-exchange-default";
public static final String ORDER_ROUTING_KEY = "order_routing_key";
public void send() {
OrderSuccessedMessage msg = new OrderSuccessedMessage();
String message = mapper.writeValueAsString(msg);
amqpTemplate.convertAndSend(EXCHANGE_NAME_DEFAULT, ORDER_ROUTING_KEY, message);
}
}
以上就是一个简单的订单系统中订单成功信息的发送
amqpTemplate.convertAndSend(EXCHANGE_NAME_DEFAULT, ORDER_ROUTING_KEY, message);第一个参数就是交换机名字,第二是routingkey,第三个则是具体的消息
下面是消费者消费,config配置同上
@Configuration
public class RabbitConfig {
//默认交换机
public static final String EXCHANGE_NAME_DEFAULT = "spring-boot-exchange-default";
@Value("${rabbitmq.hostname}")
private String hostname;
@Value("${rabbitmq.port}")
private int port;
@Value("${rabbitmq.username}")
private String username;
@Value("${rabbitmq.password}")
private String password;
@Bean
@Primary
public ConnectionFactory connectionFactory() {
CachingConnectionFactory connectionFactory = new CachingConnectionFactory(hostname, port);
if (StringUtils.isNotBlank(username)) {
connectionFactory.setUsername(username);
}
if (StringUtils.isNotBlank(password)) {
connectionFactory.setPassword(password);
}
connectionFactory.setVirtualHost(MessageConstant.VIRTUAL_HOST);
return connectionFactory;
}
@Bean
public AmqpTemplate amqpTemplate() {
return new RabbitTemplate(asyncTaskConnectionFactory());
}
@Bean
public AmqpAdmin amqpAdmin() {
RabbitAdmin amqpAdmin = new RabbitAdmin(asyncTaskConnectionFactory());
amqpAdmin.declareExchange(asyncTaskExchange());
return amqpAdmin;
}
@Bean
public TopicExchange exchange() {
TopicExchange topicExchange = new TopicExchange(Constant.EXCHANGE_NAME_DEFAULT);
topicExchange.setShouldDeclare(false);
return topicExchange;
}
}
因为该服务监听不止订单成功一个消息,还有其他消息要处理,所以写的一个通用的job来接收消息,根据不同的routingkey处理不同的Handler
包结构
不同业务写不同的job和listener即可
抽象的接收消息job
/**
* @author wp
* @since 2016/7/5
*/
@Configuration
public abstract class MessageEventJob {
@Autowired
@Qualifier("eventBusConnectionFactory")
protected ConnectionFactory connectionFactory;
@Autowired
@Qualifier("eventBusAmqpAdmin")
protected AmqpAdmin amqpAdmin;
@Autowired
@Qualifier("eventBusExchange")
protected TopicExchange topicExchange;
public SimpleMessageListenerContainer listenMessageFromContainer(String messageType, BaseEventMessageHandler messageHandler) {
Queue queue = new Queue(Constant.QUEUE_NAME_PREFIX + messageType + "_QUARTZ");
amqpAdmin.declareQueue(queue);
Binding binding = BindingBuilder.bind(queue).to(topicExchange).with(messageType);
amqpAdmin.declareBinding(binding);
SimpleMessageListenerContainer container = new SimpleMessageListenerContainer();
container.setConnectionFactory(connectionFactory);
container.setQueues(queue);
container.setDefaultRequeueRejected(false);
container.setAutoStartup(true);
container.setMessageListener(new MessageListenerAdapter(messageHandler));
return container;
}
}
如上代码的意思,声明一个queue,队列名为统一前缀+messageType+_QUARTE格式, binding绑定交换机和routingkey。
container.setMessageListener(new MessageListenerAdapter(messageHandler)); 中messageHandler是消息具体处理类,这里使用MessageListenerAdapter,
MessageListenerAdapter
1.可以把一个没有实现MessageListener
和ChannelAwareMessageListener
接口的类适配成一个可以处理消息的处理器
2.默认的方法名称为:handleMessage
,可以通过setDefaultListenerMethod
设置新的消息处理方法
3.MessageListenerAdapter
支持不同的队列交给不同的方法去执行。使用setQueueOrTagToMethodName
方法设置,当根据queue名称没有找到匹配的方法的时候,就会交给默认的方法去处理。
以上抽象的job完成,下面就该是具体不同的job,不同的job继承MessageEventJob,如下订单成功job
@Configuration
public class OrderJob extends MessageEventJob {
public static final String ORDER_ROUTING_KEY = "order_routing_key";
@Bean
public SimpleMessageListenerContainer orderPrizeContainer() {
return listenMessageFromContainer(ORDER_ROUTING_KEY, orderMessageHandler());
}
@Bean
public OrderMessageHandler orderMessageHandler() {
return new OrderMessageHandler();
}
}
在实际项目中为了更好的管理RoutingKey最好将所有的Routingkey写在一个枚举中,放在一个公共的模块中,同样Exchage name应该写在一个通用的常量中,这样方便其他人使用,维护起来也比较方便,这里是写死在每个job中。
具体订单成功消息处理则是OrderMessageHandler
public class OrderMessageHandler{
@Override
protected void handleMessage(String message) {
OrderSuccessedMessage eventMessage = mapper.readValue(message, OrderSuccessedMessage.class);
logger.info("开始处理订单成功消息, message={}", message);
}
}
这个有个问题handleMessage方法名不可改变,MessageListenerAdapter
支持不同的队列交给不同的方法去执行。使用setQueueOrTagToMethodName
方法设置,当根据queue名称没有找到匹配的方法的时候,就会交给默认的方法去处理。而它默认的方法是handleMessage,因为我这写的是通用的job,没有设置具体方法名,所以handleMessage这个方法名不可改变。
好了,这里基本也就完成一个简单的demo