RabbitMQ
初始MQ
同步通讯和异步通讯
同步调用的问题
微服务间基于Feign的调用就属于同步方式,存在一些问题。
- 耦合度高
- 性能下降
- 资源浪费
- 级联失败
异步调用方案
异步调用常见实现就是事件驱动模式
优势:
- 服务解耦
- 性能提升,吞吐量提高
- 没有强依赖关系
- 流量削峰
缺点:
- 依赖于Broker的可靠性、安全性、吞吐能力
- 架构复杂了,业务没有明显的流程线,不好追踪管理
什么是MQ
MQ,中文是消息队列,字面来看即使存放消息队列。也就是事件驱动架构中的Broker。
RabbitMQ快速入门
RabbitMQ概述
RabbitMQ是基于Erlang语言开发的开源消息中间件
官网:Messaging that just works — RabbitMQ
RabbitMQ部署指南
1.单机部署
我们在Centos7虚拟机中使用Docker来安装。
1.1.下载镜像
方式一:在线拉取
docker pull rabbitmq:3-management
方式二:从本地加载
在课前资料已经提供了镜像包:
上传到虚拟机中后,使用命令加载镜像即可:
docker load -i mq.tar
1.2.安装MQ
执行下面的命令来运行MQ容器:
docker run \
-e RABBITMQ_DEFAULT_USER=xcxc \
-e RABBITMQ_DEFAULT_PASS=xcxc666 \
--name mq \
--hostname mq1 \
-p 15672:15672 \
-p 5672:5672 \
-d \
rabbitmq:latest
1.3.浏览器访问15672端口
如果无法访问,则需要开启管理插件
# 进入mq的内部
docker exec -it mq /bin/bash
# 开启管理插件
rabbitmq-plugins enable rabbitmq_management
2.集群部署
接下来,我们看看如何安装RabbitMQ的集群。
2.1.集群分类
在RabbitMQ的官方文档中,讲述了两种集群的配置方式:
- 普通模式:普通模式集群不进行数据同步,每个MQ都有自己的队列、数据信息(其它元数据信息如交换机等会同步)。例如我们有2个MQ:mq1,和mq2,如果你的消息在mq1,而你连接到了mq2,那么mq2会去mq1拉取消息,然后返回给你。如果mq1宕机,消息就会丢失。
- 镜像模式:与普通模式不同,队列会在各个mq的镜像节点之间同步,因此你连接到任何一个镜像节点,均可获取到消息。而且如果一个节点宕机,并不会导致数据丢失。不过,这种方式增加了数据同步的带宽消耗。
我们先来看普通模式集群。
2.2.设置网络
首先,我们需要让3台MQ互相知道对方的存在。
分别在3台机器中,设置 /etc/hosts文件,添加如下内容:
192.168.150.101 mq1
192.168.150.102 mq2
192.168.150.103 mq3
并在每台机器上测试,是否可以ping通对方:
常见消息模型
- 基本消息队列
- 工作消息队列
发布订阅,又根据交换机类型不同分为三种:
- 广播
- 路由
- 主题
HelloWorld案例
官方的HelloWorld是基于最基础的消息队列模型来实现的,只包括三个角色:
- publisher:消息发布者,将消息发送到队列queue
- queue:消息队列,负责接受并缓存消息
- consumer:订阅队列,处理队列中的消息
基本消息队列的消息发送流程:
- 建立connection
- 创建channel
- 利用channel声明队列
- 利用channel向队列发送消息
基本消息队列的消息接受流程:
- 建立connection
- 创建channel
- 利用channel声明队列
- 定义consumer的消费行为handleDelivery()
- 利用channel将消费者与队列绑定
SpringAMQP
什么是SpringAMQP
官网:https://spring.io/projects/spring-amqp
案例:利用SpringAMQP实现HelloWorld中的基础消息队列功能
发送消息
引入依赖
<!--AMQP依赖,包含RabbitMQ-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
在publisher服务编写application.yml,添加mq连接信息
spring:
rabbitmq:
host: 192.168.72.133
port: 5672
username: xcxc
password: xcxc666
virtual-host: /
编写测试类
package cn.itcast.mq.spring;
import org.junit.jupiter.api.Test;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
/**
* @author xc
* @date 2023/5/8 7:00
*/
@SpringBootTest
public class SpringAmqpTest {
@Autowired
private RabbitTemplate rabbitTemplate;
@Test
public void testSend(){
String queueName = "simple.queue";
String message = "hello xc";
rabbitTemplate.convertAndSend(queueName,message);
}
}
接受消息
- 引依赖
- 配yml
- 编写配置类
package cn.itcast.mq.listener;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;
/**
* @author xc
* @date 2023/5/8 7:08
*/
@Component
public class SpringRabbitListener {
@RabbitListener(queues = "simple.queue")
public void listenSimpleQueue(String msg){
System.out.println("消费者接受到simple.queue的消息:【"+msg+"】");
}
}
Work Queue工作队列
案例:模拟WorkQueue,实现一个队列绑定多个消费者
- 在publisher服务中定义测试方法,每秒产生50条消息,发送到simple.queue
@Test
public void testSend1() throws InterruptedException {
String queueName = "simple.queue";
String message = "hello xc-";
for (int i = 0; i < 50; i++) {
Thread.sleep(20);
rabbitTemplate.convertAndSend(queueName,message+i);
}
}
- 在consumer服务中定义两个消息监听者,都监听simple.queue
package cn.itcast.mq.listener;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;
/**
* @author xc
* @date 2023/5/8 7:08
*/
@Component
@Slf4j
public class SpringRabbitListener {
@RabbitListener(queues = "simple.queue")
public void listenSimpleQueue1(String msg) throws InterruptedException {
log.info("消费者1接受到simple.queue的消息:【"+msg+"】");
Thread.sleep(20);
}
@RabbitListener(queues = "simple.queue")
public void listenSimpleQueue2(String msg) throws InterruptedException {
log.error("消费者2接受到simple.queue的消息:【"+msg+"】");
Thread.sleep(200);
}
}
- 消费者1每秒处理50条消息,消费者2每秒处理10条消息
消息预取限制
logging:
pattern:
dateformat: MM-dd HH:mm:ss:SSS
spring:
rabbitmq:
host: 192.168.72.133
port: 5672
username: xcxc
password: xcxc666
virtual-host: /
listener:
simple:
prefetch: 1 # 每次只取一条消息,处理完进行下一条消息
发布(Publish)、订阅(Subscribe)
发布订阅与之前的区别就是允许将同一个消息发送给多个消费者。实现方式是加入了exchange
常见的exchange类型包括:
- Fanout:广播
- Direct:路由
- Topic:话题
注意 :exchange负责消息路由,而不是存储,路由失败则消息丢失
发布订阅-Fanout Exchange
Fanout Exchange会将接收到的消息路由到每一个跟其绑定的queue
案例:利用SpringAMQP演示FanoutExchange的使用
- 在consumer服务中,利用代码声明队列,交换机,并将两者绑定
package cn.itcast.mq.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;
/**
* @author xc
* @date 2023/5/8 7:48
*/
@Configuration
public class FanoutConfig {
/**
* 声明交换机
*/
@Bean
public FanoutExchange fanoutExchange(){
return new FanoutExchange("xc.fanout");
}
/**
* 声明队列
*/
@Bean
public Queue fanoutQueue1(){
return new Queue("fanout.queue1");
}
@Bean
public Queue fanoutQueue2(){
return new Queue("fanout.queue2");
}
/**
* 绑定交换机与队列
*/
@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
package cn.itcast.mq.listener;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;
/**
* @author xc
* @date 2023/5/8 7:08
*/
@Component
@Slf4j
public class SpringRabbitListener {
@RabbitListener(queues = "simple.queue1")
public void listenFanoutQueue1(String msg) throws InterruptedException {
log.info("消费者1接受到simple.queue1的消息:【"+msg+"】");
}
@RabbitListener(queues = "simple.queue2")
public void listenFanoutQueue2(String msg) throws InterruptedException {
log.error("消费者2接受到simple.queue2的消息:【"+msg+"】");
}
}
- 在publisher中编写测试方法,向itcast.fanout发送消息
@Test
public void testSendExchange() throws InterruptedException {
String exchangeName = "xc.fanout";
String message = "hello xc-";
for (int i = 0; i < 50; i++) {
Thread.sleep(20);
rabbitTemplate.convertAndSend(exchangeName,"",message+i);
}
}
发布订阅-DirectExchange
Direct Exchange会将接收到的消息根据规则路由到指定的Queue,因此称为路由模式
- 每一个Queue都与Exchange设置一个BindingKey
- 发布者发送消息时,指定消息的Routingkey
- Exchange将消息路由到BindingKey与消息RoutingKey一致的队列
绑定交换机和队列
package cn.itcast.mq.listener;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.ExchangeTypes;
import org.springframework.amqp.rabbit.annotation.Exchange;
import org.springframework.amqp.rabbit.annotation.Queue;
import org.springframework.amqp.rabbit.annotation.QueueBinding;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;
/**
* @author xc
* @date 2023/5/8 7:08
*/
@Component
@Slf4j
public class SpringRabbitListener {
@RabbitListener(bindings = @QueueBinding(
value = @Queue(name = "direct.queue1"),
exchange = @Exchange(name = "xc.direct",type = ExchangeTypes.DIRECT),
key = {"red","blue"}
))
public void listenSimpleQueue1(String msg) {
log.info("消费者接受到direct.queue1的消息:【"+msg+"】");
}
@RabbitListener(bindings = @QueueBinding(
value = @Queue(name = "direct.queue2"),
exchange = @Exchange(name = "xc.direct",type = ExchangeTypes.DIRECT),
key = {"red","yellow"}
))
public void listenSimpleQueue2(String msg) {
log.info("消费者接受到direct.queue2的消息:【"+msg+"】");
}
}
发布测试
@Test
public void testSendDirectExchange() throws InterruptedException {
String exchangeName = "xc.direct";
String message = "hello xc-";
for (int i = 0; i < 50; i++) {
rabbitTemplate.convertAndSend(exchangeName,"red",message+i);
}
}
发布订阅-TopicExchange
TopicExchange与DirectExchange类似,区别在于routingKey必须是多个单词的列表,并且以 . 分割
Queue与Exchange指定BindingKey时可以使用通配符:
#:代表0个或多个单词
*:代指一个单词
案例:利用SpringAMQP演示TopicExchange的使用
绑定交换机和队列
package cn.itcast.mq.listener;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.ExchangeTypes;
import org.springframework.amqp.rabbit.annotation.Exchange;
import org.springframework.amqp.rabbit.annotation.Queue;
import org.springframework.amqp.rabbit.annotation.QueueBinding;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;
/**
* @author xc
* @date 2023/5/8 7:08
*/
@Component
@Slf4j
public class SpringRabbitListener {
@RabbitListener(bindings = @QueueBinding(
value = @Queue(name = "topic.queue1"),
exchange = @Exchange(name = "xc.topic",type = ExchangeTypes.TOPIC),
key = "china.#"
))
public void listenTopicQueue1(String msg) {
log.info("消费者接受到direct.queue1的消息:【"+msg+"】");
}
@RabbitListener(bindings = @QueueBinding(
value = @Queue(name = "topic.queue2"),
exchange = @Exchange(name = "xc.topic",type = ExchangeTypes.TOPIC),
key = "*.news"
))
public void listenTopicQueue2(String msg) {
log.info("消费者接受到direct.queue2的消息:【"+msg+"】");
}
}
发布测试
@Test
public void testSendTopicExchange1() throws InterruptedException {
String exchangeName = "xc.topic";
String message = "hello red";
rabbitTemplate.convertAndSend(exchangeName,"china.news",message);
}
@Test
public void testSendTopicExchange2() throws InterruptedException {
String exchangeName = "xc.topic";
String message = "hello red";
rabbitTemplate.convertAndSend(exchangeName,"am.news",message);
}
@Test
public void testSendTopicExchange3() throws InterruptedException {
String exchangeName = "xc.topic";
String message = "hello red";
rabbitTemplate.convertAndSend(exchangeName,"china.weahter",message);
}
消息转换器
发送消息
引入依赖
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
声明bean
package cn.itcast.mq;
import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter;
import org.springframework.amqp.support.converter.MessageConverter;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
@SpringBootApplication
public class PublisherApplication {
public static void main(String[] args) {
SpringApplication.run(PublisherApplication.class);
}
@Bean
public MessageConverter messageConverter(){
return new Jackson2JsonMessageConverter();
}
}
因为消息转换是在底层做的,所以消息直接发就行
接受消息
引入依赖
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
声明bean
package cn.itcast.mq;
import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter;
import org.springframework.amqp.support.converter.MessageConverter;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
@SpringBootApplication
public class ConsumerApplication {
public static void main(String[] args) {
SpringApplication.run(ConsumerApplication.class, args);
}
@Bean
public MessageConverter messageConverter(){
return new Jackson2JsonMessageConverter();
}
}
因为消息转换是在底层做的,所以消息直接收就行
定义收消费者
@RabbitListener(queues = "object.queue")
public void listenObjQueue(Map<String,Object> msg){
System.out.println(msg);
}
;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
@SpringBootApplication
public class ConsumerApplication {
public static void main(String[] args) {
SpringApplication.run(ConsumerApplication.class, args);
}
@Bean
public MessageConverter messageConverter(){
return new Jackson2JsonMessageConverter();
}
}
因为消息转换是在底层做的,所以消息直接收就行
定义收消费者
@RabbitListener(queues = "object.queue")
public void listenObjQueue(Map<String,Object> msg){
System.out.println(msg);
}