前言
我们可以使用SpringCloud框架中Feign组完成微服务之间的远程调用;
但是Feign组件底层基于HTTP协议,HTTP协议的特点是请求同步,而且既需要请求也需要响应,属于同步远程调用;
微服务架构在同步远程调用的场景下,如果服务提供者一直没有响应服务消费者,很容易造成服务雪崩;
如果我们通过MQ协议发送异步消息,就可以实现服务消费者和服务提供者之间的应用解耦,中间的队列还能起到流量削峰的作用,进而实现高效、可靠的异步远程调用;
那么MQ能完全替代Feign吗?
2种方式各有优劣,打电话可以立即得到响应,但是你却不能跟多个人同时通话。发送消息可以同时与多个人沟通,但是往往响应会有延迟;
同步远程调用的优势是服务消费者会一直等待服务提供者的响应消息;
一旦服务提供者的响应消息返回,服务消费者会在第一时间得知,在接收服务提供者响应消息方面,同步远程调用比异步远程调用更加及时;
所以在企业中我们要根据不同的业务需求,灵活选择同步远程调用和异步远程调用;
一、MQ概念
MQ (Message Queue),中文是消息队列,字面来看就是存放消息的队列。
它是分布式系统中重要的组件,主要解决应用解耦,流量削峰,异步消息等问题。
常见的角色有:Producer(生产者)、Consumer(消费者)、Broker(中介)。
1.常见的消息队列
| RabbitMQ | ActiveMQ | RocketMQ | Kafka |
公司/社区 | Rabbit | Apache | 阿里 | Apache |
开发语言 | Erlang | Java | Java | Scala&Java |
协议支持 | AMQP,XMPP,SMTP,STOMP | OpenWire,AMQP,STOMP,MQTT | 自定义协议 | 自定义协议 |
可用性 | 高 | 一般 | 高 | 高 |
单机吞吐量 | 一般 | 差 | 高 | 非常高 |
消息延迟 | 微秒级 | 毫秒级 | 毫秒级 | 毫秒以内 |
消息可靠性 | 高 | 一般 | 高 | 一般 |
2.常见消息队列的使用场景
- RocketMQ是阿里巴巴开发的MQ其特点是消息可靠性较高适用于电商项目;
- Kafka传输的是数据流,虽然消息可靠性较低,但是吞吐量非常高,适用于大数据项目;
- RabbitMQ的各方面性能适中适用于中小型项目;
二、RabbitMQ概念
RabbitMQ是基于AMQP(Advanced Message Queuing Protocol)协议的一款消息中间件管理系统;
官方教程:http://www.rabbitmq.com/getstarted.html
1.安装RabbitMQ
在Centos7虚拟机中使用Docker来安装RabbitMQ
#1.下载rabbitmq镜像
docker pull rabbitmq:3.8-management
#2.运行容器
docker run -d -p 15672:15672 -p 5672:5672 --name mq -v mq-plugins:/plugins --hostname mq rabbitmq:3.8-management
设置5672端口提供API服务,15672端口提供用户管理界面;
2.创建主机
为了让各个用户可以互不干扰的工作,RabbitMQ添加了虚拟主机(Virtual Hosts)的概念;
虚拟主机其实就是1个独立的访问路径,每1个用户使用不同路径,每1个路径中包含多个队列、交换机;
虚拟主机和虚拟主机之间互相隔离不会影响;
3.创建用户
4.赋予用户主机管理权限
zhanggen虚拟主机交给用户zhanggen管理;
5.两大基本消息传输模型
消息队列的消息分为2大类传输模型:点对点模型、发布 /订阅模型;
- 点对点: 生产者生产的同1条消息只能被1个消费者消费;(微信私聊)
- 发布/订阅:生产者生产的同1条消息可以被多个消费者消费;(微信群聊)
5.1.为什么有了点对点还需要发布/订阅消息传输模型?
因为生产者1次生产的1条消息只能被1个消费者消费;
为了避免消息重复冗余才需要发布/订阅消息传输模型,把生产者1次生产的1条消息,转发给N个人;
就像你家丢了一条小狗,挨家挨户地去问(点对点),不如在大喇叭里吆喝广播1下(发布/订阅)省时省力;
6.六大消息传输模型
RabbitMQ在2基本传输模型的基础上进行细化,提供了6种消息模型,但是第6种其实是RPC,并不是MQ,因此不予学习。那么也就剩下5种;
- 1、2(点对点模型)
- 3、4、5(发布/订阅模型)
三、Java调用RabbitMQ
生产者负责想消息对象的某个队列中发送消息,生产者向消息队列发送消息成功之后程序退出;
消费者启动之后会一直监听消息队列的某个队列是否有消息,程序不会退出;
1.依赖引入
<dependencies>
<!--AMQP依赖,包含RabbitMQ-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
<!--单元测试-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
</dependencies>
pom.xml
2.生产者
package com.itheima.test;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import org.junit.Test;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
public class PublisherTest {
@Test
public void testSendMessage() throws IOException, TimeoutException {
// 1.建立连接
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("192.168.56.18");
factory.setPort(5672);
factory.setVirtualHost("zhanggen");
factory.setUsername("zhanggen");
factory.setPassword("123.com");
Connection connection = factory.newConnection();
// 2.创建通道Channel
Channel channel = connection.createChannel();
// 3.创建队列
String queueName = "p2p";//队列名称
channel.queueDeclare(queueName, false, false, false, null);
// 4.发送消息
String message = "hello, rabbitmq!";
channel.basicPublish("", queueName, null, message.getBytes());
System.out.println("发送消息成功:【" + message + "】");
// 5.关闭通道和连接
channel.close();
connection.close();
}
}
PublisherTest.java
3.消费者
package com.itheima.test;
import com.rabbitmq.client.*;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
public class ConsumerTest {
public static void main(String[] args) throws IOException, TimeoutException {
// 1.建立连接
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("192.168.56.18");
factory.setPort(5672);
factory.setVirtualHost("zhanggen");
factory.setUsername("zhanggen");
factory.setPassword("123.com");
Connection connection = factory.newConnection();
// 2.创建通道Channel
Channel channel = connection.createChannel();
// 3.创建队列
String queueName = "p2p";
channel.queueDeclare(queueName, false, false, false, null);
// 4.订阅消息
channel.basicConsume(queueName, true, new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope,
AMQP.BasicProperties properties, byte[] body) throws IOException {
// 5.处理消息
String message = new String(body);
System.out.println("接收到消息:【" + message + "】");
}
});
System.out.println("消费者等待接收消息。。。。");
}
}
ConsumerTest.java
四、SringBoot调用RabbitMQ
AMQP:是用于在应用程序之间传递业务消息的开放标准。该协议与语言和平台无关,更符合微服务中独立性的要求。
Spring AMQP:基于AMQP协议定义的一套API,提供了模板来发送和接收消息,模板底层是基于RabbitMQ封装。
SpringAmqp的官方地址:https://spring.io/projects/spring-amqp
以下我们将借助SpringAmqp实现RabbitMQ的5中消息传输模型;
1.点对点消息传输模型-BasicQueue
简单消息模型,1个生产者和1个消费者进行点对点消息传输;
1.1.生产者
1.1.1.application.yml配置
在application.yml中添加MQ配置
#RabbitMQ相关配置
spring:
rabbitmq:
host: 192.168.56.18 # 主机名
port: 5672 # 端口
virtual-host: zhanggen # 虚拟主机
username: zhanggen # 用户名
password: 123.com # 密码
1.1.2.测试类
编写测试类SpringAmqpTest,并利用RabbitTemplate实现消息发送
package com.zhanggen.test;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
@SpringBootTest
@RunWith(SpringJUnit4ClassRunner.class)
public class SpringAmqpTest {
@Autowired
private RabbitTemplate rabbitTemplate;
//发送简单消息
@Test
public void testSimpleQueue() {
//参数一: 队列名称(次队列需要是提前创建好的) 参数二: 消息内容
rabbitTemplate.convertAndSend("p2p", "hello,spring amqp!");
}
}
1.2.消费者
1.2.1.application.yml配置
#RabbitMQ相关配置
spring:
rabbitmq:
host: 192.168.56.18 # 主机名
port: 5672 # 端口
virtual-host: zhanggen # 虚拟主机
username: zhanggen # 用户名
password: 123.com # 密码
1.2.2.测试类
package com.zhanggen.listener;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;
@Component
public class SpringRabbitListener {
//简单类型消息
@RabbitListener(queues = "p2p")//声明队列名称
public void listenSimpleQueueMessage(String msg) {
System.out.println("消费者接收到消息:【" + msg + "】");
}
}
2.点对点消息传输模型-WorkQueue
当消息处理比较耗时的时候,可能生产消息的速度会远远大于消息的消费速度。长此以往,消息就会堆积越来越多造成队列积压。
此时就可以使用work 模型,多个消费者共同处理消息处理,消息的消费速度就能大大提高了。
2.2.生产者
这次我们循环发送,模拟大量消息堆积现象。
在publisher服务中的SpringAmqpTest类中添加一个测试方法:
//批量发送消息
@Test
public void testWorkQueue() {
for (int i = 1; i <= 50; i++) {
rabbitTemplate.convertAndSend("p2p", "message_" + i);
}
}
2.3.消费者
要模拟多个消费者绑定到同1个队列,我们在consumer服务的SpringRabbitListener中添加2个新的方法:
//使用下面两个方法来接收p2p队列中的消息
@RabbitListener(queues = "p2p")
public void listenWorkQueue1(String msg) throws InterruptedException {
System.out.println("消费者1接收到消息:【" + msg + "】");
Thread.sleep(20);
}
@RabbitListener(queues = "p2p")
public void listenWorkQueue2(String msg) throws InterruptedException {
System.err.println("消费者2........接收到消息:【" + msg + "】");
Thread.sleep(200);
}
2.4.测试
先启动ConsumerApplication后(消费者),再执行publisher服务中刚刚编写的发送测试方法testWorkQueue(提供者)。
可以看到消费者1很快完成了自己的25条消息,费者2却在缓慢的处理自己的25条消息。
也就是说RabbitMQ按消息的总条数,平均分配给2个消费者,并没有考虑到消费者的处理能力。这样显然是有问题的。
2.5.消费者能者多劳
在spring中有一个简单的配置,可以解决以上问题。我们修改consumer服务的application.yml文件,添加配置:
spring:
rabbitmq:
listener:
simple:
prefetch: 1 # 消费者一次处理一条消息,处理完毕后再从MQ中获取
2.6.测试消费者能者多劳
2.7.小结
以上2中消息传输模型都属于点对点传输模型
Work模型的使用:
- 多个消费者绑定到同1个队列,同1条消息只会被1个消费者处理
- 通过设置prefetch来控制消费者预取的消息数量
3.发布/订阅消息传输模型-Fanout
以上介绍了2种点对点消息传输模型,以下介绍3种发布/订阅消息传输模型;
在下面的发布/订阅消息传输模型中,需要使用Exchanges转发消息到多个Queue,不再局限于使用单个Queue传输消息;
3.1.点对对消息传输模型的缺陷
如果要实现以下功能:
- UserService作消息生产者向消息中间发送1条用户信息消息;
- 邮件微服务和短信微服务都是用户消息的消费者,它们从消息中间获取用户消息之后,实现给用户发邮件+发短信功能;
3.2.引入发布/订阅消息传输模型
由于点对对消息传输模型的缺陷:生产者生产的1条消息,只能被1个消费者所消费,导致上述的2个功能只能实现其中的1个;
1条用户信息要么被邮件微服务抢到,发了邮件,要么被短信微服务抢到,发了短信;
如果想让生产者生产的1条消息,同时被多个消费者同时抢到,即完成发邮件的功能,也要完成发短信的功能;
就需修改当前点对点消息传输模型为发布/订阅消息传输模型;
发布/订阅消息传输模型(Fanout)在MQ中可以理解为广播模式。
3.2.发布/订阅消息传输模型术语工作流程
3.3.声明交换机和队列
我们习惯在消费者一方来创建交换机和队列;
package com.zhanggen.config;
import org.springframework.amqp.core.Binding;
import org.springframework.amqp.core.BindingBuilder;
import org.springframework.amqp.core.FanoutExchange;
import org.springframework.amqp.core.Queue;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
//关于Fanout消息传输模型配置
@Configuration
public class FanoutConfiguration {
//1.配置1个交换机
@Bean
public FanoutExchange fanoutExchange() {
return new FanoutExchange("MyFanoutExchange");
}
//2.配置2个队列myQueue1和myQueue2,注意方法名称就是就是bean的名称
@Bean
public Queue myQueue1() {
return new Queue("MyQueue1");
}
@Bean
public Queue myQueue2() {
return new Queue("MyQueue2");
}
//3.将2个队列(myQueue1和myQueue2)绑定到交换机(MyFanoutExchange)
@Bean
public Binding bindQueue1(FanoutExchange fanoutExchange, Queue myQueue1) {
return BindingBuilder.bind(myQueue1).to(fanoutExchange);
}
@Bean
public Binding bindQueue2(FanoutExchange fanoutExchange, Queue myQueue2) {
return BindingBuilder.bind(myQueue2).to(fanoutExchange);
}
}
3.4.消费者
//FanOut消息传输模型
@RabbitListener(queues = "MyQueue1")
public void listenFanOut1(String msg) throws InterruptedException {
System.err.println("FanOut消息传输模型消费者1........接收到消息:【" + msg + "】");
Thread.sleep(200);
}
@RabbitListener(queues = "MyQueue2")
public void listenFanOut2(String msg) throws InterruptedException {
System.err.println("FanOut消息传输模型消费者2........接收到消息:【" + msg + "】");
Thread.sleep(200);
}
3.5.生产者
//测试FanOut
@Test
public void testFanOut() {
//参数一: 交换机名称 参数二:暂时没用 参数三: 消息内容
rabbitTemplate.convertAndSend("MyFanoutExchange", "", "交换机");
}
3.6.测试
------------------------------------- ------------------------------------- -------------------------------------
4.发布/订阅消息传输模型-Direct
在Fanout消息传输模型中新增了交换机和多队列的概念,实现1条消息可以被所有订阅的队列消费的功能。
但是只要队列绑定到了1个交换机上,队列就能收到这个交换机广播的所有消息;
如果交换机把所有消息广播到了所有队列里,不仅产生广播风暴,也会有信息安全隐患,这时就要用到Direct模型;
RoutingKey:生产者向Exchange发送消息时,一般会指定一个RoutingKey;
BindingKey:当绑定Exchange和Queue时,一般会指定一个BindingKey;
BindingKey与RoutingKey相匹配时,消息将会被路由到对应的Queue中。
4.1.注解声明消费者
基于@Bean的方式声明队列和交换机比较麻烦,Spring还提供了基于注解方式来声明。
在consumer的SpringRabbitListener中添加两个消费者,同时基于注解来声明队列和交换机:
//测试Direct模型
@RabbitListener(
bindings = @QueueBinding(//绑定
exchange = @Exchange(value = "direct.exchange", type = ExchangeTypes.DIRECT),//设置交换机的名字和类型
value = @Queue("direct.queue1"),//设置对列名字
key = "base"//设置bindingkey
)
)
public void listenDirectQueue1(String message) {
System.out.println("消费者1接收到了Direct消息:" + message);
}
@RabbitListener(
bindings = @QueueBinding(//绑定
exchange = @Exchange(value = "direct.exchange", type = ExchangeTypes.DIRECT),//设置交换机的名字和类型
value = @Queue("direct.queue2"),//设置对列名字
key = {"base", "vip"}//设置bindingkey
)
)
public void listenDirectQueue2(String message) {
System.out.println("消费者2接收到了Direct消息:" + message);
}
4.2.生产者
// 测试direct
@Test
public void testSendDirect() throws Exception {
//参数一: 交换机名称 参数二:routingKey 参数三: 消息内容
rabbitTemplate.convertAndSend("direct.exchange", "vip", "hello,everyone!");
}
5.发布/订阅消息传输模型-Topic
Topic消息传输模型与Direct相比,新增了RoutingKey和BidingKey可使用通配符的方式进行匹配的功能;
RoutingKey:一般由有1个或多个单词组成,多个单词之间以”.”分割,例如:china.news
BindingKey:使用通配符匹配RoutingKey匹配规则如下:
#
:代指匹配RoutingKey的0个或多个单词*
:代指匹配RoutingKey的1个单词
5.1.消费者
//测试topic模型
@RabbitListener(bindings = @QueueBinding(
value = @Queue("topic.queue1"),
exchange = @Exchange(value = "topic.exchange", type = ExchangeTypes.TOPIC),
key = "china.#"
))
public void listenTopicQueue1(String msg) {
System.out.println("消费者1接收到Topic消息:【" + msg + "】");
}
@RabbitListener(bindings = @QueueBinding(
value = @Queue("topic.queue2"),
exchange = @Exchange(value = "topic.exchange", type = ExchangeTypes.TOPIC),
key = "#.weather"
))
public void listenTopicQueue2(String msg) {
System.out.println("消费者2接收到Topic消息:【" + msg + "】");
}
5.2.生产者
// 测试topic
@Test
public void testTopicExchange() throws Exception {
//参数一: 交换机名称 参数二:routingKey 参数三: 消息内容
rabbitTemplate.convertAndSend("topic.exchange", "china.news", "喜报!孙悟空大战孙行者,胜!!!");
}
6.消息转换器
默认情况下SpringAMQP采用的序列化方式是JDK序列化,众所周知,JDK序列化存在下列问题:
- 数据体积过大
- 可读性差
6.1.配置JSON转换器
如果我们希望消息的体积更小、可读性更高,因此可以使用JSON方式来做序列化和反序列化。
6.1.1.在父工程中引入依赖
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.9.10</version>
</dependency>
6.1.2.在publisher和consumer两个服务启动类中添加json转换器
SpringBoot的启动类也是1个配置类;
需要注意的是在publisher和consumer配置完了Json转换器之后,双方经应该传输JSON数据类型的消息,否则消息反序列化是就会报错;
@Bean
public MessageConverter jsonMessageConverter(){
return new Jackson2JsonMessageConverter();
}
6.1.3.测试