消息队列是大型复杂系统解耦利器。本文根据应用广泛的消息队列RabbitMQ,介绍Spring Boot应用程序中队列中间件的开发和应用。

一、RabbitMQ基础

1、RabbitMQ简介

RabbitMQ是Spring所在公司Pivotal自己的产品,是基于AMQP高级队列协议的消息中间件,采用erlang开发,所以你的RabbitMQ队列服务器需要erlang环境。

可以直接参考官方的说法:RabbitMQ is the most widely deployed open source message broker.言简意赅,一目了然。

2、AMQP

高级消息队列协议(AMQP)是一个异步消息传递所使用的应用层协议规范。作为线路层协议(AMQP是一个抽象的协议,它不负责处理具体的数据),而不是API(例如Java消息系统JMS),AMQP客户端能够无视消息的来源任意发送和接受信息。
AMQP的原始用途只是为金融界提供一个可以彼此协作的消息协议,而现在的目标则是为通用消息队列架构提供通用构建工具。因此,面向消息的中间件(MOM)系统,例如发布/订阅队列,没有作为基本元素实现。反而通过发送简化的AMQ实体,用户被赋予了构建例如这些实体的能力。这些实体也是规范的一部分,形成了在线路层协议顶端的一个层级:AMQP模型。这个模型统一了消息模式,诸如之前提到的发布/订阅,队列,事务以及流数据,并且添加了额外的特性,例如更易于扩展,基于内容的路由。

扩展阅读:既然有高级的消息协议,必然有简单的协议,STOMP(Simple (or Streaming) Text Orientated Messaging Protocol),也就是简单消息文本协议,猛击这里

3、MSMQ

这里附带介绍一下MSMQ。.NET开发者接触最多的可能还是这个消息队列,我知道有两个以.NET作为主要开发语言的公司都选择MSMQ来开发公共框架如ESB、日志组件等。

如果你有.NET下MSMQ(微软消息队列)开发和使用经验,一定不会对队列常用术语陌生。对比一下,对后面RabbitMQ的学习和理解非常有帮助。

逻辑结构如下:

4、基本术语

安装好RabbitMQ后,可以启用插件,打开RabbitMQ自带的后台,一图胜千言,你会看到很多似曾相识的技术术语和名词。

当然你也可以参考这里的图片示例一个一个验证下面的名词。

(1)Broker:消息队列服务器实体。

(2)Producer:生产者。

(3)Consumer:消费者。

(4)Queue(队列):消息队列载体,每个消息都会被投入到一个或多个队列。Queue是 RabbitMQ 的内部对象,用于存储消息;消费者Consumer就是通过订阅队列来获取消息的,RabbitMQ 中的消息都只能存储在 Queue 中,生产者Producer生产消息并最终投递到 Queue 中,消费者可以从 Queue 中获取消息并消费;多个消费者可以订阅同一个 Queue。

(5)Connection(连接):Producer 和 Consumer 通过TCP 连接到 RabbitMQ Server。

(6)Channel(信道):基于 Connection创建,数据流动都是在 Channel 中进行。

(7)Exchange(交换器):生产者将消息发送到 Exchange(交换器),由Exchange 将消息路由到一个或多个 Queue 中(或者丢弃);Exchange 并不存储消息;Exchange Types 常用的有 Fanout、Direct、Topic 和Header四种类型,每种类型对应不同的路由规则:
Direct:完全匹配,消息路由到那些
Routing Key 与 Binding Key 完全匹配的 Queue 中。比如 Routing Key
为mq_cleint-key,只会转发mq_cleint-key,不会转发mq_cleint-key.1,也不会转发mq_cleint-key.1.2。
Topic:模式匹配,Exchange 会把消息发送到一个或者多个满足通配符规则的 routing-key 的 Queue。其中*表示匹配一个
word,#匹配多个 word 和路径,路径之间通过.隔开。如满足a.*.c的 routing-key
有a.hello.c;满足#.hello的 routing-key 有a.b.c.hello。
Fanout:忽略匹配,把所有发送到该 Exchange 的消息路由到所有与它绑定 的Queue 中。

Header:也根据规则匹配,相较于Direct和Topic固定地使用RoutingKey ,Headers 则是一个自定义匹配规则的类型。在队列与交换器绑定时, 会设定一组键值对(Key-Value)规则, 消息中也包括一组键值对( Headers 属性), 当这些键值对有一对,,或全部匹配时, 消息被投送到对应队列。

(8)Binding(绑定):是 Exchange(交换器)将消息路由给 Queue 所需遵循的规则。

(9)Routing Key(路由键):消息发送给 Exchange(交换器)时,消息将拥有一个路由键(默认为空), Exchange(交换器)根据这个路由键将消息发送到匹配的队列中。

(10)Binding Key(绑定键):指定当前 Exchange(交换器)下,什么样的 Routing Key(路由键)会被下派到当前绑定的 Queue 中。

5、应用场景

我们使用一个技术或组件或中间件,必须要非常理解它的适用场景,否则很容易误用。

RabbitMQ的经典应用场景包括:异步处理、应用解耦、流量削峰、日志处理、消息通讯。

已经有很多人总结了这5种场景下的RabbitMQ实际应用。

推荐阅读:猛击这里

到这里,RabbitMQ基础知识介绍结束,下面开始动手实践。

添加依赖

      <dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>

RabbitMQ

配置RabbitMQ

## RabbitMQ相关配置
spring.application.name=springbootdemo
spring.rabbitmq.host=127.0.0.1
spring.rabbitmq.port=5672
spring.rabbitmq.username=springbootmq
spring.rabbitmq.password=123456

application.mq.properties

新增RabbitMQConfig类

package com.power.demo.messaging;

import org.springframework.amqp.core.*;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; /**
* RabbitMQ消息队列配置类
* <p>
* 注意:如果已在配置文件中声明了Queue对象,就不用在RabbitMQ的管理员页面创建队列(Queue)了
*/
@Configuration
public class RabbitMQConfig { /**
* 声明接收字符串的队列 Hello 默认
*
* @return
*/
@Bean
public Queue stringQueue() { //boolean isDurable = true;//是否持久化
//boolean isExclusive = false; //仅创建者可以使用的私有队列,断开后自动删除
//boolean isAutoDelete = false; //当所有消费客户端连接断开后,是否自动删除队列
//Queue queue = new Queue(MQField.HELLO_STRING_QUEUE, isDurable, isExclusive, isAutoDelete);
//return queue; //return new Queue(MQField.HELLO_STRING_QUEUE); //默认支持持久化 return QueueBuilder.durable(MQField.HELLO_STRING_QUEUE)
//.exclusive()
//.autoDelete()
.build();
} /**
* 声明接收Goods对象的队列 Hello 支持持久化
*
* @return
*/
@Bean
public Queue goodsQueue() { return QueueBuilder.durable(MQField.HELLO_GOODS_QUEUE).build();
} /**
* 声明WorkQueue队列 competing consumers pattern,多个消费者不会重复消费队列的相同消息
*
* @return
*/
@Bean
public Queue workQueue() {
return QueueBuilder.durable(MQField.MY_WORKER_QUEUE).build();
} /**
* 消息队列中最常见的模式:发布订阅模式
* <p>
* 声明发布订阅模式队列 Publish/Subscribe
* <p>
* exchange类型包括:direct, topic, headers 和 fanout
**/ /*fanout(广播)队列相关声明开始*/
@Bean
public Queue fanOutAQueue() {
return QueueBuilder.durable(MQField.MY_FANOUTA_QUEUE).build();
} @Bean
public Queue fanOutBQueue() {
return QueueBuilder.durable(MQField.MY_FANOUTB_QUEUE).build();
} @Bean
FanoutExchange fanoutExchange() {
return (FanoutExchange) ExchangeBuilder.fanoutExchange(MQField.MY_FANOUT_EXCHANGE).build(); //return new FanoutExchange(MQField.MY_FANOUT_EXCHANGE);
} @Bean
Binding bindingExchangeA(Queue fanOutAQueue, FanoutExchange fanoutExchange) {
return BindingBuilder.bind(fanOutAQueue).to(fanoutExchange);
} @Bean
Binding bindingExchangeB(Queue fanOutBQueue, FanoutExchange fanoutExchange) {
return BindingBuilder.bind(fanOutBQueue).to(fanoutExchange);
} /*fanout队列相关声明结束*/ /*topic队列相关声明开始*/ @Bean
public Queue topicAQueue() {
return QueueBuilder.durable(MQField.MY_TOPICA_QUEUE).build();
} @Bean
public Queue topicBQueue() {
return QueueBuilder.durable(MQField.MY_TOPICB_QUEUE).build();
} @Bean
TopicExchange topicExchange() {
return (TopicExchange) ExchangeBuilder.topicExchange(MQField.MY_TOPIC_EXCHANGE).build();
} //绑定时,注意队列名称与上述方法名一致
@Bean
Binding bindingTopicAExchangeMessage(Queue topicAQueue, TopicExchange topicExchange) {
return BindingBuilder.bind(topicAQueue).to(topicExchange).with(MQField.MY_TOPIC_ROUTINGKEYA);
} @Bean
Binding bindingTopicBExchangeMessages(Queue topicBQueue, TopicExchange topicExchange) { return BindingBuilder.bind(topicBQueue).to(topicExchange).with(MQField.MY_TOPIC_ROUTINGKEYB); } /*topic队列相关声明结束*/ /*direct队列相关声明开始*/ @Bean
public Queue directAQueue() {
return QueueBuilder.durable(MQField.MY_DIRECTA_QUEUE).build();
} @Bean
public Queue directBQueue() {
return QueueBuilder.durable(MQField.MY_DIRECTB_QUEUE).build();
} /**
* 声明Direct交换机 支持持久化.
*
* @return the exchange
*/
@Bean
DirectExchange directExchange() {
return (DirectExchange) ExchangeBuilder.directExchange(MQField.MY_DIRECT_EXCHANGE).durable(true).build();
} @Bean
Binding bindingDirectAExchangeMessage(Queue directAQueue, DirectExchange directExchange) {
return BindingBuilder.bind(directAQueue).to(directExchange).with(MQField.MY_DIRECT_ROUTINGKEYA);
} @Bean
Binding bindingDirectBExchangeMessage(Queue directBQueue, DirectExchange directExchange) {
return BindingBuilder.bind(directBQueue).to(directExchange)
//.with(MQField.MY_DIRECT_ROUTINGKEYB)
.with(MQField.MY_DIRECT_ROUTINGKEYB);
} /*direct队列相关声明结束*/
}

RabbitMQConfig

RabbitMQConfig我将常用到的模式都配置在里面了,注释已经写得很清楚,在详细介绍模式的地方会用到这里定义的队列、绑定和交换器。

持久化配置

在RabbitMQConfig类中尤其注意这几个参数,包括是否可持久化durable;仅创建者可以使用的私有队列,断开后自动删除exclusive;当所有消费客户端连接断开后,是否自动删除队列autoDelete。其中durable和autoDelete对队列和交换器都可以配置。

RabbitMQ支持的消息的持久化(durable),也就是将数据写在磁盘上,为了数据安全考虑,绝大多数场景下我们都会选择持久化,可能记录一些不是业务必须的日志稍微例外。
消息队列持久化包括3个部分:

(1)、队列持久化,在声明时指定Queue.durable为1

(2)、交换器持久化,在声明时指定Exchange.durable为1

(3)、消息持久化,在投递时指定消息的delivery_mode为2(而1表示非持久化) 参考:这里

如果Exchange和Queue都是持久化的,那么它们之间的Binding也是持久化的;如果Exchange和Queue两者之间有一个持久化,另一个非持久化,就不允许建立绑定。

二、常见模式

在Spring Boot下使用RabbitMQ非常容易,直接调用AmqpTemplate类封装好的接口即可。

1、hello world

P为生产者,C为消费者,中间红色框表示消息队列。生产者P将消息发送到消息队列Queue,消费者C对消息进行处理。

生产者:

package com.power.demo.messaging.hello;

import com.power.demo.entity.vo.GoodsVO;
import com.power.demo.messaging.MQField;
import org.springframework.amqp.core.AmqpTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils; /**
* Hello消息生产者
**/
@Component
public class HelloSender { @Autowired
private AmqpTemplate rabbitTemplate; public boolean send(String message) throws Exception {
boolean isOK = false; if (StringUtils.isEmpty(message)) {
System.out.println("消息为空");
return isOK;
} rabbitTemplate.convertAndSend(MQField.HELLO_STRING_QUEUE, message); isOK = true; System.out.println(String.format("HelloSender发送字符串消息结果:%s", isOK)); return isOK;
} public boolean send(GoodsVO goodsVO) throws Exception { boolean isOK = false; rabbitTemplate.convertAndSend(MQField.HELLO_GOODS_QUEUE, goodsVO); isOK = true; System.out.println(String.format("HelloSender发送对象消息结果:%s", isOK)); return isOK; } }

HelloSender

消费者:

package com.power.demo.messaging.hello;

import com.power.demo.entity.vo.GoodsVO;
import com.power.demo.messaging.MQField;
import com.power.demo.util.SerializeUtil;
import org.springframework.amqp.rabbit.annotation.RabbitHandler;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component; /**
* Hello消息消费者
**/
@Component
public class HelloReceiver { @RabbitListener(queues = MQField.HELLO_STRING_QUEUE)
@RabbitHandler
public void process(String message) { try {
Thread.sleep(5000);
} catch (Exception e) {
e.printStackTrace();
} System.out.println("HelloReceiver接收到的字符串消息是 => " + message);
} @RabbitListener(queues = MQField.HELLO_GOODS_QUEUE)
@RabbitHandler
public void process(GoodsVO goodsVO) {
System.out.println("------ 接收实体对象 ------");
System.out.println("HelloReceiver接收到的实体对象是 => " + SerializeUtil.Serialize(goodsVO));
}
}

HelloReceiver

这是最简单的一种模式,这个最简单示例,可以看到应用场景里的异步处理的影子。

在Controller中,新增一个接口:

    @RequestMapping(value = "/hello/sendmsg", method = RequestMethod.GET)
@ApiOperation("简单字符串消息测试")
@ApiImplicitParams({
@ApiImplicitParam(paramType = "query", name = "message", required = true, value = "字符串消息", dataType = "String")
})
public String sendMsg(String message) throws Exception { boolean isOK = helloSender.send(message); return String.valueOf(isOK);
}

sendmsg

按照传统方式调用RPC接口,通常都是同步等待接口返回,而使用队列后,消息生产者直接向RabbitMQ服务器发送一条消息,不需要同步等待这个消息的处理结果。

示例代码中,消息消费者会刻意等待5秒(Thread.sleep(5000);)后才处理(打印出)消息,但是实际调用这个接口的时候,非常快就返回成功结果了,因为这个发送消息的动作不需要等待消费者消费消息的结果。

发送的消息,除了简单消息对象如字符串等,示例里你还看到有一个发送商品对象的消息,也就是说明RabbitMQ支持自定义的复杂对象消息。

2、work queues

P为生产者,C1、C2为消费者,中间红色框表示消息队列。生产者P将消息发送到消息队列Queue,消费者C1和C2对消息进行处理。

这种模式比较容易产生误解的地方是,多个消费者会不会消费队列里的同一条消息。答案是不会。

官方的说明是因为消费者根据竞争消费模式(competing consumers pattern)分派任务(Distributing tasks among workers (the competing consumers pattern) )。

对于work queues这种模式,同一条消息M1,要么C1拉取到,要么C2拉取到,不会出现C1和C2同时拉取到并消费。

当然,这种模式还可以扩展,除了一个生产者,也可以有多个生产者。

生产者:

package com.power.demo.messaging.workqueues;

import com.power.demo.messaging.MQField;
import org.springframework.amqp.core.AmqpTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils; @Component
public class WorkProducerA { @Autowired
private AmqpTemplate rabbitTemplate; public boolean send(String message) throws Exception {
boolean isOK = false; if (StringUtils.isEmpty(message)) {
System.out.println("消息为空");
return isOK;
} rabbitTemplate.convertAndSend(MQField.MY_WORKER_QUEUE, message); isOK = true; System.out.println(String.format("WorkProducerA发送字符串消息结果:%s", isOK)); return isOK;
}
}

WorkProducerA

相同队列下另一个生产者:

package com.power.demo.messaging.workqueues;

import com.power.demo.messaging.MQField;
import org.springframework.amqp.core.AmqpTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils; @Component
public class WorkProducerB { @Autowired
private AmqpTemplate rabbitTemplate; public boolean send(String message) throws Exception {
boolean isOK = false; if (StringUtils.isEmpty(message)) {
System.out.println("消息为空");
return isOK;
} rabbitTemplate.convertAndSend(MQField.MY_WORKER_QUEUE, message); isOK = true; System.out.println(String.format("WorkProducerB发送字符串消息结果:%s", isOK)); return isOK;
}
}

WorkProducerB

消费者:

package com.power.demo.messaging.workqueues;

import com.power.demo.messaging.MQField;
import org.springframework.amqp.rabbit.annotation.RabbitHandler;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component; import java.util.concurrent.atomic.AtomicInteger; @Component
public class WorkConsumerA { private static AtomicInteger atomicInteger = new AtomicInteger(); @RabbitListener(queues = MQField.MY_WORKER_QUEUE)
@RabbitHandler
public void process(String message) throws Exception { int index = atomicInteger.getAndIncrement(); Thread.sleep(2000); System.out.println("WorkConsumerA接收到的字符串消息是 => " + message); System.out.println("WorkConsumerA自增序号 => " + index);
} }

WorkConsumerA

另一个消费者:

package com.power.demo.messaging.workqueues;

import com.power.demo.messaging.MQField;
import org.springframework.amqp.rabbit.annotation.RabbitHandler;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component; import java.util.concurrent.atomic.AtomicInteger; @Component
public class WorkConsumerB { private static AtomicInteger atomicInteger = new AtomicInteger(); @RabbitListener(queues = MQField.MY_WORKER_QUEUE)
@RabbitHandler
public void process(String message) throws Exception { int index = atomicInteger.getAndIncrement(); Thread.sleep(10); System.out.println("WorkConsumerB接收到的字符串消息是 => " + message); System.out.println("WorkConsumerB自增序号 => " + index);
} }

pub/sub

应用最广泛的发布/订阅模式。

官方的说法是:发送多个消息到多个消费者(Sending messages to many consumers at once.)

这个模式和work queues模式最明显的区别是,队列Queue前加了一层,多了Exchange(交换器)。

P为生产者,X为交换器,C1、C2为消费者,中间红色框表示消息队列。生产者P将消息不是直接发送到队列Queue,而是发送到交换器X(注意:交换器Exchange并不存储消息),然后由交换机X发送给两个队列,两个消费者C1和C2各自监听一个队列,来消费消息。

根据交换器类型的不同,又可以分为Fanout、Direct和Topic这三种消费方式,Headers方式实际应用不是非常广泛,本文暂不讨论。

3、fanout

任何发送到Fanout Exchange的消息都会被转发到与该Exchange绑定(Binding)的所有Queue上。

(1)可以理解为路由表的模式

(2)这种模式不需要RoutingKey,即使配置了也忽略

(3)这种模式需要提前将Exchange与Queue进行绑定,一个Exchange可以绑定多个Queue,一个Queue可以同多个Exchange进行绑定

(4)如果接受到消息的Exchange没有与任何Queue绑定,则消息会被抛弃

Fanout广播模式实现同一个消息被多个消费者消费,而work queues是同一个消息只能有一个消费者(竞争去)消费。

生产者:

package com.power.demo.messaging.pubsub.fanout;

import com.power.demo.entity.vo.GoodsVO;
import com.power.demo.messaging.MQField;
import org.springframework.amqp.core.AmqpTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils; @Component
public class FanoutSender { @Autowired
private AmqpTemplate rabbitTemplate; public boolean send(GoodsVO goodsVO) throws Exception { boolean isOK = false; if (goodsVO == null) {
System.out.println("消息为空");
return isOK;
} rabbitTemplate.convertAndSend(MQField.MY_FANOUT_EXCHANGE, "", goodsVO); isOK = true; System.out.println(String.format("FanoutSender发送对象消息结果:%s", isOK)); return isOK; } }

FanoutSender

消费者:

package com.power.demo.messaging.pubsub.fanout;

import com.power.demo.entity.vo.GoodsVO;
import com.power.demo.messaging.MQField;
import com.power.demo.util.SerializeUtil;
import org.springframework.amqp.rabbit.annotation.RabbitHandler;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component; @Component
public class FanoutReceiverA { @RabbitListener(queues = MQField.MY_FANOUTA_QUEUE)
@RabbitHandler
public void process(GoodsVO goodsVO) {
System.out.println("FanoutReceiverA接收到的商品消息是 => " + SerializeUtil.Serialize(goodsVO));
}
}

FanoutReceiverA

另一个消费者:

package com.power.demo.messaging.pubsub.fanout;

import com.power.demo.entity.vo.GoodsVO;
import com.power.demo.messaging.MQField;
import com.power.demo.util.SerializeUtil;
import org.springframework.amqp.rabbit.annotation.RabbitHandler;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component; @Component
public class FanoutReceiverB { @RabbitListener(queues = MQField.MY_FANOUTB_QUEUE)
@RabbitHandler
public void process(GoodsVO goodsVO) {
System.out.println("FanoutReceiverB接收到的商品消息是 => " + SerializeUtil.Serialize(goodsVO));
}
}

FanoutReceiverB

4、direct

Fanout是1对多以广播的方式,发送给所有的消费者。

Direct则是创建消息队列的时候,指定一个BindingKey。当发送者发送消息的时候,指定对应的RoutingKey,当RoutingKey和消息队列的BindingKey一致的时候,消息将会被发送到该消息队列中。
Direct广播模式最明显不同于Fanout模式的地方是,消费者可以进行消息过滤,有选择的进行接收想要消费的消息,也就是队列绑定关键字,发送者将数据根据关键字发送到Exchange,Exchange根据关键字判定应该将数据发送(路由)到指定队列。

任何发送到Direct Exchange的消息都会被转发到RoutingKey中指定的Queue。

(1)消息传递时需要一个“RoutingKey”,可以简单的理解为要发送到的队列名字

(2)如果vhost中不存在RouteKey中指定的队列名,则该消息会被抛弃

生产者:

package com.power.demo.messaging.pubsub.direct;

import com.power.demo.messaging.MQField;
import org.springframework.amqp.core.AmqpTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils; @Component
public class DirectSender { @Autowired
private AmqpTemplate rabbitTemplate; public boolean sendDirectA(String message) throws Exception {
boolean isOK = false; if (StringUtils.isEmpty(message)) {
System.out.println("消息为空");
return isOK;
} rabbitTemplate.convertAndSend(MQField.MY_DIRECT_EXCHANGE, MQField.MY_DIRECT_ROUTINGKEYA, message); isOK = true; System.out.println(String.format("DirectSender发送DirectA字符串消息结果:%s", isOK)); return isOK;
} public boolean sendDirectB(String message) throws Exception {
boolean isOK = false; if (StringUtils.isEmpty(message)) {
System.out.println("消息为空");
return isOK;
} rabbitTemplate.convertAndSend(MQField.MY_DIRECT_EXCHANGE, MQField.MY_DIRECT_ROUTINGKEYB, message); isOK = true; System.out.println(String.format("DirectSender发送DirectB字符串消息结果:%s", isOK)); return isOK;
} }

DirectSender

消费者:

package com.power.demo.messaging.pubsub.direct;

import com.power.demo.messaging.MQField;
import org.springframework.amqp.rabbit.annotation.RabbitHandler;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component; @Component
public class DirectReceiverA { @RabbitListener(queues = MQField.MY_DIRECTA_QUEUE)
@RabbitHandler
public void process(String message) {
System.out.println("DirectReceiverA接收到的字符串消息是 => " + message);
} }

DirectReceiverA

另一个消费者:

package com.power.demo.messaging.pubsub.direct;

import com.power.demo.messaging.MQField;
import org.springframework.amqp.rabbit.annotation.RabbitHandler;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component; @Component
public class DirectReceiverB { @RabbitListener(queues = MQField.MY_DIRECTB_QUEUE)
@RabbitHandler
public void process(String message) {
System.out.println("DirectReceiverB接收到的字符串消息是 => " + message);
} }

DirectReceiverB

5、topic

Topic转发信息主要是依据通配符,队列和交换机的绑定主要是依据一种模式(通配符+字符串),而当发送消息的时候,只有指定的RoutingKey和该模式相匹配的时候,消息才会被发送到该消息队列中。

任何发送到Topic Exchange的消息都会被转发到所有关心RoutingKey中指定话题的Queue上

(1)每个队列都有其关心的主题,所有的消息都带有一个“标题”(RoutingKey),Exchange会将消息转发到所有关注主题能与RouteKey模糊匹配的队列

(2)需要RoutingKey,也需要提前绑定Exchange与Queue

(3)在进行绑定时,要提供一个该队列关心的主题,如“#.log.#”表示该队列关心所有涉及log的消息(一个RoutingKey为”mq.log.error”的消息会被转发到该队列)

(4)“#”表示0个或若干个关键字,“*”表示一个关键字。如“log.*”能与“log.warn”匹配,无法与“log.warn.timeout”匹配;但“log.#”能与上述两者都匹配

(5)如果Exchange没有发现能够与RouteKey匹配的Queue,则会抛弃此消息

生产者:

package com.power.demo.messaging.pubsub.topic;

import com.power.demo.messaging.MQField;
import org.springframework.amqp.core.AmqpTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils; @Component
public class TopicSender { @Autowired
private AmqpTemplate rabbitTemplate; public boolean sendTopicA(String message) throws Exception {
boolean isOK = false; if (StringUtils.isEmpty(message)) {
System.out.println("消息为空");
return isOK;
} rabbitTemplate.convertAndSend(MQField.MY_TOPIC_EXCHANGE, MQField.MY_TOPIC_ROUTINGKEYA, message); isOK = true; System.out.println(String.format("TopicSender发送TopicA字符串消息结果:%s", isOK)); return isOK;
} public boolean sendTopicB(String message) throws Exception {
boolean isOK = false; if (StringUtils.isEmpty(message)) {
System.out.println("消息为空");
return isOK;
} rabbitTemplate.convertAndSend(MQField.MY_TOPIC_EXCHANGE, MQField.MY_TOPIC_ROUTINGKEYB, message); isOK = true; System.out.println(String.format("TopicSender发送TopicB字符串消息结果:%s", isOK)); return isOK;
} public boolean sendToMatchedTopic() { boolean isOK = false; String routingKey = "my_topic_routingkeyA.16";//模糊匹配MQField.MY_TOPIC_ROUTINGKEYA //String routingKey = "my_topic_routingkeyB.32";//模糊匹配MQField.MY_TOPIC_ROUTINGKEYB String matchedKeys = "";
if (MQField.MY_TOPIC_ROUTINGKEYA.contains(routingKey.split("\\.")[0])) {
matchedKeys = "TopicReceiverA";
} else if (MQField.MY_TOPIC_ROUTINGKEYB.contains(routingKey.split("\\.")[0])) {
matchedKeys = "TopicReceiverB";
} String msg = "message to matched receivers:" + matchedKeys; rabbitTemplate.convertAndSend(MQField.MY_TOPIC_EXCHANGE, routingKey, msg); isOK = true; System.out.println(String.format("TopicSender发送字符串消息结果:%s", isOK)); return isOK;
} }

TopicSender

消费者:

package com.power.demo.messaging.pubsub.topic;

import com.power.demo.messaging.MQField;
import org.springframework.amqp.rabbit.annotation.RabbitHandler;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component; @Component
public class TopicReceiverA { @RabbitListener(queues = MQField.MY_TOPICA_QUEUE)
@RabbitHandler
public void process(String message) {
System.out.println("TopicReceiverA接收到的字符串消息是 => " + message);
} }

TopicReceiverA

另一个消费者:

package com.power.demo.messaging.pubsub.topic;

import com.power.demo.messaging.MQField;
import org.springframework.amqp.rabbit.annotation.RabbitHandler;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component; @Component
public class TopicReceiverB { @RabbitListener(queues = MQField.MY_TOPICB_QUEUE)
@RabbitHandler
public void process(String message) {
System.out.println("TopicReceiverB接收到的字符串消息是 => " + message);
} }

TopicReceiverB

示例代码中,定义了两个topic,生产者通过调用sendToMatchedTopic方法,根据RoutingKey模糊匹配,将消息发送到匹配的队列上。

到这里,发布订阅模式的介绍就结束了。我们再来总结下发布订阅模式下RabbitMQ消息队列主要工作流程。以Topic为例:

生产者
1、获取一个连接(Connection)
2、从连接(Connection)上获取一个信道( Channel)
3、声明一个交换器( Exchange)
4、声明1个或多个队列(Queue)
5、把队列(Queue)绑定到交换器(Exchange)上
6、向指定的交换器(Exchange)发送消息,消息路由到特定队列(Queue)

消费者

RabbitMQ消费者消费消息,支持推(push)模式和拉(pull)模式,这里以拉模式说明下流程。

1、创建一个连接(Connection)
2、启动MainLoop后台线程,通过连接(Connection)循环拉取消息
3、处理并确认消息被消费

6、rpc

RPC调用流程说明:
(1)当客户端启动的时候,它创建一个匿名独享的回调队列

(2)在 RPC 请求中,客户端发送带有两个属性的消息:一个是设置回调队列的 reply_to 属性,另一个是设置唯一值的 correlation_id 属性

(3)将请求发送到一个 rpc_queue 队列中

(4)服务器等待请求发送到这个队列中来。当请求出现的时候,它执行他的工作并且将带有执行结果的消息发送给 reply_to 字段指定的队列。

(5)客户端等待回调队列里的数据。当有消息出现的时候,它会检查 correlation_id 属性。如果此属性的值与请求匹配,将它返回给应用

Callback queue回调队列,客户端向服务器发送请求,服务器端处理请求后,将其处理结果保存在一个存储体中。而客户端为了获得处理结果,那么客户在向服务器发送请求时,同时发送一个回调队列地址reply_to。
Correlation
id关联标识,客户端可能会发送多个请求给服务器,当服务器处理完后,客户端无法辨别在回调队列中的响应具体和那个请求时对应的。为了处理这种情况,客户端在发送每个请求时,同时会附带一个独有correlation_id属性,这样客户端在回调队列中根据correlation_id字段的值就可以分辨此响应属于哪个请求。

服务端:

package com.power.demo.messaging.rpc;

import com.power.demo.messaging.MQField;
import com.rabbitmq.client.ConnectionFactory;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Consumer;
import com.rabbitmq.client.DefaultConsumer;
import com.rabbitmq.client.AMQP;
import com.rabbitmq.client.Envelope; import java.io.IOException;
import java.util.concurrent.TimeoutException; public class RPCServer { private static int fib(int n) {
if (n == 0) {
return 0;
}
if (n == 1) {
return 1;
}
return fib(n - 1) + fib(n - 2);
} //直接运行此方法
public static void main(String[] argv) {
ConnectionFactory factory = new ConnectionFactory();
//factory.setHost("localhost"); Connection connection = null;
try {
connection = factory.newConnection();
final Channel channel = connection.createChannel(); channel.queueDeclare(MQField.MY_RPC_QUEUE, false, false, false, null); channel.basicQos(1); System.out.println(" [x] Awaiting RPC requests"); Consumer consumer = new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
AMQP.BasicProperties replyProps = new AMQP.BasicProperties
.Builder()
.correlationId(properties.getCorrelationId())
.build(); String response = ""; try {
String message = new String(body, "UTF-8");
int n = Integer.parseInt(message); System.out.println(" [.] fib(" + message + ")");
response += fib(n); System.out.println(String.format("RPCServer计算fib数列应答:%s", response)); } catch (RuntimeException e) {
System.out.println(" [.] " + e.toString());
} finally {
channel.basicPublish("", properties.getReplyTo(), replyProps, response.getBytes("UTF-8"));
channel.basicAck(envelope.getDeliveryTag(), false);
// RabbitMq consumer worker thread notifies the RPC server owner thread
synchronized (this) {
this.notify();
}
}
}
}; channel.basicConsume(MQField.MY_RPC_QUEUE, false, consumer);
// Wait and be prepared to consume the message from RPC client.
while (true) {
synchronized (consumer) {
try {
consumer.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
} catch (IOException | TimeoutException e) {
e.printStackTrace();
} finally {
if (connection != null)
try {
connection.close();
} catch (IOException _ignore) {
}
}
}
}

RPCServer

客户端:

package com.power.demo.messaging.rpc;

import com.power.demo.messaging.MQField;
import com.rabbitmq.client.ConnectionFactory;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.DefaultConsumer;
import com.rabbitmq.client.AMQP;
import com.rabbitmq.client.Envelope; import java.io.IOException;
import java.util.UUID;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.TimeoutException; public class RPCClient { private Connection connection;
private Channel channel;
private String replyQueueName; public RPCClient() throws IOException, TimeoutException {
ConnectionFactory factory = new ConnectionFactory();
//factory.setHost("localhost"); connection = factory.newConnection();
channel = connection.createChannel(); replyQueueName = channel.queueDeclare().getQueue();
} public String call(String message) throws IOException, InterruptedException {
final String corrId = UUID.randomUUID().toString(); AMQP.BasicProperties props = new AMQP.BasicProperties
.Builder()
.correlationId(corrId)
.replyTo(replyQueueName)
.build(); channel.basicPublish("", MQField.MY_RPC_QUEUE, props, message.getBytes("UTF-8")); final BlockingQueue<String> response = new ArrayBlockingQueue<String>(1); channel.basicConsume(replyQueueName, true, new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
if (properties.getCorrelationId().equals(corrId)) {
response.offer(new String(body, "UTF-8"));
}
}
}); return response.take();
} public void close() throws IOException {
connection.close();
} //直接运行此方法
public static void main(String[] argv) {
RPCClient fibonacciRpc = null;
String response = null;
try {
fibonacciRpc = new RPCClient(); System.out.println(" [x] Requesting fib(10)");
response = fibonacciRpc.call("10");
System.out.println(" [.] Got '" + response + "'");
System.out.println(String.format("RPCClient得到计算fib数列应答:%s", response));
} catch (IOException | TimeoutException | InterruptedException e) {
e.printStackTrace();
} finally {
if (fibonacciRpc != null) {
try {
fibonacciRpc.close();
} catch (IOException _ignore) {
}
}
}
}
}

RPCClient

示例代码我这里直接改造了一下官方的demo代码。启动RPCServer,再运行RPCClient就可以看到RPC调用结果了。

三、常见问题

1、幂等性

生产环境各种业务系统出现重复消息是不可避免的,因为不能保证生产者不发送重复消息。

对于读操作而言,重复消息可能无害,但是对于写操作,重复消息容易造成业务灾难,比如相同消息多次扣减库存,多次支付请求扣款等。

有一种情况也会造成重复消息,就是RabbitMQ对设置autoAck=false之后没有被Ack的消息是不会清除掉的,消费者可以多次重复消费。

我个人认为RabbitMQ只是消息传递的载体,要保证幂等性,还是需要在消费者业务逻辑上下功夫。

2、有序消息

我碰到过某厂有一个开发团队通过Kafka来实现有序队列,因为发送的消息有先后依赖关系,需要消费者收到多个消息保存起来最后聚合后一起处理业务逻辑。

但是,其实大部分业务场景下我们都不需要消息有先后依赖关系,因为有序队列产生依赖关系,后续消费很容易造成各种处理难题。

归根结底,我认为需要有序消息的业务系统在设计上就是不合理的,争取在设计上规避才好。当然良好的设计需要丰富的经验和优化,以及妥协。

3、高可用

RabbitMQ支持集群,模式主要可分为三种:单一模式、普通模式和镜像模式。

RabbitMQ支持弹性部署,在业务高峰期间可通过集群弹性部署支撑业务系统。

RabbitMQ支持消息持久化,如果队列服务器出现问题,消息做了持久化,后续恢复正常,消息数据不丢失不会影响正常业务流程。

RabbitMQ还有很多高级特性,比如发布确认和事务等,虽然可能会降低性能,但是增强了可靠性。

参考:

http://www.rabbitmq.com/

https://msdn.microsoft.com/en-us/library/ms711472(v=vs.85).aspx

http://www.cnblogs.com/dubing/p/4017613.html

https://blog.csdn.net/super_rd/article/details/70238869

https://blog.csdn.net/joeyon1985/article/details/39429117

http://www.cnblogs.com/saltlight-wangchao/p/6214334.html

http://www.cnblogs.com/binyue/p/4763766.html

https://my.oschina.net/u/2948566/blog/1624963

https://www.cnblogs.com/rjzheng/p/8994962.html

Spring Boot消息队列应用实践的更多相关文章

  1. SpringBoot(八) Spring和消息队列RabbitMQ

    概述 1.大多数应用中,可以通过消息服务中间件来提升系统异步能力和拓展解耦能力. 2.消息服务中的两个重要概念:消息代理(Message broker)和目的地(destination) 当消息发送者 ...

  2. Spring Boot WebFlux 快速入门实践

    02:WebFlux 快速入门实践 Spring Boot 2.0 spring.io 官网有句醒目的话是: BUILD ANYTHING WITH SPRING BOOT Spring Boot ( ...

  3. spring boot + mybatis + druid配置实践

    最近开始搭建spring boot工程,将自身实践分享出来,本文将讲述spring boot + mybatis + druid的配置方案. pom.xml需要引入mybatis 启动依赖: < ...

  4. Spring Boot开发MongoDB应用实践

    本文继续上一篇定时任务中提到的邮件服务,简单讲解Spring Boot中如何使用MongoDB进行应用开发. 上文中提到的这个简易邮件系统大致设计思路如下: 1.发送邮件支持同步和异步发送两种 2.邮 ...

  5. spring boot 使用及最佳实践

    第一部分,spring boot 文档 Spring boot的使用 使用maven进行构建 用户可以通过继承spring-boot-starter-parent来获取默认的依赖. l  默认java ...

  6. Spring Boot 16 条最佳实践

    Spring Boot是最流行的用于开发微服务的Java框架.在本文中,我将与你分享自2016年以来我在专业开发中使用Spring Boot所采用的最佳实践.这些内容是基于我的个人经验和一些熟知的Sp ...

  7. spring整合消息队列rabbitmq

    ps:本文只是简单一个整合介绍,属于抛砖引玉,具体实现还需大家深入研究哈.. 1.首先是生产者配置 ? 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 ...

  8. Spring Boot Admin简介及实践

    问题 在若干年前的单体应用时代,我们可以相对轻松地对整个业务项目进行健康检查.指标监控.配置管理等等项目治理.如今随着微服务的发展,我们将大型单体应用按业务模型进行划分,以此形成众多小而自治的微服务, ...

  9. Spring Boot 配置文件 – 在坑中实践

    摘要: 原创出处 www.bysocket.com 「泥瓦匠BYSocket 」欢迎转载,保留摘要,谢谢!   『 仓廪实而知礼节,衣食足而知荣辱 - 管仲 』   本文提纲 一.自动配置 二.自定义 ...

随机推荐

  1. 【第二十一篇】手C# MVC 微信授权登录 OAuth2.0授权登录

    首先一定要熟读,最起码过一遍微信开发者文档 微信开发者文档 文档写的很清楚 授权登录四步走 在正文开始前,我得讲清楚一个事情 敲黑板,划重点:微信一共有两个 access_token 一个是7200就 ...

  2. Java中对List去重, Stream去重

    问题 当下互联网技术成熟,越来越多的趋向去中心化.分布式.流计算,使得很多以前在数据库侧做的事情放到了Java端.今天有人问道,如果数据库字段没有索引,那么应该如何根据该字段去重?大家都一致认为用Ja ...

  3. python——模块与包2

    模块与包2 1 什么是包 包是一种通过使用.'模块名'来组织python模块名称空间的方式. 无论是import形式还是from...import形式,凡是在导入语句中(而不是在使用时)遇到带点的,都 ...

  4. 二、配置QtDesigner、PyUIC及PyRcc

    配置QtDesigner.PyUIC及PyRcc 安装完PyQt 5 及PyQt5-tools 后,则需要在Pycharm中配置QtDesigner.PyUIC及PyRcc. 配置QtDesigner ...

  5. 超级好用的前端开发测试Chrome插件-基于REST的Web服务客户端

    基于REST的Web服务客户端是一款功能强大的谷歌浏览器插件,使用基于REST的Web服务客户端(模拟REST客户端)可以让用户使用谷歌浏览器模拟REST请求来测试REST风格. 基于REST的Web ...

  6. iOS 检测屏幕是否锁定 🔓 / 🔒

    1. 导入头文件 #import <notify.h> 2. 给 CFNotificationCenter 添加观察者 - (void)addLockStatusObserver { CF ...

  7. 妙用 scale 与 transfrom-origin,精准控制动画方向

    上次发完 不可思议的纯 CSS 导航栏下划线跟随效果 这篇文章之后,很多朋友找我讨论,感叹 CSS 的奇妙. 然后昨天,群里一位朋友问到了一个和这个效果比较类似的效果,问如何 将下面这个动画的下划线效 ...

  8. [LeetCode] String Compression 字符串压缩

    Given an array of characters, compress it in-place. The length after compression must always be smal ...

  9. NOI2006 郁闷的出纳员

    题目描述 OIER公司是一家大型专业化软件公司,有着数以万计的员工.作为一名出纳员,我的任务之一便是统计每位员工的工资.这本来是一份不错的工作,但是令人郁闷的是,我们的老板反复无常,经常调整员工的工资 ...

  10. hdu 5437Alisha’s Party(优先队列)

    题意:邀请k个朋友,每个朋友带有礼物价值不一,m次开门,每次开门让一定人数p(如果门外人数少于p,全都进去)进来,当所有人到时会再开一次,每次都是礼物价值高的人先进. /*小伙伴最开始gg了,结果发现 ...