RabbitMQ 基础
持续更新…
MQ(消息队列)
消息队列的作用
- 解耦:在项目启动之初是很难预测未来会遇到什么困难的,消息中间件在处理过程中插入了一个隐含的,基于数据的接口层,两边都实现这个接口,这样就允许独立的修改或者扩展两边的处理过程,只要两边遵守相同的接口约束即可。
- 冗余存储:在某些情况下处理数据的过程中会失败,消息中间件允许把数据持久化直到它们完全被处理。
- 削峰:在访问量剧增的情况下,而应用仍然需要发挥作用,虽然这样的突发流量并不常见,但是使用消息中间件采用队列的形式可以减少突发访问压力,不会因为突发的超时负荷要求而崩溃。
- 扩展性:消息中间件解耦了应用的过程,所以提供消息入队和处理的效率是很容易的,只需要增加处理流程就可以了。
- 可恢复性:当系统一部分组件失效时,不会影响到整个系统。消息中间件降低了进程间的耦合性,当一个处理消息的进程挂掉后,加入消息中间件的消息仍然可以在系统恢复后重新处理。
- 缓冲:消息中间件通过一个缓冲层来帮助任务最高效率的执行。
- 异步通信:通过把把消息发送给消息中间件队列,消息中间件并不立即处理它,后续再慢慢处理。
- 顺序保证:在大多数场景下,处理数据的顺序也很重要,大部分消息中间件支持一定的顺序性。
AMQP简述
AMQP(Advanced Message Queuing Protocol,高级消息队列协议),是个线路层的协议规范,而不是API规范,由于AMQP是一个线路层协议规范,因此它天然就是跨平台的,就像SMTP、HTTP等协议一样,只要开发者按照规范格式发送数据,任何平台都可以通过AMQP进行消息交互。像目前流行的StormMQ、RabbitMQ等都实现了AMQP。
什么是RabbitMQ?
RabbitMQ是一个实现了AMQP的Apache开源消息中间件,使用高性能的Erlang编写。RabbitMQ有可靠性、支持多种协议、高可用、支持消息集群、多语言客户端(如:Python、Ruby、.NET、Java、JMS、C、PHP、ActionScript、XMPP、STOMP等),路由(包括点对点和发布/订阅)等特点,在分布式系统中存储转发信息,具有极佳的性能。
RabbitMQ的主要特点
- 可靠性: RabbitMQ使用一些机制来保证可靠性,如持久化、传输确认及发布确认等。
- 灵活的路由: 在消息进入队列之前,通过交换器来路由消息。对于典型的路由功能,RabbitMQ己经提供了一些内置的交换器来实现。针对更复杂的路由功能,可以将多个交换器绑定在一起,也可以通过插件机制来实现自己的交换器。
- 扩展性: 多个RabbitMQ节点可以组成一个集群,也可以根据实际业务情况动态地扩展集群中节点。
- 高可用性: 队列可以在集群中的机器上设置镜像,使得在部分节点出现问题的情况下队仍然可用。
- 多种协议: RabbitMQ除了原生支持AMQP协议,还支持STOMP,MQTT等多种消息中间件协议。
- 多语言客户端: RabbitMQ几乎支持所有常用语言,比如Jav a、Python、Ruby、PHP、C#、JavaScript等。
- 管理界面: RabbitMQ提供了一个易用的用户界面,使得用户可以监控和管理消息、集群中的节点等。
- 插件机制: RabbitMQ提供了许多插件,以实现从多方面进行扩展,当然也可以编写自己的插件。
如下图,每一个服务可对应一个微服务,通过MQ系统可以实现异步机制并对每个服务模块解耦化,而不必像传统的同步机制:
消息队列角色
- Provider:消息生产者,生产消息,发送消息。
- Consumer:消息消费者:接受消息,使用消息。
- Queue:有序存储消息。类似仓库、中转站。队列可以存储很多的消息,因为它基本上是一个无限制的缓冲区,前提是你的机器有足够的存储空间。多个生产者可以将消息发送到同一个队列中,多个消费者也可以只从同一个队列接收数据。
Docker安装RabbitMQ
sudo docker run -itd /
-e RABBITMQ_DEFAULT_USER=root -e RABBITMQ_DEFAULT_PASS=root /
-p 15555:15672 -p 5555:5672 rabbitmq:management
打开相应网址:
Linux安装RabbitMQ
首先安装Erlang,然后安装rabbitMQ
sudo apt-get install erlang-nox
sudo apt-get install rabbitmq-server
sudo rabbitmqctl add_user root root # 添加用户root,密码为root
sudo rabbitmqctl set_user_tags root administrator # 赋予administrator权限
sudo rabbitmqctl set_permissions -p / root '.*' '.*' '.*' # 赋予virtual host中所有资源的配置、写、读权限以便管理其中的资源
启用RabbitMQ图形化管理界面插件
sudo rabbitmq-plugins enable rabbitmq_management
此时同一局域网内主机均可以访问该管理网站…
简单案例
通过SpringBoot使用RabbitMQ
主要依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
添加application.yml文件:
server:
port: 11111
spring:
rabbitmq:
host: 192.168.50.248 # rabbitMQ主机名
port: 5672 # rabbitMQ端口
username: root # rabbitMQ用户名
password: root # rabbitMQ密码
# 自定义队列名称
rabbitMQ:
queue:
name: Queue-1
添加队列:
package cn.wu.config;
import org.springframework.amqp.core.Queue;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class QueueConfig {
@Value("${rabbitMQ.queue.name}")
private String queueName;
@Bean // 注入IoC管理
public Queue createQueue(){
return new Queue(queueName);
}
}
添加消息消费者:
package cn.wu.consumer;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;
@Component
@Slf4j
public class Receiver {
// 接收消息并处理
@RabbitListener(queues = {"${rabbitMQ.queue.name}"})
public void handler(String msg){
// 处理消息
log.info("接收到的消息为: "+msg);
}
}
添加消息生产者:
package cn.wu.provider;
import org.springframework.amqp.core.AmqpTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
@Component("senderBean")
public class Sender {
private AmqpTemplate amqpTemplate;
@Autowired
public void setAmqpTemplate(AmqpTemplate amqpTemplate) {
this.amqpTemplate = amqpTemplate;
}
@Value("${rabbitMQ.queue.name}")
private String queueName;
public void sendMessage(String msg){
// 向名称为queueName的队列转化并发送消息msg
amqpTemplate.convertAndSend(this.queueName,msg);
}
}
新建控制层:
package cn.wu.controller;
import cn.wu.provider.Sender;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class TestController {
Sender sender;
@Autowired
@Qualifier("senderBean")
public void setSender(Sender sender) {
this.sender = sender;
}
@GetMapping("/test/{msg}")
public String test(@PathVariable("msg") String msg){
sender.sendMessage(msg);
return "";
}
}
此时启动后,可以得到连接信息如下:
访问相应URL(http://localhost:11111/test/Hello World):
RabbitMQ原理图
交换策略
- Direct(发布与订阅完全匹配)
- Fanout(广播)
- Topic(主题,规则匹配)
Direct方式(注解方式)
修改yml文件:
# 自定义队列名称
rabbitMQ:
queueName-1: queue-1 # 队列1的名称
queueName-2: queue-2 # 队列2的名称
exchangeName: exchangeName # 交换器名称
routingKeyName-1: routingKeyName1 # 路由键1的名称 用于绑定队列1 发送者根据该路由键来选择队列
routingKeyName-2: routingKeyName2 # 路由键2的名称 用于绑定队列2 发送者根据该路由键来选择队列
修改消费者(添加绑定配置… ):
package cn.wu.consumer;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.ExchangeTypes;
import org.springframework.amqp.rabbit.annotation.*;
import org.springframework.stereotype.Component;
@Component
@RabbitListener(bindings = {@QueueBinding(
value = @Queue(value = "${rabbitMQ.queueName-1}", durable = "true"), // 设置绑定的队列名称,持久存储
exchange = @Exchange(name = "${rabbitMQ.exchangeName}", // 交换器的名称
type = ExchangeTypes.DIRECT), // 交换器执行的策略
key = "${rabbitMQ.routingKeyName-1}" // 路由键,绑定交换器和队列
)}
)
@Slf4j
public class ReceiverOne {
// 接收消息并处理
@RabbitHandler // 配合@RabbitListener使用,根据接受的参数类型进入具体的方法
public void handler(String msg){
// 处理业务流程
// 事件处理
log.info("消息消费者I 接收到的消息为: "+msg);
}
}
package cn.wu.consumer;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.ExchangeTypes;
import org.springframework.amqp.rabbit.annotation.*;
import org.springframework.stereotype.Component;
@Component
@Slf4j
@RabbitListener(bindings = {@QueueBinding(
value = @Queue(value = "${rabbitMQ.queueName-2}", durable = "true"), // 设置绑定的队列名称,以及作为可删除的临时队列
exchange = @Exchange(name = "${rabbitMQ.exchangeName}", // 交换器的名称
type = ExchangeTypes.DIRECT), // 交换器执行的策略
key = "${rabbitMQ.routingKeyName-2}" // 路由键,绑定交换器和队列
)}
)
public class ReceiverTwo {
// 接收消息并处理
@RabbitHandler
public void handler(String msg){
// 处理业务流程
log.info("消息消费者II 接收到的消息为: "+msg);
}
}
修改生产者:
package cn.wu.provider;
import org.springframework.amqp.core.AmqpTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
@Component("senderBean")
public class Sender {
private AmqpTemplate amqpTemplate;
@Autowired
public void setAmqpTemplate(AmqpTemplate amqpTemplate) {
this.amqpTemplate = amqpTemplate;
}
@Value("${rabbitMQ.exchangeName}")
private String exchangeName;
@Value("${rabbitMQ.routingKeyName-1}")
private String routingKeyName1;
@Value("${rabbitMQ.routingKeyName-2}")
private String routingKeyName2;
public void sendMessage_1(String msg){
// 向名路由键routingKeyName1绑定的队列转化并发送消息msg
// 第一个参数为交换器名称,第二个参数为路由键名称,第三个为发送的参数
amqpTemplate.convertAndSend(this.exchangeName,this.routingKeyName1,msg);
}
public void sendMessage_2(String msg){
// 向名路由键routingKeyName2绑定的队列转化并发送消息msg
// 第一个参数为交换器名称,第二个参数为路由键名称,第三个为发送的参数
amqpTemplate.convertAndSend(this.exchangeName,this.routingKeyName2,msg);
}
}
控制层:
package cn.wu.controller;
import cn.wu.provider.Sender;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class TestController {
Sender sender;
@Autowired
@Qualifier("senderBean")
public void setSender(Sender sender) {
this.sender = sender;
}
@GetMapping("/test/{receive}/{msg}")
public String test(@PathVariable("receive") Integer receive ,@PathVariable("msg") String msg){
if(receive == 1){
sender.sendMessage_1(msg);
}else if(receive == 2){
sender.sendMessage_2(msg);
}
return "";
}
}
结果(分别访问 http://localhost:11111/test/1/你好,世界!,分别访问http://localhost:11111/test/2/你好,世界!):
大致结构:
Fanout方式
以前面代码作为基础
不需要改变application.yml文件
修改消息消费者类@RabbitListener注解:
@RabbitListener(bindings = {@QueueBinding(
value = @Queue(value = "${rabbitMQ.queueName-1}", durable = "true"), // 设置绑定的队列名称,持久存储
exchange = @Exchange(name = "${rabbitMQ.exchangeName}", // 交换器的名称
type = ExchangeTypes.FANOUT) // 交换器执行的策略: 广播形式
// 此时是不需要绑定routing key的…
)}
)
修改消息生产者类:
public void sendMessage_1(String msg){
// 向名路由键routingKeyName1绑定的队列转化并发送消息msg
// 第一个参数为交换器名称,第二个参数为路由键名称,第三个为发送的参数
// 由于是广播的形式,所以路由键可以不需要再指定
amqpTemplate.convertAndSend(this.exchangeName,"",msg);
}
public void sendMessage_2(String msg){
// 向名路由键routingKeyName2绑定的队列转化并发送消息msg
// 第一个参数为交换器名称,第二个参数为路由键名称,第三个为发送的参数
// 由于是广播的形式,所以路由键可以不需要再指定
amqpTemplate.convertAndSend(this.exchangeName,"",msg);
}
不管访问哪一个URL,,两个消息接收者都会接收到:
Topic方式
- * 代表只匹配单个字符
- # 代表匹配至少一个字符
- 路由键以符号 . 来连接
基本结构:
修改applcation.yml:
rabbitMQ:
queueName-1: queue-1 # 队列1的名称
queueName-2: queue-2 # 队列2的名称
exchangeName: exchangeName # 交换器名称
routingKeyName-1: route.students.1 # 路由键1的名称 用于绑定队列1 发送者根据该路由键来选择队列
routingKeyName-2: route.books.a # 路由键2的名称 用于绑定队列2 发送者根据该路由键来选择队列
修改消息生产者:
package cn.wu.provider;
import org.springframework.amqp.core.AmqpTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
@Component("senderBean")
public class Sender {
private AmqpTemplate amqpTemplate;
@Autowired
public void setAmqpTemplate(AmqpTemplate amqpTemplate) {
this.amqpTemplate = amqpTemplate;
}
@Value("${rabbitMQ.exchangeName}")
private String exchangeName;
@Value("${rabbitMQ.routingKeyName-1}")
private String routingKeyName1;
@Value("${rabbitMQ.routingKeyName-2}")
private String routingKeyName2;
public void sendMessage_1(String msg){
// 向名路由键routingKeyName1绑定的队列转化并发送消息msg
// 第一个参数为交换器名称,第二个参数为路由键名称,第三个为发送的参数
// 由于是广播的形式,所以路由键可以不需要再指定
amqpTemplate.convertAndSend(this.exchangeName,this.routingKeyName1,msg);
}
public void sendMessage_2(String msg){
// 向名路由键routingKeyName2绑定的队列转化并发送消息msg
// 第一个参数为交换器名称,第二个参数为路由键名称,第三个为发送的参数
// 由于是广播的形式,所以路由键可以不需要再指定
amqpTemplate.convertAndSend(this.exchangeName,this.routingKeyName2,msg);
}
}
修改消息消费者:
package cn.wu.consumer;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.ExchangeTypes;
import org.springframework.amqp.rabbit.annotation.*;
import org.springframework.stereotype.Component;
@Component
@RabbitListener(bindings = {@QueueBinding(
value = @Queue(value = "${rabbitMQ.queueName-1}", autoDelete = "true"), // 设置绑定的队列名称,临时连接
exchange = @Exchange(name = "${rabbitMQ.exchangeName}", // 交换器的名称
type = ExchangeTypes.TOPIC), // 交换器执行的策略: 广播形式
key = "#.1" // 匹配所有以-1结尾的所有路由键
)}
)
@Slf4j
public class ReceiverOne {
// 接收消息并处理
@RabbitHandler // 配合@RabbitListener使用,根据接受的参数类型进入具体的方法
public void handler(String msg){
// 处理业务流程
// 事件处理
log.info("消息消费者I 接收到的消息为: "+msg);
}
}
package cn.wu.consumer;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.ExchangeTypes;
import org.springframework.amqp.rabbit.annotation.*;
import org.springframework.stereotype.Component;
@Component
@Slf4j
@RabbitListener(bindings = {@QueueBinding(
value = @Queue(name = "${rabbitMQ.queueName-2}", autoDelete = "true"), // 设置绑定的队列名称,以及作为可删除的临时队列
exchange = @Exchange(name = "${rabbitMQ.exchangeName}", // 交换器的名称
type = ExchangeTypes.TOPIC), // 交换器执行的策略
key = "#.a" // 匹配所有以.a结尾的所有路由键
)}
)
public class ReceiverTwo {
// 接收消息并处理
@RabbitHandler
public void handler(String msg){
// 处理业务流程
log.info("消息消费者II 接收到的消息为: "+msg);
}
}
package cn.wu.consumer;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.ExchangeTypes;
import org.springframework.amqp.rabbit.annotation.*;
import org.springframework.stereotype.Component;
import java.util.concurrent.atomic.AtomicInteger;
@Component
@RabbitListener(bindings = {@QueueBinding(
value = @Queue(value = "${rabbitMQ.queueName-1}", autoDelete = "true"), // 设置绑定的队列名称,临时连接
exchange = @Exchange(name = "${rabbitMQ.exchangeName}", // 交换器的名称
type = ExchangeTypes.TOPIC), // 交换器执行的策略: 广播形式
key = "route.#" // 匹配所有以route.开头的路由键
)}
)
@Slf4j
public class ReceiverThree {
AtomicInteger count = new AtomicInteger(0);
@RabbitHandler
public void handler(String msg){
count.incrementAndGet();
log.info("消息接收者III第"+count.get()+"次接收到的消息为: "+msg);
}
}
修改控制层:
package cn.wu.controller;
import cn.wu.provider.Sender;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class TestController {
Sender sender;
@Autowired
@Qualifier("senderBean")
public void setSender(Sender sender) {
this.sender = sender;
}
@GetMapping("/test/{receive}/{msg}")
public String test(@PathVariable("receive") Integer receive ,@PathVariable("msg") String msg){
if(receive == 1){
sender.sendMessage_1(msg);
}else if(receive == 2){
sender.sendMessage_2(msg);
}
return "";
}
}
多次点击相同URL,多次生产消息,此时,由于第一个消息消费者和第三个消息消费者共用一个队列(第三个消息消费者也和第二个消费者共用同一个队列),默认轮流获取队列元素,因此最终结果为: