一、消息队列(MQ)介绍
1.1 什么是消息队列
- 消息队列,即MQ,Message Queue。
- 消息队列是典型的:生产者、消费者模型。生产者不断向消息队列中生产消息,消费者不断的从队列中获取消息。因为消息的生产和消费都是异步的,而且只关心消息的发送和接收,没有业务逻辑的侵入,这样就实现了生产者和消费者的解耦。
如果以后有其它系统也依赖商品服务的数据,同样监听消息即可,商品服务无需任何代码修改。
1.2 AMQP和JMS
MQ是消息通信的模型,并不是具体实现。现在实现MQ的有两种主流方式:AMQP、JMS。
两者间的区别和联系:
- JMS是定义了统一的接口,来对消息操作进行统一;AMQP是通过规定协议来统一数据交互的格式
- JMS限定了必须使用Java语言;AMQP只是协议,不规定实现方式,因此是跨语言的。
- JMS规定了两种消息模型;而AMQP的消息模型更加丰富
1.3 常见MQ产品
- ActiveMQ:基于JMS
- RabbitMQ:基于AMQP协议,erlang语言开发,稳定性好
- RocketMQ:基于JMS,阿里巴巴产品,目前交由Apache基金会
- Kafka:分布式消息系统,高吞吐量
二、RabbitMQ消息队列模型
RabbitMQ提供了6种消息模型,但是第6种其实是RPC,并不是MQ,因此不予学习。那么也就剩下5种。
但是其实3、4、5这三种都属于订阅模型,只不过进行路由的方式不同。
2.1 简单队列模式(Queue)
RabbitMQ是一个消息代理:它接受和转发消息。 你可以把它想象成一个邮局:当你把邮件放在邮箱里时,你可以确定邮差先生最终会把邮件发送给你的收件人。 在这个比喻中,RabbitMQ是邮政信箱,邮局和邮递员。
RabbitMQ与邮局的主要区别是它不处理纸张,而是接受,存储和转发数据消息的二进制数据块。
P(producer/ publisher):生产者,一个发送消息的用户应用程序。
C(consumer):消费者,消费和接收有类似的意思,消费者是一个主要用来等待接收消息的用户应用程序
队列(红色区域):RabbitMQ内部类似于邮箱的一个概念。虽然消息流经RabbitMQ和你的应用程序,但是它们只能存储在队列中。队列只受主机的内存和磁盘限制,实质上是一个大的消息缓冲区。许多生产者可以发送消息到一个队列,许多消费者可以尝试从一个队列接收数据。
P(producer/ publisher):生产者,一个发送消息的用户应用程序。
C(consumer):消费者,消费和接收有类似的意思,消费者是一个主要用来等待接收消息的用户应用程序
队列(红色区域):RabbitMQ内部类似于邮箱的一个概念。虽然消息流经RabbitMQ和你的应用程序,但是它们只能存储在队列中。队列只受主机的内存和磁盘限制,实质上是一个大的消息缓冲区。许多生产者可以发送消息到一个队列,许多消费者可以尝试从一个队列接收数据。
2.2 work消息模型
工作队列或者竞争消费者模式
任务是平均分配给消费者的。也就是说消费者是平均接收的。类似于轮训。
- 配置类
@Configuration
public class WorkQueueConfig {
/**
* 队列名.
*/
private final String work = "work_queue";
@Bean
public Queue workQueue() {
return new Queue(work);
}
}
- 接收端
@Component
public class ReceiveListener {
// @RabbitListener(queues = "spring.test.queue")
@RabbitListener(queues = "work_queue")
public void receive01(String message){
System.out.println("--lis1" + message);
}
@RabbitListener(queues = "work_queue")
public void receive02(String message){
System.out.println("lis2" + message);
}
}
- 发送端
@RequestMapping("/send02")
public void send02() {
for (int i = 0; i < 50; i++) {
rabbitTemplate.convertAndSend("work_queue", "工作消息队列" + i);
}
}
部分结果:
可以看到,任务是平均分发的。
但是这样会出现一个问题。处理的快的消费者处理完后就处于闲置状态,而处理得慢的消费者还在慢慢执行。
2.2.3 能者多劳
- 平均分配出现的问题:
假如消费者1比消费者2的效率要低,一次任务的耗时较长,然而两人最终消费的消息数量是一样的,消费者2大量时间处于空闲状态,消费者1一直忙碌,这好像不太合理。
正确的做法应该是消费越快的人,消费的越多。
- 怎么实现呢?
我们可以设置每次只处理一个信息,告诉RabbitMQ一次不要向一个消费端发送多于一条消息。 或者换句话说,不要向一个消费端发送新消息,直到它处理并确认了前一个消息。 相反,它会将其分派给不是处于忙碌状态的下一个消费端。
修改配置文件如下:
部分运行结果:
2.3 发布者订阅者模式(Publish/Subscribe)
1、一个生产者,多个消费者
2、每一个消费者都有自己的一个队列
3、生产者没有将消息直接发送到队列,而是发送到了交换机
4、每个队列都要绑定到交换机
5、生产者发送的消息,经过交换机到达队列,实现一个消息被多个消费者获取的目的
例:
配置类
@Configuration
public class FanoutQueueConfig {
/**
* 声明队列名
*/
private final String fanout1 = "fanout_queue_1";
private final String fanout2 = "fanout_queue_2";
/**
* 声明交换机的名字.
*/
private final String fanoutExchange = "fanoutExchange";
//声明队列1
@Bean
public Queue fanoutQueue1() {
return new Queue(fanout1);
}
//声明队列2
@Bean
public Queue fanoutQueue2() {
return new Queue(fanout2);
}
//声明交换机
@Bean
public FanoutExchange exchange() {
return new FanoutExchange(fanoutExchange);
}
// 将队列1绑定到交换机
@Bean
public Binding bindingQueue1() {
return BindingBuilder.bind(fanoutQueue1()).to(exchange());
}
// 将队列2绑定到交换机
@Bean
public Binding bindingQueue2() {
return BindingBuilder.bind(fanoutQueue2()).to(exchange());
}
}
监听器(获得消息的)
@Component
public class ReceiveListener {
@RabbitListener(queues = "fanout_queue_1")
public void receive03(String message) {
System.out.println("f-lis1--" + message);
}
@RabbitListener(queues = "fanout_queue_2")
public void receive04(String message) {
System.out.println("f-lis2--" + message);
}
}
发送请求
@RequestMapping("/send03")
public void send03() {
System.out.println("订阅模式消息");
for (int i = 0; i < 5; i++) {
rabbitTemplate.convertAndSend("fanoutExchange", "", "订阅模式消息" + i);
}
}
2.4 路由模式(Direct)
在订阅模式中,生产者发布消息,所有消费者都可以获取所有消息。
在路由模式中,我们将添加一个功能 —— 只能订阅一部分消息。 例如,我们只能将重要的错误消息引导到日志文件(以节省磁盘空间),同时仍然能够在控制台上打印所有日志消息。
但是,在某些场景下,我们希望不同的消息被不同的队列消费。这时就要用到Direct类型的Exchange。
在Direct模型下,队列与交换机的绑定,不能是任意绑定了,而是要指定一个RoutingKey(路由key)
消息的发送方在向Exchange发送消息时,也必须指定消息的routing key。
路由模式不指定routing key,就变成了发布者订阅模式
配置类:
@Configuration
public class DirectQueueConfig {
/**
* 声明队列名.
*/
private final String direct1 = "direct_queue_1";
private final String direct2 = "direct_queue_2";
/**
* 声明交换机的名字.
*/
private final String directExchange = "directExchange";
//队列1
@Bean
public Queue directQueue1() {
return new Queue(direct1);
}
//队列2
@Bean
public Queue directQueue2() {
return new Queue(direct2);
}
//交换机
@Bean
public DirectExchange directExchange() {
return new DirectExchange(directExchange);
}
//绑定队列1到交换机
@Bean
public Binding bindingDirectExchange1() {
return BindingBuilder.bind(directQueue1())
.to(directExchange())
.with("update"); // 指定路由
}
//绑定队列2到交换机
@Bean
public Binding bindingDirectExchange2() {
return BindingBuilder.bind(directQueue2())
.to(directExchange())
.with("add"); //指定路由
}
}
发送请求:
@RequestMapping("/send04")
public void send04() {
for (int i = 0; i < 5; i++) {
String message = "路由模式--routingKey=update消息" + i;
System.out.println("我是生产信息的:" + message);
rabbitTemplate.convertAndSend("directExchange", "update", message);
}
}
@RequestMapping("/send05")
public void send05() {
for (int i = 0; i < 5; i++) {
String message = "路由模式--routingKey=add消息" + i;
System.out.println("我是生产信息的:" + message);
rabbitTemplate.convertAndSend("directExchange", "add", message);
}
}
监听接收消息
@RabbitListener(queues = "direct_queue_1")
public void receive05(String message) {
System.out.println("d-lis1--" + message);
}
@RabbitListener(queues = "direct_queue_2")
public void receive06(String message) {
System.out.println("d-lis2--" + message);
}
2.5 主题模式(Topic)
Topic
类型的Exchange
与Direct
相比,都是可以根据RoutingKey
把消息路由到不同的队列。只不过Topic
类型Exchange
可以让队列在绑定Routing key
的时候使用通配符!
Routingkey
由一个或多个单词组成,多个单词之间以”.”分割,例如: item.insert
通配符规则:
#
:匹配一个或多个词
*
:匹配不多不少恰好1个词