MQ
1. 简介
1.1 什么是消息队列
我们可以把消息队列看作是一个存放消息的容器,当我们需要使用消息的时候,直接从容器中取出消息供自己使用即可。
1.2 为什么要用消息队列
通常来说,使用消息队列能为我们的系统带来下面三点好处:
- 通过异步处理提高系统性能(减少响应所需时间)。
- 扣减库存、记录日志等异步操作
- 削峰/限流
- 上游大量的请求存放到队列中,下游从队列取数据
- 降低系统耦合性。
- 增加业务功能订阅队列即可,取消功能 取消订阅即可。
因为用户请求数据写入消息队列之后就立即返回给用户了,但是请求数据在后续的业务校验、写数据库等操作中可能失败。因此,使用消息队列进行异步处理之后,需要适当修改业务流程进行配合,比如用户在提交订单之后,订单数据写入消息队列,不能立即返回用户订单提交成功,需要在消息队列的订单消费者进程真正处理完该订单之后,甚至出库后,再通过电子邮件或短信通知用户订单成功,以免交易纠纷。
broker事件代理者
支付完成后,订单等其他服务就会异步执行,不会影响用户的体验
1.3 存在的问题
- 系统可用性降低: 系统可用性在某种程度上降低,为什么这样说呢?在加入 MQ 之前,你不用考虑消息丢失或者说 MQ 挂掉等等的情况,但是,引入 MQ 之后你就需要去考虑了!
- 系统复杂性提高: 加入 MQ 之后,你需要保证消息没有被重复消费、处理消息丢失的情况、保证消息传递的顺序性等等问题!
- 一致性问题: 我上面讲了消息队列可以实现异步,消息队列带来的异步确实可以提高系统响应速度。但是,万一消息的真正消费者并没有正确消费消息怎么办?这样就会导致数据不一致的情况了!
2. 什么是MQ
MQ(MessageQueue),中文是消息队列,字面来看就是存放消息的队列。也就是事件驱动架构中的Broker。
怎么选?
- 大量数据,要求不高:日志记录
- 大型企业要定制:RocketMQ
- 普通企业,要求可靠性:RabbitMQ
2.1 为啥叫消息队列
首先我问一个问题,消息队列为什么要叫消息队列?
你可能觉得很弱智,这玩意不就是存放消息的队列嘛?不叫消息队列叫什么?
的确,早期的消息中间件是通过 队列 这一模型来实现的,可能是历史原因,我们都习惯把消息中间件成为消息队列。
这里涉及到了两种模型:队列模型
主题模型
2.1.1 队列模型
就像我们理解队列一样,消息中间件的队列模型就真的只是一个队列
“广播” 的概念,也就是说如果我们此时我们需要将一个消息发送给多个消费者(比如此时我需要将信息发送给短信系统和邮件系统),这个时候单个队列即不能满足需求了。
当然你可以让 Producer
生产消息放入多个队列中,然后每个队列去对应每一个消费者。问题是可以解决,创建多个队列并且复制多份消息是会很影响资源和性能的。
2.1.2 主题模型
有,那就是 主题模型 或者可以称为 发布订阅模型 。
在主题模型中,消息的生产者称为 发布者(Publisher) ,消息的消费者称为 订阅者(Subscriber) ,存放消息的容器称为 主题(Topic) 。
其中,发布者将消息发送到指定主题中,订阅者需要 提前订阅主题 才能接受特定主题的消息。
RocketMQ
中的消息模型就是按照 主题模型 所实现的。你可能会好奇这个 主题 到底是怎么实现的呢?你上面也没有讲到呀!
其实对于主题模型的实现来说每个消息中间件的底层设计都是不一样的,就比如 Kafka
中的 分区 ,RocketMQ
中的 队列 ,RabbitMQ
中的 Exchange
。我们可以理解为 主题模型/发布订阅模型 就是一个标准,那些中间件只不过照着这个标准去实现而已。
3. 部署
mq的docker镜像
pull
docker pull rabbitmq:3-management
或者直接下载
链接:https://pan.baidu.com/s/1C-hW4BiEc4WoQA_PPzXl2g?pwd=3bav
提取码:3bav
docker load -i mq.tar
3.1 启动MQ
docker run \
-e RABBITMQ_DEFAULT_USER=aguomq \
-e RABBITMQ_DEFAULT_PASS=1314520 \
--name mq \
--hostname mq1 \
-p 15672:15672 \
-p 5672:5672 \
-d \
rabbitmq:3-management
3.2 可视化页面
之后可以打开 http://aguo.pro:15672来查看
- 连接
- 包含所有的连接,如生产者与消费者的连接信息。
- 交换机
- 负责提交消息给队列
- 队列
- 存放的任务等
- 管理
- 包含用户管理、虚拟主机管理
虚拟主机,对于不同用户使用同一个rabbitmq实现不同的业务功能时使用,起到隔离作用。
结果图如下
3.3 概念
- channel:操作MQ的工具
- exchange:路由消息到队列中
- queue:缓存消息
- virtual host:虚拟主机,是对queue、exchange等资源的泛辑分组
4. 常见消息模型
4.1 基本模型
基本消息队列(BasicQueue)
工作消息队列(WorkQueue)
4.1.1 HelloWord案例
官方的HelloWorld是基于最基础的消息队列模型来实现的,只包括三个角色:
- publisher:消息发布者,将消息发送到队列queue
- queue:消息队列,负责接受并缓存消息
- consumer:订阅队列,处理队列中的消息
发布者
public static void main(String[] args) throws IOException, TimeoutException {
// 1.建立连接
ConnectionFactory factory = new ConnectionFactory();
// 1.1.设置连接参数,分别是:主机名、端口号、vhost、用户名、密码
factory.setHost("aguo.pro");
factory.setPort(5672);
factory.setVirtualHost("/");
factory.setUsername("aguomq");
factory.setPassword("1314520");
// 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("等待接收消息。。。。");
}
消费者
public static void main(String[] args)throws IOException, TimeoutException {
// 1.建立连接
ConnectionFactory factory = new ConnectionFactory();
// 1.1.设置连接参数,分别是:主机名、端口号、vhost、用户名、密码
factory.setHost("aguo.pro");
factory.setPort(5672);
factory.setVirtualHost("/");
factory.setUsername("aguomq");
factory.setPassword("1314520");
// 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();
}
4.2 含有交换机模型
发布订阅(Publish、Subscribe),又根据交换机类型不同分为三种:
- Fanout Exchange:广播
- Direct Exchange:路由
- Topic Exchange:主题
5.SpringAMQP
5.1 什么是SpringAMQP
类似JDBC是接口,MySQL实现这个接口。
核心:便捷得发送、便捷得接收、自动化队列声明
5.2 实现HelloWord
共三部分
- 导入依赖
- 配置rabbitmq地址
- 注入使用/监听
生产者
- 导入依赖
<!--AMQP依赖,包含RabbitMQ-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
- 配置地址
spring:
rabbitmq:
host: aguo.pro
username: aguomq
password: 1314520
virtual-host: /
port: 5672
- 使用
@RunWith(SpringRunner.class)
@SpringBootTest
public class SpringAmqpTest {
@Autowired
private RabbitTemplate rabbitTemplate;
@Test
public void sendMessage() {
rabbitTemplate.convertAndSend("simple.queue","hello Springamqp");
}
}
消费者
SpringAMOP如何接收消息?
- 引入amgp的starter依赖
- 配置RabbitMQ:地址
- 定义类,添加@Component注解
- 类中声明方法,添加@RabbitListener注解,方法参数就时消息
注意:消息一旦消费就会从队列删除,RabbitMQ没有消息回溯功能
同样都是要导入依赖、配置地址,主要是监听方法的实现,如下
@Component//被spring容器管理
public class rabbitConsumer {
@RabbitListener(queues = "simple.queue")//定义监听器
public void receiveMessage(String message){
System.out.println("收到的消息是:"+message);
}
}
5.2.1 消息预取
rabbitmq中,消费者先从队列拿过来放着,然后再处理,弊端就是消费者没有根据自己的能力来取任务,通通预取了出来。
配置:
spring:
rabbitmq:
listener:
simple:
prefetch: 1 #可以任意配置值
5.3 发布与订阅
publish、subscribe
交换的转发规则要根据其类型来决定:
- Fanout:广播
- Direct:路由
- Topic:话题
API中的交换机继承结构图
注意:exchange负责消息路由,而不是存储,路由转发失败那么消息就会丢失!
- 交换机的作用
- 接收oublisher发送的消息
- 将消息按照规则路由到与之绑定的队列
- 不能缓存消息,路由失败,消息丢失
- FanoutExchange的会将消息路由到每个绑定的队列
- 声明队列、交换机、绑定关系的Bean是什么?
- Queue
- FanoutExchange
- Binding
5.4 广播(FanoutExchange)
实现思路如下:
- 在consumer服务中,利用代码声明队列、交换机,并将两者绑定
- 在consumer服务中,编写两个消费者方法,分别监听fanout.queue1和fanout.queue2
- 在publisher中编写测试方法,向litcast.fanout发送消息
可以看出,主要是在consumer中配置,publisher不关心任务放在哪个队列,也不关心谁来处理,只需要告诉交换机是谁然后丢任务给他
Consumer配置类
注意Queue的类型!
import org.springframework.amqp.core.Binding;
import org.springframework.amqp.core.Queue;
@Configuration
public class FanoutConfig {
//声明交换机
@Bean
public FanoutExchange fanoutExchange() {
return new FanoutExchange("itcast.fanout");
}
//声明队列1
@Bean
public Queue fanoutQueue1() {
return new Queue("itcast.qunue1");
}
//绑定队列1
@Bean
public Binding fanoutBinding1(FanoutExchange fanoutExchange,
Queue fanoutQueue1) {
return BindingBuilder
.bind(fanoutQueue1)
.to(fanoutExchange);
}
//声明队列2
@Bean
public Queue fanoutQueue2() {
return new Queue("itcast.qunue2");
}
//绑定队列2
@Bean
public Binding fanoutBinding2(FanoutExchange fanoutExchange,
Queue fanoutQueue2) {
return BindingBuilder
.bind(fanoutQueue2)
.to(fanoutExchange);
}
}
consumer监听
跟之前的监听配置一样,没啥区别就是绑定的额队列不一样罢了
@Component
public class rabbitConsumer {
@RabbitListener(queues = "aguo.qunue1")
public void receiveMessage(String message){
System.out.println("收到的消息11是:"+message);
}
@RabbitListener(queues = "aguo.qunue2")
public void receiveMessage2(String message){
System.out.println("收到的消息22是:"+message);
}
}
publisher发布者
//交换机名称
String exchangeName = "itcast.fanout";
//消息
String message = "Hello rabbitMq";
//发送消息
rabbitTemplate.convertAndSend(exchangeName,"",message);
5.5 路由(DirectExchange)
Direct Exchange会将接收到的消息根据规则路由到指定的Queue,因此称为路由模式(routes)。
- 每一个Queue都与Exchangei设置一个BindingKey
- 发布者发送消息时,指定消息的RoutingKey
- Exchange将消息路由到Binding Key与消息Routing Key一致的队列
aguo大白话
就是我们给队列标记不同的key,发布者发消息要指定key,交换机将该消息丢到拥有这个key的队列中(可以重复)
实现
这里我们使用注解来实现
consumer
//消费者1
@RabbitListener(bindings = @QueueBinding(
exchange = @Exchange(name = "itcast.direct",type = ExchangeTypes.DIRECT),
value = @Queue(name = "direct.queue1"),
key = {"red","blue"}
))
public void receiveMessage2(String message){
System.out.println("消费者 red blue:"+message);
}
//消费者2
@RabbitListener(bindings = @QueueBinding(
exchange = @Exchange(name = "itcast.direct",type = ExchangeTypes.DIRECT),
value = @Queue(name = "direct.queue2"),
key = {"red","yellow"}
))
public void receiveMessage3(String message){
System.out.println("消费者 red yellow:"+message);
}
解释下注解
@RabbitListener(
bindings = @QueueBinding(//绑定注解
//指定交换机、指定交换机的模式 Fanout、direct
exchange = @Exchange(name = "itcast.direct",type = ExchangeTypes.DIRECT),
//指定该消费者所绑定的队列名称
value = @Queue(name = "direct.queue1"),
//指定该队列有什么key
key = {"red","blue"}
))
publisher
@Test
public void sendMessage() throws InterruptedException {
//交换机名称
String exchangeName = "itcast.direct";
//发送的key
String key ="red";
//消息
String message = "Hello rabbitMq:"+key;
//发送消息
rabbitTemplate.convertAndSend(exchangeName,key,message);
}
5.6 话题(TopicExchange)
TopicExchange与DirectExchange:类似,区别在于routing Key必须是多个单词的列表,并且以,分割。Queue与Exchange指定BindingKey时可以使用通配符:
- #: 代指0个或多个单词
- *: 代指一个单词
就是给一个队列一个统配的key,如china.#,发布者发消息指定具体的key,交换机去匹配队列的统配路径,匹配成功则把消息丢过去
如
China.news 中国所有新闻
China.# :中国所有东西
*.weather 所有国家的天气
consumer
@RabbitListener(bindings = @QueueBinding(
exchange = @Exchange(name = "itcast.topic",type = ExchangeTypes.TOPIC),
value = @Queue(name = "topic.queue1"),
key = "china.#"
))
public void receiveMessage1(String message){
System.out.println("消费者 china:"+message);
}
@RabbitListener(bindings = @QueueBinding(
exchange = @Exchange(name = "itcast.topic",type = ExchangeTypes.TOPIC),
value = @Queue(name = "topic.queue2"),
key = "#.news"
))
public void receiveMessage2(String message){
System.out.println("消费者 news"+message);
}
注解解释
@RabbitListener(
bindings = @QueueBinding(//绑定注解
//指定交换机、指定交换机的模式 Fanout、direct
exchange = @Exchange(name = "itcast.topic",type = ExchangeTypes.TOPIC),//交换机类型指定是topic
value = @Queue(name = "topic.queue2"),
//指定队列的队列统配名
key = "#.news"
))
publisher
@Test
public void sendMessage() throws InterruptedException {
//交换机名称
String exchangeName = "itcast.topic";
//发送的key
String key ="beijing.news";
//消息
String message = "Hello rabbitMq Topic"+key;
//发送消息
rabbitTemplate.convertAndSend(exchangeName,key,message);
}
5.7 消息者想都消费
消费者不指定队列名称
@RabbitListener(bindings = @QueueBinding(
exchange = @Exchange(name = "aguo.fanout",type = ExchangeTypes.FANOUT),
value = @Queue()
))
public void repeatMessage1(String message){
System.out.println("多次消费者消费1"+message);
}
@RabbitListener(bindings = @QueueBinding(
exchange = @Exchange(name = "aguo.fanout",type = ExchangeTypes.FANOUT),
value = @Queue()
))
public void repeatMessage2(String message){
System.out.println("多次消费者消费2"+message);
}
最后会自动随机生成一个队列
消费者指定交换机即可
@Test
public void sendMessage() throws InterruptedException {
//交换机名
String exchangeName = "aguo.fanout";
rabbitTemplate.convertAndSend(exchangeName,"","广播消息");
}
- 缺点
- 消费者必须先与生成者运行,这样才能创建出临时队列出来
6. 消息转化器
java是面向对象的,如果我们直接传入对象的话,那么默认会使用JDK的序列化方式,而我们希望的是使用的是JSON的系列化方式
一定要注意发送发与接收方必须使用相同的MessageConverter解析器
6.1 配置JSON转化器
导入Jackson依赖
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
配置类中配置JSON转化器
@Bean
public MessageConverter messageConverter() {
return new Jackson2JsonMessageConverter();
}
6.2 使用
发送消息时候,会自动转化为JSON格式的字符串
接收消息时候,会自动将JSON字符串转为Bean对象
publisher
@Test
public void sendMessage() throws InterruptedException {
//交换机名
String exchangeName = "itcast.fanout";
//对象名
User user = new User(1, "aguo");
//发送消息
rabbitTemplate.convertAndSend(exchangeName,"",user);
}
看看管理页面
消费者
@RabbitListener(queues = "itcast.qunue2")
public void receiveMessage11(User user){
System.out.println("收到的对象是:"+user);
}