消息中间件 rabbitMq
一. RabbitMQ介绍
RabbitMQ是实现了高级消息队列协议(AMQP)的开源消息代理软件(亦称面向消息的中间件)。RabbitMQ服务器是用Erlang语言编写的,而集群和故障转移是构建在开放电信平台框架上的。RabbitMQ是一种消息中间件,用于处理来自客户端的异步消息。服务端将要发送的消息放入到队列池中。接收端可以根据RabbitMQ配置的转发机制接收服务端发来的消息。RabbitMQ依据指定的转发规则进行消息的转发、缓冲和持久化操作,主要用在多服务器间或单服务器的子系统间进行通信,是分布式系统标准的配置。
1. RabbitMQ如何工作的?
首先来看看RabbitMQ里的几个重要概念:
- 交换机(Exchange):交换机负责从生产者(消息发送者)那里接收消息,并根据交换类型分发到对应的消息列队里。要实现消息的接收,一个队列必须到绑定一个交换机
- 队列(Queue):存储消息的缓存
- 路由键(Routing Key):路由键是交换机绑定列队的一个键,路由键可以说是消息的目的地址。一个交换机可以绑定多个队列,意味着他会有多个路由键,交换机会根据发送消息时的路由键分配到匹配的队列中
- 绑定(Binding):绑定是队列和交换机的一个关联连接,绑定时需要指定路由键
- 生产者(Producer):发送消息的应用
- 消费者(Consumer):接收消息的应用
- 消息(Message):由生产者通过RabbitMQ发送给消费者的信息
- 连接(Connection):连接RabbitMQ和应用服务器的TCP连接
- 通道(Channel):连接里的一个虚拟通道。当你通过消息队列发送或者接收消息时,这个操作都是通过通道进行的
- 虚拟主机(Virtual Host):虚拟主机是提供逻辑分组和资源分离。物理资源的分离不是虚拟主机的目标,应将其视为实现细节。 每个虚拟主机下可以有多个交换机和队列
1.1 生产者发送消息流程
- 生产者连接到 RabbitMQ Broker,建立一个连接(Connection),开启一个信道(Channel)
- 生产者声明一个交换器,并设置相关属性,比如交换机类型、是否持久化等
- 生产者声明一个队列并设置相关属性,比如是否排他、是否持久化、是否自动删除等
- 生产者通过路由键将交换器和队列绑定起来
- 生产者发送消息至 RabbitMQ Broker,其中包含路由键、交换器等信息
- 相应的交换器根据收到的路由键查找相匹配的队列
- 如果找到,则将从生产者发送过来的消息存入相应的队列中
- 如果没有找到,则根据生产者配置的属性选择丢弃还是回退给生产者
- 关闭信道
- 关闭连接
1.2 消费者接收消息的过程
- 消费者连接到 RabbitMQ Broker,建立一个连接(Connection),开启一个信道(Channel)
- 消费者向 RabbitMQ Broker 请求消费相应队列中的消息,可能会设置相应的回调函数,以及做一些准备工作
- 等待 RabbitMQ Broker 回应并投递相应队列中的消息,消费者接收消息
- 消费者确认(ack)接收到的消息
- RabbitMQ 从队列中删除相应已经被确认的消息
- 关闭信道
- 关闭连接
2. Exchange(交换机)路由规则
2.1 Direct (完全匹配模式)
Direct模式下Exchange将生产者投递消息中所携带的Router Key和Queue中的Router Key进行比较,如果完全匹配,就会将这条消息投递到匹配的Queue中
2.2 Fanout (广播模式)
Fanout模式下会忽略Routing Key,当Exchange收到生产者投递的消息后,会把消息投递到与自己绑定的所有Queue中
2.3 Topic (正则匹配模式)
Topic模式下会匹配Routing Key,Exchage会根据 # 和 * 进行Routing Key的匹配
# 表示0个或多个关键词, * 表示1个关键词。(关键词非单个字母)
2.4 Headers
这也是忽略路由键的一种路由方式。路由器和交换机路由的规则是通过Headers信息来交换的,类似HTTP的Headers。它不依赖与bindingKey和routingKey,而是在绑定队列与交换器的时候指定一个键值对;当交换器在分发消息的时候会先解开消息体里的headers数据,然后判断里面是否有所设置的键值对,如果发现匹配成功,才将消息分发到队列中;这种交换器类型在性能上相对来说较差,在实际工作中很少会用到
2.5 docker 安装 rabbitMq
docker run -d --name rabbitmq -p 5671:5671 -p 5672:5672 -p 4369:4369 -p 25672:25672 -p 15671:15671 -p 15672:15672 rabbitmq:management
等待安装完成,浏览器访问 http://127.0.0.1:15672/
用户名密码都是 guest
二. SpringBoot 整合 RabbitMQ
1. 导入maven
<!-- rabbitmq -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
2. application.yml
rabbitmq:
host: 127.0.0.1
port: 5672
# 指定虚拟主机
virtual-host: /
username: guest
password: guest
3. RabbitMQ配置类
@Configuration
public class MyRabbitConfig {
/**
* 配置消息转换器
*/
@Bean
public MessageConverter messageConverter(){
return new Jackson2JsonMessageConverter();
}
/**
* Topic模式的 交换机
* 如果交换机不存在则创建
* 如果存在 就不进行操作
*/
@Bean
public Exchange firstExchange(){
boolean isDurable = true; // 是否开启持久化
boolean autoDelete = true; // 是否自动删除 (没有绑定队列 并且没有生产者连接时)
return new TopicExchange("firstExchange", isDurable, autoDelete);
}
/**
* 队列 1
* 如果队列不存在则创建
* 如果存在 就不进行操作
*/
@Bean
public Queue firstQueue(){
boolean isDurable = true; // 是否开启持久化
boolean exclusive = false; // 只被一个连接(connection)使用,而且当连接关闭后队列即被删除
boolean autoDelete = false; // 是否自动删除 (没有绑定队列 并且没有生产者连接时)
return new Queue("firstQueue", isDurable, exclusive, autoDelete);
}
/**
* 队列 2
* 如果队列不存在则创建
* 如果存在 就不进行操作
*/
@Bean
public Queue secondQueue(){
boolean isDurable = true; // 是否开启持久化
boolean exclusive = false; // 只被一个连接(connection)使用,而且当连接关闭后队列即被删除
boolean autoDelete = false; // 是否自动删除 (没有绑定队列 并且没有生产者连接时)
return new Queue("secondQueue", isDurable, exclusive, autoDelete);
}
/**
* 交换机 与 1号队列绑定
*/
@Bean
public Binding firstBinding(){
String queueName = "firstQueue"; // 绑定队列的名字
Binding.DestinationType bindingType = Binding.DestinationType.QUEUE; // 绑定类型 交换机不仅可以和队列绑定,也可以和交换机绑定。所以建立绑定关系时需要指定绑定类型
String exchangeName = "firstExchange"; // 绑定交换机的名字
String routingKey = "firstQueue"; // 路由键的值
return new Binding(queueName, bindingType, exchangeName,routingKey, null);
}
/**
* 交换机 与 2号队列绑定
*/
@Bean
public Binding secondBinding(){
String queueName = "secondQueue"; // 绑定队列的名字
Binding.DestinationType bindingType = Binding.DestinationType.QUEUE; // 绑定类型 交换机不仅可以和队列绑定,也可以和交换机绑定。所以建立绑定关系时需要指定绑定类型
String exchangeName = "firstExchange"; // 绑定交换机的名字
String routingKey = "secondQueue"; // 路由键的值
return new Binding(queueName, bindingType, exchangeName,routingKey, null);
}
}
4. 消费者监听消息
@Component
@RabbitListener(queues = "firstQueue") // 监听 1号队列消息
public class FirstQueueReceiver {
@RabbitHandler
public void process(Object message) {
System.out.println("接收者 FirstQueueReceiver," + message.toString());
}
}
5. 生产发送消息
@RestController
@RequestMapping("/")
public class RabbitMqController {
@Autowired
private RabbitTemplate rabbitTemplate;
@GetMapping("/sendMQ")
public String sendMQ(@RequestParam(value = "num", required = false, defaultValue = "10") Integer num){
HashMap<Object, Object> dataMap = new HashMap<>();
dataMap.put("code",200);
dataMap.put("msg","success");
String exchangeName = "firstExchange";
String routeKey = "firstQueue";
rabbitTemplate.convertAndSend(exchangeName, routeKey, dataMap, new CorrelationData(UUID.randomUUID().toString().replace("-","")));
return "发送成功";
}
}
三. 消息确认机制
为什么要有消息确认
- 由于网络可能以不可预知的方式出现故障,且检测故障可能需要耗费一些时间
- 因此不能保证发送的消息能够到达对等方或由它成功地处理。
消息确认流程图:
1. 生产者确认
由于:
- 生产者向 RabbitMQ Server 发出的消息可能会在发送途中丢失或者需要经过一定的延迟后才能成功发送到 RabbitMQ Server
- 因此,需要 RabbitMQ 告诉生产者,生产者才能知道自己发布的消息是否已经送达
在编码时我们可以用两个选项用来控制消息投递的可靠性:
- 消息从 producer 到 RabbitMQ broker cluster
成功,则会返回一个 confirmCallback
; - 消息从 exchange 到 queue 投递
失败,则会返回一个 returnCallback
需要开启配置:
rabbitmq:
host: 192.168.31.11
port: 5672
# 指定虚拟主机
virtual-host: /
username: guest
password: guest
# 开启发送确认
publisher-confirms: true
# 开启发送失败退回(消息有没有找到合适的队列)
publisher-returns: true
RabbitMq配置类
@Slf4j
@Configuration
public class MyRabbitConfig {
@Autowired
CachingConnectionFactory connectionFactory;
@Bean
RabbitTemplate rabbitTemplate() {
//若使用confirm-callback ,必须要配置publisherConfirms 为true
connectionFactory.setPublisherConfirms(true);
//若使用return-callback,必须要配置publisherReturns为true
connectionFactory.setPublisherReturns(true);
RabbitTemplate rabbitTemplate = new RabbitTemplate(connectionFactory);
/**
* 设置确认回调
* correlationData: 消息的唯一id
* ack: 消息是否成功收到
* cause:失败的原因
*/
rabbitTemplate.setConfirmCallback((correlationData, ack , cause) -> {
System.out.println("收到消息: " + correlationData.getId() + "ack: " + ack + "cause: " + cause);
});
/**
* 设置消息抵达队列回调:可以很明确的知道那些消息失败了
* message: 投递失败的消息详细信息
* replyCode: 回复的状态码
* replyText: 回复的文本内容
* exchange: 当时这个发送给那个交换机
* routerKey: 当时这个消息用那e个路由键
*/
rabbitTemplate.setReturnsCallback((res) -> {
System.out.println("发送失败: [" + res.getMessage() + "]" + "replyCode: " + res.getReplyCode() + "replyText:" + res.getReplyText() + "exchange:" + res.getExchange() + "routerKey:" + res.getRoutingKey());
});
return rabbitTemplate;
}
}
2. 消费者确认
ACK确认模式
确认模式有三种:
- AcknowledgeMode.NONE:不确认
默认情况下消息消费者是NONE模式,默认所有消息消费成功,会不断的向消费者推送消息。
因为rabbitMq认为所有消息都被消费成功,所以队列中不在存有消息,消息存在丢失的危险 - AcknowledgeMode.AUTO:自动确认
在自动确认模式下,消息发送后即被认为成功投递,不管消费者端是否成功处理本次投递 - AcknowledgeMode.MANUAL:手动确认
消费者收到消息后,手动调用basic.ack/basic.nack/basic.reject后,RabbitMQ收到这些消息后,才认为本次投递成功
手动确认模式可以使用 prefetch,限制通道上未完成的(“正在进行中的”)发送的数量
配置开启 ack动确认模式
rabbitmq:
host: 192.168.31.11
port: 5672
# 指定虚拟主机
virtual-host: /
username: guest
password: guest
# 开启手ack动确认模式
listener:
simple:
acknowledge-mode: manual
确认机制:
@Component
@RabbitListener(queues = "firstQueue")
public class FirstQueueReceiver {
@RabbitHandler
public void process(Object data, Channel channel, Message message) {
System.out.println("接收者 FirstQueueReceiver," + data.toString());
try {
long deliveryTag = message.getMessageProperties().getDeliveryTag(); // 表示消息投递序号,每次消费消息或者消息重新投递后,deliveryTag都会增加。手动消息确认模式下,我们可以对指定deliveryTag的消息进行ack、nack、reject等操作
boolean multiple = false; // 是否批量确认,值为 true 则会一次性 ack所有小于当前消息 deliveryTag 的消息。 假设发送三条消息deliveryTag分别是5、6、7,可它们都没有被确认,当我发第四条消息此时deliveryTag为8,multiple设置为 true,会将5、6、7、8的消息全部进行确认
channel.basicAck(deliveryTag, multiple); // basicAck:表示成功确认,使用此回执方法后,消息会被rabbitmq broker 删除
// void basicNack(long deliveryTag, boolean multiple, boolean requeue)
// basicNack :表示失败确认,一般在消费消息业务异常时用到此方法,可以将消息重新投递入队列
// deliveryTag:表示消息投递序号。
// multiple:是否批量确认。
// requeue:值为 true 消息将重新入队列
} catch (IOException e) {
e.printStackTrace();
}
}
}
四. 死信队列
死信队列 听上去像 消息“死”了 其实也有点这个意思,死信队列 是 当消息在一个队列 因为下列原因
- 消息被拒绝(basic.reject/ basic.nack)并且不再重新投递 requeue=false
- 消息超期 (rabbitmq Time-To-Live -> messageProperties.setExpiration())
- 队列超载
- DLX也是一个正常的Exchange,和一般的Exchange没有区别,它能在任何的队列上被指定,实际上就是设置某个队列的属性。
- 当这个队列中有死信时,RabbitMQ就会自动的将这个消息重新发布到设置的Exchange上去,进而被路由到另一个队列。
- 可以监听这个队列中的消息做相应的处理。
1. 消费者
在消费者RabbitMQ配置类中,新加三个Bean
/**
* 死信交换机
* 收到的消息 会推送给死信队列
*/
@Bean
public Exchange delayExchange(){
boolean isDurable = true; // 是否开启持久化
boolean autoDelete = false; // 是否自动删除
return new TopicExchange("delayExchange", isDurable, autoDelete);
}
/**
* 死信队列
*/
@Bean
public Queue delayQueue(){
Map<String, Object> args = new HashMap<>();
// 如果 超过这个时间没有消费这条消息,就变成死信
args.put("x-message-ttl", 60000);
// 如果信死了 就转发给 哪个交换机处理
args.put("x-dead-letter-exchange", "firstExchange");
// 转发时的路由键
args.put("x-dead-letter-routing-key","secondQueue");
return new Queue("delayQueue", true, false, false, args);
}
/**
* 死信交换机 绑定 死信队列
*/
@Bean
public Binding delayBinding(){
String queueName = "delayQueue"; // 绑定队列的名字
Binding.DestinationType bindingType = Binding.DestinationType.QUEUE; // 绑定类型 交换机不仅可以和队列绑定,也可以和交换机绑定。所以建立绑定关系时需要指定绑定类型
String exchangeName = "delayExchange"; // 绑定交换机的名字
String routingKey = "delayQueue"; // 路由键的值
return new Binding(queueName, bindingType, exchangeName,routingKey, null);
}
监听二号队列的消息
@Component
@RabbitListener(queues = "secondQueue")
public class secondQueueReceiver {
@RabbitHandler
public void process(Object message) {
System.out.println("收到死信队列:" + message.toString());
}
}
2. 生产者
@GetMapping("/sendDeadMQ")
public String sendDeadMQ(){
HashMap<Object, Object> dataMap = new HashMap<>();
dataMap.put("code",200);
dataMap.put("msg","success");
String exchangeName = "delayExchange";
String routeKey = "delayQueue";
rabbitTemplate.convertAndSend(exchangeName, routeKey, dataMap, new CorrelationData(UUID.randomUUID().toString().replace("-","")));
return "发送成功";
}