文章目录
初识MQ
1. 同步通讯
优点:时效性较强,可以立刻得到结果
存在的问题:
- 耦合度高:每次加入新的需求,都要修改原来的代码
- 性能下降:调用者需要等待服务提供者响应,如果调用链过长则响应时间等于每次调用的时间之和
- 资源浪费:调用链中的每个服务在等待响应过程中,不能释放请求占用的资源,高并发场景下会极度浪费系统资源
- 级联失败:如果服务提供者出现问题,所有调用方都会跟着出问题,迅速导致整个微服务群故障
2. 异步通讯
异步调用常见实现就是事件驱动模式。
优势:
- 服务解耦
- 性能提升,吞吐量提高
- 服务没有强依赖,不担心级联失败问题
- 流量削峰
缺陷:
- 依赖于Broker的可靠性、安全性、吞吐能力
- 架构复杂,业务没有明显的流程线,不好追踪管理
3. MQ
MQ(MessageQueue),中文是消息队列, 字面看起来是存放消息的队列,也就是事件驱动架构中的Broker
4. MQ常见框架
RabbitMQ快速入门
1. 安装RabbitMQ
安装并运行RabbitMQ
docker pull rabbitmq:3-management
docker run \
-e RABBITMQ_DEFAULT_USER=itcast \
-e RABBITMQ_DEFAULT_PASS=123321 \
--name mq \
--hostname mq1 \
-p 15672:15672 \ # RabbitMQ控制台端口
-p 5672:5672 \ # RabbitMQ通信端口
-d \
rabbitmq:3-management
2. RabbitMQ中的几个概念
- channel:操作MQ的工具
- exchange:路由消息到队列中
- queue:缓存消息
- virtual host:虚拟主机,是对queue、exchange等资源的逻辑分组
3. 官方HelloWorld案例
官方的HelloWorld是基于最基础的消息队列模型来实现的,只包括三个角色:
- publisher:消息发布者,将消息发送到队列queue
- queue:消息队列,负责接收并缓存消息
- consumer:订阅队列,处理队列中的消息
所需依赖
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<!--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>
Publisher代码
public class PublisherTest {
@Test
public void testSendMessage() throws IOException, TimeoutException {
// 1.建立连接
ConnectionFactory factory = new ConnectionFactory();
// 1.1.设置连接参数,分别是:主机名、端口号、vhost、用户名、密码
factory.setHost("xxx.xxx.xxx.xx");
factory.setPort(5672);
factory.setVirtualHost("/");
factory.setUsername("xxx");
factory.setPassword("xxx");
// 1.2.建立连接
Connection connection = factory.newConnection();
// 2.创建通道Channel
Channel channel = connection.createChannel();
// 3.创建队列
String queueName = "simple.queue";
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();
}
}
Consumer代码
public class ConsumerTest {
public static void main(String[] args) throws IOException, TimeoutException {
// 1.建立连接
ConnectionFactory factory = new ConnectionFactory();
// 1.1.设置连接参数,分别是:主机名、端口号、vhost、用户名、密码
factory.setHost("xxx.xxx.xxx.xx");
factory.setPort(5672);
factory.setVirtualHost("/");
factory.setUsername("xxx");
factory.setPassword("xxx");
// 1.2.建立连接
Connection connection = factory.newConnection();
// 2.创建通道Channel
Channel channel = connection.createChannel();
// 3.创建队列
String queueName = "simple.queue";
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("等待接收消息。。。。");
}
}
基本消息队列的消息发送流程:
- 建立connection
- 创建channel
- 利用channel声明队列
- 利用channel向队列发送消息
基本消息队列的消息接收流程:
- 建立connection
- 创建channel
- 利用channel声明队列
- 定义consumer的消费行为handleDelivery()
- 利用channel将消费者与队列绑定
Spring AMQP
1. 什么是Spring AMQP
AMQP:Advanced Message Queuing Protocol,是用于在应用程序或之间传递业务消息的开放标准。该协议与语言和平台无关,更符合微服务中独立性的要求
Spring AMQP:是基于AMQP协议定义的一套API规范,提供了模板来发送和接收消息。包含两部分,其中spring-amqp是基础抽象,spring-rabbit是底层默认实现
案例:利用SpringAMQP实现HelloWorld中的基础消息队列功能
-
在父工程中引入spring-amqp依赖
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-amqp</artifactId> </dependency>
-
在publisher服务中编写yml配置文件
spring: rabbitmq: host: xxx.xxx.xxx.xx # 主机名 port: 5672 # 端口号 virtual-host: / # 虚拟主机 username: xxx # 用户名 password: xxx # 密码
-
新建测试类,利用RabbitTemplate的convertAndSend方法发送消息到simple.queue这个队列
@RunWith(SpringRunner.class) @SpringBootTest public class SpringAmqpTest { @Autowired private RabbitTemplate rabbitTemplate; @Test public void testSendMessage2SimpleQueue() { String queueName = "simple.queue"; String message = "hello springamqp"; rabbitTemplate.convertAndSend(queueName, message); } }
-
在consumer服务中配置yml文件,设置spring.rabbitmq.listener.simple.prefetch的值,可以控制预取消息的上限
spring: rabbitmq: host: xxx.xxx.xxx.xx # 主机名 port: 5672 # 端口号 virtual-host: / # 虚拟主机 username: xxx # 用户名 password: xxx # 密码 listener: simple: prefetch: 1
-
在consumer服务中新建类,编写消费逻辑
@Component public class SpringRabbitListener { @RabbitListener(queues = "simple.queue") //queues的值为消息队列的名称,可以写多个 public void listenSimpleQueue(String msg){ System.out.println("msg = " + msg); } }
-
启动ConsumerApplication,运行查看结果
消息一旦消费就会从队列删除,RabbitMQ没有消息回溯功能
2. Work Queue工作队列模型
Work Queue工作队列,可以提高消息处理速度,避免队列消息堆积
案例:模拟WorkQueue,实现一个队列绑定多个消费者
-
在publisher服务中定义测试方法,每秒产生50条消息,发送到simple.queue
@Test public void testSendMessage2WorkQueue() throws InterruptedException { String queueName = "simple.queue"; String message = "hello message"; for (int i = 1; i <= 50; i++) { rabbitTemplate.convertAndSend(queueName, message + i); Thread.sleep(20); } }
-
在consumer服务中定义两个消息监听者,都监听simple.queue队列
@Component public class SpringRabbitListener { @RabbitListener(queues = "simple.queue") public void listenWorkQueue1(String msg) throws InterruptedException { System.out.println("消费者1接收到的消息========>" + msg + "---" + LocalTime.now()); Thread.sleep(20); } @RabbitListener(queues = "simple.queue") public void listenWorkQueue2(String msg) throws InterruptedException { System.err.println("消费者2接收到的消息--------》" + msg + "---" + LocalTime.now()); Thread.sleep(200); } }
-
消费者1每秒处理50条消息,消费者2每秒处理10条消息
work模型的使用:
- 多个消费者绑定到一个队列,同一条消息只会被一个消费者处理
- 通过设置prefetch来控制消费者预取的消息数量
3. 发布(Publish)、订阅(Subscribe)模型
发布订阅模式与之前案例的区别就是允许将同意消息发送给多个消费者。实现方式是加入了exchange(交换机)。
exchange负责消息路由,而不是存储,路由失败则消息丢失
4. 发布、订阅模型-Fanout(广播)
Fanout Exchange会将接收到的消息路由到每一个跟其绑定的queue
案例:利用SpringAMQP演示FanoutExchange的使用
-
在consumer服务中,利用代码声明队列、交换机,并将两者绑定
@Configuration public class FanoutConfig { /** * 创建fanout交换机 * @return 名为my.fanout的交换机 */ @Bean public FanoutExchange fanoutExchange(){ return new FanoutExchange("my.fanout"); //创建名为my.fanout的交换机 } /** * 创建队列 * @return 名为fanout.queue1的队列 */ @Bean public Queue fanoutQueue1(){ return new Queue("fanout.queue1"); } @Bean public Queue fanoutQueue2(){ return new Queue("fanout.queue2"); } /** * 绑定队列1到交换机 * @param fanoutQueue1 * @param fanoutExchange * @return */ @Bean public Binding fanoutBinding1(Queue fanoutQueue1, FanoutExchange fanoutExchange){ return BindingBuilder.bind(fanoutQueue1).to(fanoutExchange); } @Bean public Binding fanoutBinding2(Queue fanoutQueue2, FanoutExchange fanoutExchange){ return BindingBuilder.bind(fanoutQueue2).to(fanoutExchange); } }
-
在consumer服务中,编写两个消费者方法,分别监听fanout.queue1和fanout.queue2
@Component public class SpringRabbitListener { @RabbitListener(queues = "fanout.queue1") public void listenFanoutQueue1(String msg){ System.out.println("消费者接收到fanout.queue1的消息:【" + msg + "】"); } @RabbitListener(queues = "fanout.queue2") public void listenFanoutQueue2(String msg){ System.out.println("消费者接收到fanout.queue2的消息:【" + msg + "】"); } }
-
在publisher中编写测试方法,向my.fanout发送消息
@Test public void testSendFanoutExchange() { String exchangeName = "my.fanout"; String message = "hello fanout"; //发送消息,参数分别是交换机名称、routingKey(暂时为空)、消息 rabbitTemplate.convertAndSend(exchangeName, "", message); }
交换机的作用是什么?
- 接收publisher发送的消息
- 将消息按照规则路由到与之绑定的队列
- 不能缓存消息,路由失败,消息丢失
- FanoutExchange会将消息路由到每个绑定的队列
相关Bean:
- 交换机:FanoutExchange
- 队列:Queue
- 绑定:Binding
5. 发布、订阅模型-Direct(路由)
Direct Exchange会将接收到的消息根据规则路由到指定的queue,因此称为路由模式(routes)
- 每一个queue都会与Exchange设置一个BindingKey
- 发布者发送消息时,指定消息的RoutingKey
- Exchange将消息路由到BindingKey与消息RoutingKey一致的队列
案例:利用SpringAMQP演示DirectExchange的使用
-
利用@RabbitListener声明Exchange、Queue、RoutingKey
-
在consumer服务中,编写两个消费者方法,分别监听direct.queue1、direct.queue2
@Component public class SpringRabbitListener { @RabbitListener(bindings = @QueueBinding( value = @Queue(name = "direct.queue1"), exchange = @Exchange(name = "my.direct", type = ExchangeTypes.DIRECT), key = {"red", "blue"} )) public void listenDirectQueue1(String msg){ System.out.println("消费者接收到direct.queue1的消息:【" + msg + "】"); } @RabbitListener(bindings = @QueueBinding( value = @Queue(name = "direct.queue2"), exchange = @Exchange(name = "my.direct", type = ExchangeTypes.DIRECT), key = {"red", "yellow"} )) public void listenDirectQueue2(String msg){ System.out.println("消费者接收到direct.queue2的消息:【" + msg + "】"); } }
ExchangeTypes是一个枚举类型,为交换机的类型
-
在publisher中编写测试方法,向my.direct发送消息
@Test public void testSendDirectExchange() { String exchangeName = "my.direct"; String message = "hello red"; rabbitTemplate.convertAndSend(exchangeName, "red", message); //发送消息,参数分别是交换机名称、routingKey为哪条规则,与之对应的队列就可以收到消息、消息 }
Direct交换机与Fanout交换机的区别:
- Fanout交换机将消息路由给每一个与之绑定的队列
- Direct交换机根据RoutingKey判断路由给哪个队列
- 如果多个队列具有相同的RoutingKey,则与Fanout功能类似
基于@RabbitListener注解声明队列和交换机有哪些常见注解?
- @Queue
- @Exchange
6. 发布、订阅模型-Topic(话题)
TopicExchange与DirectExchange类似,区别在于routingKey必须是多个单词的列表,并且以.
分割。例如:
- shanghai.news:代表有上海新闻的消息
- shanghai.weather:代表有上海天气的消息
- beijing.news:代表有北京新闻的消息
- beijing.weather:代表有北京天气的消息
Queue与Exchange指定BindingKey时可以使用通配符:
#:代指0个或多个单词
*:代指一个单词
案例:利用SpringAMQP演示TopicExchange的使用
-
利用@RabbitListener声明Exchange、Queue、RoutingKey
-
在consumer服务中,编写两个消费者方法,分别监听topic.queue1、topic.queue2
@Component public class SpringRabbitListener { @RabbitListener(bindings = @QueueBinding( value = @Queue(name = "topic.queue1"), exchange = @Exchange(name = "my.topic", type = ExchangeTypes.TOPIC), key = "shanghai.#" )) public void listenTopicQueue1(String msg){ System.out.println("消费者接收到topic.queue1的消息:【" + msg + "】"); } @RabbitListener(bindings = @QueueBinding( value = @Queue(name = "topic.queue2"), exchange = @Exchange(name = "my.topic", type = ExchangeTypes.TOPIC), key = "#.news" )) public void listenTopicQueue2(String msg){ System.out.println("消费者接收到topic.queue2的消息:【" + msg + "】"); } }
-
在publisher中编写测试方法,向my.topic发送消息
@Test public void testSendTopicExchange() { String exchangeName = "my.topic"; String message = "hello shanghai news"; rabbitTemplate.convertAndSend(exchangeName, "shanghai.news", message); //发送消息,参数分别是交换机名称、routingKey(暂时为空)、消息 }
7. 消息转换器
在SpringAMQP的发送方法中,接收消息的类型是Object,我们可以发送任意对象类型的消息,SpringAMQP会帮我们序列化为字节后发送
Spring的对消息对象的处理是由org.springframework.amqp.support.convert.MessageConvert来处理的,其默认实现是SimpleMessageConvert,基于JDK的ObjectOutputStream完成序列化。
如果需要修改只需要定义一个MessageConvert类型的Bean即可,推荐用JSON方式序列化
-
在父工程引入依赖
<dependency> <groupId>com.fasterxml.jackson.dataformat</groupId> <artifactId>jackson-dataformat-xml</artifactId> </dependency>
-
在服务配置类中定义MessageConverter
@Bean public MessageConverter messageConverter(){ return new Jackson2JsonMessageConverter(); }
SpringAMQP中消息的序列化和反序列化实现
- 利用MessageConverter实现的,默认是JDK的序列化
- 注意发送方与接收方必须使用相同的MessageConverter