概念
Message Queue : 消息队列,接收并存储消息,再转发。
三个作用
1.流量消峰填谷
使用消息队列做缓冲,减少突然访问的压力
2.解耦
如订单系统和库存系统之间不在互相影响,独立运行,达到了应用解耦的目的;
即RabbitMQ多用于分布式系统之间的通信
3.异步处理
在项目中,可将一些无需即时返回且耗时的操作提取出来,进行异步处理,而这种异步处理的方式大大的节省了服务器的请求响应时间,从而提高了系统的吞吐量。
劣势:
- 系统可用性降低:系统引入的外部依赖越多,系统稳定性越差
- 系统复杂度提高
- 需保证消息数据处理的一致性问题
流程图解
各名词解释:
Broker:接收和分发消息的应用, 消息处理中心:负责消息存储、确认、重试等,一般其中会包含多个 queue
Connection:publisher/consumer 和 broker 之间的 TCP 连接
Channel:如果每一次访问 RabbitMQ 都建立一个 Connection,在消息量大的时候建立 TCP Connection 的开销将是巨大的,效率也较低。Channel 是在 connection 内部建立的逻辑连接,如果应用程序支持多线程,通常每个 thread 创建单独的 channel 进行通讯,AMQP method 包含了 channel id 帮助客户端和 message broker 识channel,所以 channel 之间是完全隔离的。Channel 作为轻量级的Connection 极大减少了操作系统建立 TCP connection 的开销 Exchange:message 到达 broker 的第一站,根据分发规则,匹配查询表中的 routing key,分发消息到 queue 中去。
常用的类型有:direct (point-to-point), topic (publish-subscribe) and fanout (multicast) Queue:消息最终被送到这里等待 consumer 取走
Binding:exchange 和 queue 之间的虚拟连接,binding 中可以包含 routing key,Binding 信息被保存到 exchange 中的查询表中,用于 message 的分发依据
Routing Key:路由关键字,exchange根据这个关键字进行消息投递;
四种mq对比
RoutingKey与BindingKey的区别:
RoutingKey:路由键。生产者将消息发给交换器的时候,一般会指定一个RoutingKey用来指明这个消息的路由规则,这个RoutingKey需要与交换器类型和绑定键(BindingKey)联合使用才能最终生效。在交换器类型和绑定键(BindingKey)固定的情况下,生产者可以在发送消息给交换器时,通过指定RoutingKey来决定消息流向哪里。
BindingKey:RabbitMQ中通过绑定将交换器与队列关联起来,在绑定的时候一般会指定一个绑定键(BindingKey),这样RabbitMQ就知道如何正确地将消息路由到队列了。
生产者将消息发送给交换器时,需要一个RoutingKey,当BindingKey和RoutingKey相匹配时,消息会被路由到对应的队列中。在绑定多个队列到同一个交换器的时候,这些绑定允许使用相同的BindingKey。
BindingKey并不是在所有的情况下都生效,它依赖于交换器类型,比如fanout类型的交换器就会无视BindingKey,而是将消息路由到所有绑定到该交换器的队列中。
BindingKey其实也属于路由键中的一种,官方解释为:the routing key to use fot the binding。可以翻译为:在绑定的时候使用的路由键。大多数时候,包括官方文档和RabbitMQ Java API中都把BindingKey和RoutingKey看作RoutingKey,
为了避免混淆,可以这么理解:
在使用绑定的时候,其中需要的路由键是BindingKey。 涉及的客户端方法为:channel.exchangeBind、channel.queueBind 对应的AMQP命令为:Exchange.Bind、Queue.Bind。
在发送消息的时候,其中需要的路由键是RoutingKey。 涉及的客户端方法为:channel.basicPublish 对应的AMQP命令为:Basic.Publish。
大多数情况下习惯性地将BindingKey写成RoutingKey,尤其是在使用direct类型的交换器的时候。
工作模式
1、简单模式
一个队列一个消费者
配置类RabbitmqConfig:
@Bean
public Queue simpleQueue(){
return QueueBuilder.durable("simpleQueue").build();
}
生产者类Producter:
@Autowired
private RabbitTemplate rabbitTemplate;
/**
* 往队列中发消息
*/
public void send(String msg){
System.out.println("生产者发消息:"+msg);
rabbitTemplate.convertAndSend("simpleQueue",msg);
}
消费者类Consumer:
/**
* @RabbitListener(queues = "simpleQueue"): 监听指定的队列
*/
@RabbitListener(queues = "simpleQueue")
public void receive(String msg){
System.out.println("消费者接收到消息:"+msg);
}
2、work工作模式
一个队列多个消费者, c1和c2共同监听一个消息队列,但是一个消息只能被一个消费者消费。
配置类RabbitmqConfig:
@Bean
public Queue workQueue(){
return QueueBuilder.durable("workQueue").build();
}
生产者类Producter:
@Autowired
private RabbitTemplate rabbitTemplate;
public void send(String msg){
System.out.println("生产者发消息:"+msg);
rabbitTemplate.convertAndSend("workQueue",msg);
}
消费者类Consumer:
@RabbitListener(queues = "workQueue")
public void receiveA(String msg){
System.out.println("消费者接收到消息:"+msg);
}
@RabbitListener(queues = "workQueue")
public void receiveB(String msg){
System.out.println("消费者接收到消息:"+msg);
}
3、发布订阅模式(三种)
生产者将消息不是直接发送到队列,而是发送到交换机。
exchange交换机功能:
- 接收生产者发送的消息。
- 知道如何处理消息,例如递交给某个特定队列、所有队列、或是将消息丢弃。到底如何操作,取决于Exchange的类型。
注意:exchange交换机只负责转发消息,不具备存储消息的能力,因此如果没有任何队列与Exchange绑定,或者没有符合路由规则的队列,那么消息会丢失!
3.1、Fanout(广播模式):
将消息交给所有绑定到交换机的队列
配置类RabbitmqConfig:
创建两个队列和一个交换机、并绑定
/**
* 发布订阅模式
* 创建两个队列
* @return
*/
@Bean
public Queue fanoutQueueA(){
return QueueBuilder.durable("fanoutQueueA").build();
}
@Bean
public Queue fanoutQueueB(){
return QueueBuilder.durable("fanoutQueueB").build();
}
/**
* 创建交换机
*/
@Bean
public FanoutExchange fanoutExchange(){
return new FanoutExchange("fanoutExchange");
}
/**
* 把两个队列绑定到交换机上
*/
@Bean
public Binding fanoutQueueAToFanoutExchange(Queue fanoutQueueA,FanoutExchange fanoutExchange){
return BindingBuilder.bind(fanoutQueueA).to(fanoutExchange);
}
@Bean
public Binding fanoutQueueBToFanoutExchange(Queue fanoutQueueB,FanoutExchange fanoutExchange){
return BindingBuilder.bind(fanoutQueueB).to(fanoutExchange);
}
生产者类Producter:
@Autowired
private RabbitTemplate rabbitTemplate;
public void send(String msg){
System.out.println("生产者发消息:"+msg);
rabbitTemplate.convertAndSend("fanoutExchange","",msg);
}
消费者类Consumer:
@RabbitListener(queues = "fanoutQueueA")
public void receiveA(String msg){
System.out.println("消费者A接收到消息:"+msg);
}
@RabbitListener(queues = "fanoutQueueB")
public void receiveB(String msg){
System.out.println("消费者B接收到消息:"+msg);
}
3.2、direct(Routing路由模式)
将消息交给绑定到交换机的特定队列
特点(三点):
1、队列与交换机不是任意绑定,而是要指定一个RoutingKey
(路由key);
2、消息的发送方在向Exchange发送消息时,也必须指定消息的 `RoutingKey;
3、Exchange不再把消息交给每一个绑定的队列,而是根据消息的Routing Key
进行判断,只有队列的Routingkey
与消息的 Routing key
完全一致,才会接收到消息。
配置类RabbitmqConfig:
//创建两个队列
@Bean
public Queue directQueueA() {
return QueueBuilder.durable("directQueueA").build();
}
@Bean
public Queue directQueueB() {
return QueueBuilder.durable("directQueueB").build();
}
//创建一个交换机
@Bean
public DirectExchange directExchange() {
return new DirectExchange("DirectExchange");
}
//将两个队列与交换机绑定,指定RoutingKey
@Bean
public Binding directQueueAToDirectExchange(Queue directQueueA, DirectExchange directExchange) {
return BindingBuilder.bind(directQueueA).to(directExchange).with("red");
}
@Bean
public Binding directQueueBToDirectExchange(Queue directQueueB, DirectExchange directExchange) {
return BindingBuilder.bind(directQueueB).to(directExchange).with("green");
}
生产者类Producter:
@Autowired
private RabbitTemplate rabbitTemplate;
public void send(String msg,String routeKey){
System.out.println("生产者发送消息:"+msg);
rabbitTemplate.convertAndSend("DirectExchange",routeKey,msg);
}
消费者类Consumer:
@RabbitListener(queues = "directQueueA")
public void receiveA(String msg) {
System.out.println("消费者A接收到的消息:" + msg);
}
@RabbitListener(queues = "directQueueB")
public void receiveB(String msg) {
System.out.println("消费者B接收到的消息:" + msg);
}
3.3、Topics通配符模式/ 主题模式(模糊路由)
Topic
类型与Direct
相比,都是可以根据RoutingKey
把消息路由到不同的队列。只不过Topic
类型Exchange
可以让队列在绑定Routing key
的时候使用通配符!Routingkey
一般都是有一个或多个单词组成,多个单词之间以”.”分割,例如: item.insert
通配符规则:
#
:匹配0个或多个词
*
:匹配只有1个词
举例:
item.#
:能够匹配item.insert.abc
或者 item.insert
item.*
:只能匹配item.insert
消息确认
yml配置:
publisher-confirm-type: correlated #开启交换机消息确认
publisher-returns: true #开启队列消息确认
listener:
simple:
acknowledge-mode: manual #开启消费者方,手动确认
prefetch: 1 #消费者一次消费队列中的一个消息,默认值:250个, 作用:可以限流,分批消费
发送消息确认机制:
发送到交换机、传输到队列
1)为确保消息是否发送到交换机,设置发布时确认,确认消息是否到达 Broker 服务器;
2)为确保消息是否传输到队列,设置回调函数,此方法只有在消息发送失败时才会执行。
CallbackConfig
继承RabbitTemplate.ConfirmCallback, RabbitTemplate.ReturnsCallback并重写方法
@Slf4j
@Configuration
public class CallbackConfig implements RabbitTemplate.ConfirmCallback, RabbitTemplate.ReturnCallback {
@Autowired
private RabbitTemplate rabbitTemplate;
@PostConstruct
public void init() {
rabbitTemplate.setConfirmCallback(this);
rabbitTemplate.setReturnCallback(this);
rabbitTemplate.setMandatory(true);
}
/**
* 把相关的内容放入mysql:
* 1、交换机名称
* 2、routingKey
* 3、消息
*/
@Override
public void confirm(CorrelationData correlationData, boolean ack, String cause) {
if (ack) {
return;
}
//交换机名称
MessageProperties messageProperties = correlationData.getReturnedMessage().getMessageProperties();
String exchange = messageProperties.getReceivedExchange();
//routingKey
String routingKey = messageProperties.getReceivedRoutingKey();
//消息
byte[] msgs = correlationData.getReturnedMessage().getBody();
//把交换机名称、routingKey、消息进行持久化(放入mysql);
// 正常对这类消息的处理:定时扫描,并把消息重新发到mq;
//如果定时处理出现问题,保底人工处理
log.error("confirm error info:{}, exchange:{},routingKey:{},message:{}",
cause, exchange, routingKey, msgs);
}
* // 在这个方法中,您可以处理未能被路由到队列的消息
* // message:无法被路由的消息
* // replyCode:响应码,用于指示失败的原因
* // replyText:响应文本,提供了更详细的错误信息
* // exchange:消息发送时指定的交换机
* // routingKey:消息发送时指定的路由键
@Override
public void returnedMessage(Message message, intreplyCode, String replyText, String exchange, String routingKey) {
//把交换机名称、routingKey、消息进行持久化(放入mysql);
// 正常对这类消息的处理:定时扫描,并把消息重新发到mq;
//如果定时处理出现问题,保底人工处理
log.error("confirm error info:{}, exchange:{},routingKey:{},message:{}",
replyText, exchange, routingKey, message.getBody());
}
}
配置类RabbitmqConfig:
* 验证消息确认队列
*/
@Bean
public Queue confirmQueue() {
return QueueBuilder.durable("confirmQueue").build();
}
@Bean
public DirectExchange confirmExchange() {
return new DirectExchange("confirmExchange");
}
@Bean
public Binding confirmQueueToConfirmExchange(Queue confirmQueue, DirectExchange confirmExchange) {
return BindingBuilder.bind(confirmQueue).to(confirmExchange).with("confirm");
}
消费者消息确认机制
消费者类Consumer:
@RabbitListener(queues = "confirmQueue")
public void receive(String msg, Message message, Channel channel) throws IOException {
try {
//1.拿到信息
System.out.println("消费者接收 信息:" + msg);
String s = new String(message.getBody());
//确认消息
//arg1: 消息Id
//arg2: 是否批量确认
//channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
} catch (Exception e) {
e.printStackTrace();
//args3:消息是否重新放回队列,一般是false
//没有成功被消费者消费的消息,又没有放回原队列,称为死信
channel.basicNack(message.getMessageProperties().getDeliveryTag(), false,false);
}
}
基于插件的延迟队列
配置类RabbitmqConfig:
/**
* 定义延迟队列
*/
@Bean
public Queue delayQueue() {
return QueueBuilder.durable("delayQueue").build();
}
/**
* customExchange:自定义交换机,其实就是fanout,direct,topic其中一种
*
* @return
*/
@Bean
public CustomExchange customExchange() {
Map map = new HashMap();
//指定交换机类型
map.put("x-delayed-type", "direct");
/**
* arg1:交换机名字
* arg2: 指定交换机里的信息类型,延迟消息
* arg3: 是否持久化,是否将没有被消费的消息持久化
* arg4: 没有队列绑定到交换机,交换机是否删除。
* arg5: 初始化参数
*/
return new CustomExchange("customExchange", "x-delayed-message", true, false, map);
}
@Bean
public Binding delayQueueToCustomExchange(Queue delayQueue, CustomExchange customExchange) {
return BindingBuilder.bind(delayQueue).to(customExchange).with("delay").noargs();
}
生产者类Producter:
@Autowired
private RabbitTemplate rabbitTemplate;
public void send(String msg, int delayTime) {
log.info("生产者发送消息:" + msg);
rabbitTemplate.convertAndSend("customExchange", "delay", msg,
new MessagePostProcessor() {
/**
* 在消息发送到消息队列之前对消息进行处理
* @param message
* @return
* @throws AmqpException
*/
@Override
public Message postProcessMessage(Message message) throws AmqpException {
// 给消息设置过期时间,单位是毫秒
message.getMessageProperties().setDelay(delayTime);
return message;
}
});
}
消费者类Consumer:
@RabbitListener(queues = "delayQueue")
public void receive(String msg, Channel channel, Message message) {
try {
//1.拿到消息
log.info("消费者接收到消息:" + msg);
//2.做业务
//3.确认消息
channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
} catch (Exception e) {
try {
channel.basicNack(message.getMessageProperties().getDeliveryTag(), false, false);
} catch (Exception ex) {
ex.printStackTrace();
}
}
}
死信队列
队列消息变成死信(deadmessage)之后,它能被重新被发送到另一个交换器中
,这个交换器就是DLX,全称为Dead-Letter-Exchange,绑定DLX的队列就称之为死信队列。
消息变成死信的几种情况:
1、消息被拒绝(basic.reject/ basic.nack)并且requeue=false
2、消息TTL过期
3、队列达到最大长度