RabbitMQ
MQ 的基本概念
MQ全称 Message Queue(消息队列),是在消息的传输过程中保存消息的容器。多用于分布式系统之间进
行通信。
MQ 的优势和劣势
优势: 应用解耦 、异步提速 、削峰填谷 ;
劣势: 系统可用性降低 、一致性问题 、系统复杂度提高 。
使用 MQ 需要满足什么条件
① 生产者不需要从消费者处获得反馈。引入消息队列之前的直接调用,其接口的返回值应该为空,这才让明明下层的动作还没做,上层却当成动作做完了继续往后走,即所谓异步成为了可能。
② 容许短暂的不一致性。
③ 确实是用了有效果。即解耦、提速、削峰这些方面的收益,超过加入MQ,管理MQ这些成本。
RabbitMQ 简介
AMQP, 即 Advanced Message Queuing Protocol(高级消息队列协议),是一个网络协议,是应用层协议的一个开放标准,为面向消息的中间件设计。基于此协议的客户端与消息中间件可传递消息,并不受客户端/中间件不同产品,不同的开发语言等条件的限制。 2006年, AMQP 规范发布。 类比HTTP。
RabbitMQ 基础架构图
RabbitMQ 中的相关概念:
**Broker:**接收和分发消息的应用,RabbitMQ Server就是 Message Broker。
**Virtual host:**出于多租户和安全因素设计的,把 AMQP 的基本组件划分到一个虚拟的分组中,类似于网络中的 namespace 概念。当多个不同的用户使用同一个 RabbitMQ server 提供的服务时,可以划分出多个vhost,每个用户在自己的 vhost 创建 exchange/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 的分发依据。
RabbitMQ 提供了 6种工作模式
简单模式、 work queues、 Publish/Subscribe 发布与订阅模式、Routing、路由模式、 Topics 主题模式、 RPC 远程调用模式 。
简单模式概念
P:生产者,也就是要发送消息的程序。
C:消费者:消息的接收者,会一直等待消息到来。
queue:消息队列,图中红色部分。类似一个邮箱,可以缓存消息;生产者向其中投递消息,消费者从其中取出消息 。
Work queues 工作队列模式
在一个队列中有多个消费者 ,消费者之间对于同一个消息的关系是竞争的关系 。对于任务过重或任务较多情况使用工作队列可以提高任务处理的速度。
Pub/Sub 订阅模式
**P:**生产者,也就是要发送消息的程序,但是不再发送到队列中,而是发给X(交换机)
**C:**消费者,消息的接收者,会一直等待消息到来
**Queue:**消息队列,接收消息、缓存消息
**Exchange:**交换机(X)。一方面,接收生产者发送的消息。另一方面,知道如何处理消息,例如递交给某个特别队列、
递交给所有队列、或是将消息丢弃。到底如何操作,取决于Exchange的类型。 Exchange有常见以下3种类型:
➢ Fanout:广播,将消息交给所有绑定到交换机的队列
➢ Direct:定向,把消息交给符合指定routing key 的队列
➢ Topic:通配符,把消息交给符合routing pattern(路由模式) 的队列
**Exchange(交换机)**只负责转发消息,不具备存储消息的能力,因此如果没有任何队列与 Exchange 绑定,或者没有符合
路由规则的队列,那么消息会丢失!
发布订阅模式与工作队列模式的区别:
- 工作队列模式不用定义交换机,而发布/订阅模式需要定义交换机。
- 发布/订阅模式的生产方是面向交换机发送消息,工作队列模式的生产方是面向队列发送消息(底层使用默认交换机)。
- 发布/订阅模式需要设置队列和交换机的绑定,工作队列模式不需要设置,实际上工作队列模式会将队列绑 定到默认的交换机 。
Routing 路由模式
队列与交换机的绑定,不能是任意绑定了,而是要指定一个 RoutingKey(路由key)
消息的发送方在向 Exchange 发送消息时,也必须指定消息的 RoutingKey
Exchange 不再把消息交给每一个绑定的队列,而是根据消息的 Routing Key 进行判断,只有队列的
Routingkey 与消息的 Routing key 完全一致,才会接收到消息
P:生产者,向 Exchange 发送消息,发送消息时,会指定一个routing key
X: Exchange(交换机),接收生产者的消息,然后把消息递交给与 routing key 完全匹配的队列
C1:消费者,其所在队列指定了需要 routing key 为 error 的消息
C2:消费者,其所在队列指定了需要 routing key 为 info、 error、 warning 的消息
Topics 通配符模式
Topic 类型与 Direct 相比,都是可以根据 RoutingKey 把消息路由到不同的队列。只不过 Topic 类型。Exchange 可以让队列在绑定 Routing key 的时候使用通配符!
Routingkey 一般都是有一个或多个单词组成,多个单词之间以” .”分割,例如: item.insert
通配符规则: # 匹配一个或多个词, * 匹配不多不少恰好1个词,例如: item.# 能够匹配 item.insert.abc或者 item.insert, item.* 只能匹配 item.insert
SpringBoot 整合 RabbitMQ
生产端
- 创建生产者SpringBoot工程
- 引入start,依赖坐标
org.springframework.boot
spring-boot-starter-amqp
- 编写yml配置,基本信息配置
spring:
rabbitmq:
host: 192.168.121.140
port: 5672
username: admin
password: admin
virtual-host: /
- 定义交换机,队列以及绑定关系的配置类
@Configuration
public class RabbitMQConfig {
public static final String EXCHANGE_NAME = "boot_topic_exchange2";
public static final String QUEUE_NAME = "boot_queue66";
// 1 交换机
@Bean("bootExchange")
public Exchange bootExchange(){
return ExchangeBuilder.topicExchange(EXCHANGE_NAME).durable(true).build();
}
//2.Queue 队列
@Bean("bootQueue")
public Queue bootQueue(){
return QueueBuilder.durable(QUEUE_NAME).build();
}
//3. 队列和交互机绑定关系 Binding
/* 1. 知道哪个队列
2. 知道哪个交换机
3. routing key
noargs():表示不指定参数
*/
@Bean
public Binding bindQueueExchange(@Qualifier("bootQueue") Queue queue,
@Qualifier("bootExchange") Exchange exchange){
return BindingBuilder.bind(queue).to(exchange).with("confirm").noargs();
}
}
- 注入RabbitTemplate,调用方法,完成消息发送
//1.注入RabbitTemplate
@Autowired
private RabbitTemplate rabbitTemplate;
@Test
public void testSend(){
rabbitTemplate.convertAndSend(RabbitMQConfig.EXCHANGE_NAME,"boot.haha","boot mq hello~~~");
}
消费端
- 创建消费者SpringBoot工程
- 引入start,依赖坐标
org.springframework.boot
spring-boot-starter-amqp
- 编写yml配置,基本信息配置
- 定义监听类,使用@RabbitListener注解完成队列监听。
@Component
public class RabbimtMQListener {
@RabbitListener(queues = "boot_queue")
public void ListenerQueue(Message message){
//System.out.println(message);
System.out.println(new String(message.getBody()));
}
}
RabbitMQ高级特性
消息可靠性投递
在使用 RabbitMQ 的时候,作为消息发送方希望杜绝任何消息丢失或者投递失败场景。 RabbitMQ 为我们提
供了两种方式用来控制消息的投递可靠性模式。
confirm 确认模式
return 退回模式
abbitmq 整个消息投递的路径为:producer—>rabbitmq broker—>exchange—>queue—>consumer
- 消息从 producer 到 exchange 则会返回一个 confirmCallback 。
- 消息从 exchange–>queue 投递失败则会返回一个 returnCallback 。
我们将利用这两个 callback 控制消息的可靠性投递
代码实例
确认模式
public class ProducerTest {
@Autowired
private RabbitTemplate rabbitTemplate;
/**
* 确认模式:
* 步骤:
* 1. 确认模式开启:yml中publisher-confirm-type: correlated
* 2. 在rabbitTemplate定义ConfirmCallBack回调函数
*/
@Test
public void testConfirm() {
//2. 定义回调
rabbitTemplate.setConfirmCallback(new RabbitTemplate.ConfirmCallback() {
@Override
public void confirm(CorrelationData correlationData, boolean ack, String cause) {
if (ack){
//接收成功
System.out.println("接收成功消息");
}else {
//接收失败
System.out.println("接收失败消息" + cause);
//做一些处理,让消息再次发送。
}
}
});
//3. 发送消息
rabbitTemplate.convertAndSend(RabbitMQConfig.EXCHANGE_NAME, "confirm", "消息");//成功
//rabbitTemplate.convertAndSend("test_exchange_confirm000", "confirm", "message confirm....");//失败
}
}
退回模式
public class ProducerTest {
@Autowired
private RabbitTemplate rabbitTemplate;
/**
* 回退模式: 当消息发送给Exchange后,Exchange路由到Queue失败时 才会执行 ReturnCallBack
* 步骤:
* 1. 开启回退模式:publisher-returns="true"
* 2. 设置ReturnCallBack
* 3. 设置Exchange处理消息的模式:
* 1). 如果消息没有路由到Queue,则丢弃消息(默认)
* 2). 如果消息没有路由到Queue,返回给消息发送方ReturnCallBack
* rabbitTemplate.setMandatory(true);
*/
@Test
public void testReturn() {
//设置交换机处理失败消息的模式
rabbitTemplate.setMandatory(true);
//2.设置ReturnCallBack
rabbitTemplate.setReturnCallback(new RabbitTemplate.ReturnCallback() {
/**
* @param message 消息对象
* @param replyCode 错误码
* @param replyText 错误信息
* @param exchange 交换机
* @param routingKey 路由键
*/
@Override
public void returnedMessage(Message message, int replyCode, String replyText, String exchange, String routingKey) {
System.out.println("return 执行了....");
System.out.println(message);
System.out.println(replyCode);
System.out.println(replyText);
System.out.println(exchange);
System.out.println(routingKey);
//处理
}
});
//3. 发送消息
rabbitTemplate.convertAndSend(RabbitMQConfig.EXCHANGE_NAME, "confirm", "message confirm....");
rabbitTemplate.convertAndSend(RabbitMQConfig.EXCHANGE_NAME, "confirm111", "message confirm....");
}
}
消息的可靠投递小结
spring:
rabbitmq:
publisher-confirm-type: correlated #开启确认模式
publisher-returns: true #开启退回模式
- 使用 rabbitTemplate.setConfirmCallback 设置回调函数。当消息发送到 exchange 后回调 confirm 方法。在方法中判断 ack,如果为true,则发送成功,如果为false,则发送失败,需要处理。
- 使用 rabbitTemplate.setReturnCallback 设置退回函数,当消息从exchange 路由到 queue 失败后,如果设置了 rabbitTemplate.setMandatory(true) 参数,则会将消息退回给 producer并执行回调函数returnedMessage
Consumer ACK
ack指Acknowledge,确认。 表示消费端收到消息后的确认方式。
有二种确认方式:
自动确认:默认
手动确认:
spring:
rabbitmq:
acknowledge-mode: manual
其中自动确认是指,当消息一旦被Consumer接收到,则自动确认收到,并将相应 message 从 RabbitMQ 的消息缓存中移除。但是在实际业务处理中,很可能消息接收到,业务处理出现异常,那么该消息就会丢失。
如果设置了手动确认方式,则需要在业务处理成功后,调用channel.basicAck(),手动签收,如果出现异常,则调用channel.basicNack()方法,让其自动重新发送消息。
代码实例
自动确认
@Component
public class AckListener implements MessageListener {
@RabbitListener(queues = "boot_queue66")
@Override
public void onMessage(Message message) {
System.out.println(new String(message.getBody()));
}
}
手动确认: 添加监听器
/**Consumer ACK机制:
* 1. 设置手动签收。acknowledge="manual"
* 2. 让监听器类实现ChannelAwareMessageListener接口
* 3. 如果消息成功处理,则调用channel的 basicAck()签收
* 4. 如果消息处理失败,则调用channel的basicNack()拒绝签收,broker重新发送给consumer
*/
@Component
public class AckListener1 implements ChannelAwareMessageListener {
@RabbitListener(queues = "boot_queue66")
@Override
public void onMessage(Message message, Channel channel) throws Exception {
Thread.sleep(1000);
// 获取消息传递标记
long deliveryTag = message.getMessageProperties().getDeliveryTag();
try {
// ① 接收消息
System.out.println(new String(message.getBody()));
// ② 处理业务逻辑
System.out.println("处理业务逻辑");
int i = 3/0;//出现错误
// ③ 手动签收
/**
* 第一个参数:表示收到的标签
* 第二个参数:如果为true表示可以签收所有的消息
*/
channel.basicAck(deliveryTag,true);
} catch (Exception e) {
e.printStackTrace();
// ④ 拒绝签收
/*
第三个参数:requeue:重回队列。
设置为true,则消息重新回到queue,broker会重新发送该消息给消费端
*/
channel.basicNack(deliveryTag,true,true);
}
}
}
Consumer Ack 小结
none:自动确认,manual:手动确认
如果在消费端没有出现异常,则调用channel.basicAck(deliveryTag,true);方法确认签收消息。如果出现异常,则在catch中调用 basicNack,拒绝消息,让MQ重新发送消息。如果设置不重新发送消息,这条消息不会重新回归队列中重新发送,会丢失这条数据。
并且再消息队列中不会保存。
消费端限流
消费端的确认模式一定为手动确认。acknowledge=“manual”
在配置文件中配置,concurrency配置属性设置消费端一次拉取多少消息
spring:
rabbitmq:
host: 127.0.0.1
port: 5672
username: nov17
password: nov17
virtual-host: testhost
listener:
simple:
max-concurrency: 1 #每次最多拿一条消息
acknowledge-mode: manual #手动ACK
TTL
TTL 全称 Time To Live(存活时间/过期时间)。当消息到达存活时间后,还没有被消费,会被自动清除。RabbitMQ可以对消息设置过期时间,也可以对整个队列(Queue)设置过期时间。如果两种ttl都设置了,那么就选择tll小的执行。
代码实例
设定整个队列的过期时间
配置类
public static final String ttl_queue_name = "xiangjiao.ttl.queue";
public static final String ttl_exchange_name = "xiangjiao.ttl.exchange";
public static final String ttl_routing_key = "xiangjiao.ttl.routingKey";
@Bean(value = "getTtlQueue")
public Queue getTtlQueue(){
// 设置 ttl 队列,并设定 x-message-ttl 参数,表示 消息存活最大时间,单位 ms
//return QueueBuilder.durable(ttl_queue_name).withArgument("x-message-ttl",10000).build();
Map<String, Object> arguments = new HashMap<>();
arguments.put("x-message-ttl",10000);
return new Queue(ttl_queue_name,true,false,false,arguments);
}
@Bean(value = "getTTlExchange")
public DirectExchange getTTlExchange(){
// 设置交换机属性,并保证交换机持久化
return new DirectExchange(ttl_exchange_name, true, false);
}
@Bean
public Binding bindExchangeAndQueueTTL(@Qualifier(value = "getTTlExchange") DirectExchange getTTlExchange,
@Qualifier(value = "getTtlQueue") Queue queue){
return BindingBuilder.bind(queue).to(getTTlExchange).with(ttl_routing_key);
}
对单个消息设定过期时间
针对单个消息设定不同的过期时间操作,则需要去掉队列过期设置。采取直连 Direct 交换机类型。
代码实例
/**
* 发送消息,指定ttl参数信息(单个消息);
* 测试需要将消息消费者关闭监听
* @return
*/
@RequestMapping("/sendTtl")
@ResponseBody
public String sendTtl(){
//发送10条消息
for (int i = 0; i < 10; i++) {
String msg = "msg"+i;
System.out.println("发送消息 msg:"+msg);
MessageProperties messageProperties = new MessageProperties();
messageProperties.setExpiration("5000"); // 针对消息设定时限
// 将消息数据和设置属性进行封装,采取消息发送模板,将消息数据推送至指定的交换机 exchange 中
Message message = new Message(msg.getBytes(), messageProperties);
rabbitmqService.sendMessage(MQConfiguration.EXCHANGE, MQConfiguration.ROUTING_KEY,message);
//每两秒发送一次
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
return "send ok";
}
总结
- 1、设置队列过期时间使用参数:x-message-ttl,单位:ms(毫秒),会对整个队列消息统一过期。
- 2、设置消息过期时间使用参数:expiration。单位:ms(毫秒),当该消息在队列头部时(消费时),会单独判断这一消息是否过期。
- 3、如果两者都进行了设置,以时间短的为准。
死信队列
什么是死信
死信队列,俗称DLX,翻译过来的名称为Dead Letter Exchange 死信交换机。当消息限定时间内未被消费,成为 Dead Message后,可以被重新发送到另一个交换机中,发挥其应有的价值!
死信队列逻辑图
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-sbegPDzn-1690797567586)(E:\笔记截图\R5.png)]
1、消息队列分为正常交换机、正常消息队列;以及死信交换机和死信队列。
2、正常队列针对死信信息,需要将数据 重新 发送至死信交换机中。
配置类编写
/**
* 死信队列配置
*/
@Configuration
public class DeadMsgMqConfig {
// 定义正常交换机和正常队列信息(交换机名、队列名、路由key)
public static final String queue_name = "xj_natural_queue";
public static final String exchange_name = "xj_natural_exchange";
public static final String routing_key = "xj_natural_routingKey";
// 定义死信交换机名、死信队列名、路由key
public static final String queue_name_dead = "xj_dead_queue";
public static final String exchange_name_dead = "xj_dead_exchange";
public static final String routing_key_dead = "xj_dead_routingKey";
/**
* 设置正常的消息队列;
* 正常的消息队列具备以下几种功能:
* 1、消息正常消费,需要绑定对应的消费者(这里为了测试死信,不创建消费者)
* 2、当消息失效后,需要将指定的消息发送至 死信交换机 中
* @return
*/
@Bean(value = "getNaturalQueue")
public Queue getNaturalQueue(){
return QueueBuilder.durable(queue_name)
// 正常的队列,在消息失效后,需要将消息丢入 死信 交换机中
// 这里只需要针对名称进行绑定
.withArgument("x-dead-letter-exchange",exchange_name_dead)
// 丢入 死信交换机,需要设定指定的 routingkey
.withArgument("x-dead-letter-routing-key",routing_key_dead)
// 设置正常队列中消息的存活时间为 10s,当然也可以针对单个消息进行设定不同的过期时间
.withArgument("x-message-ttl",10000)
// 设定当前队列中,允许存放的最大消息数目
.withArgument("x-max-length",10)
.build();
}
/**
* 设定正常的消息交换机
* @return
*/
@Bean(value = "getNaturalExchange")
public Exchange getNaturalExchange(){
// 这里为了测试,采取 direct exchange
return ExchangeBuilder.directExchange(exchange_name)
.durable(true) // 设定持久化
.build();
}
/**
* 将正常的消息交换机和正常的消息队列进行绑定
* @param queue
* @param directExchange
* @return
*/
@Bean
public Binding bindNaturalExchangeAndQueue(
@Qualifier(value = "getNaturalQueue") Queue queue,
@Qualifier(value = "getNaturalExchange") Exchange directExchange
){
return BindingBuilder
// 绑定消息队列
.bind(queue)
// 至指定的消息交换机
.to(directExchange)
// 匹配 routingkey
.with(routing_key)
// 无参数,不加会报错提示
.noargs();
}
/**
* 定义死信队列
* @return
*/
@Bean(value = "getDealQueue")
public Queue getDealQueue(){
return QueueBuilder.durable(queue_name_dead).build();
}
/**
* 定义死信交换机
* @return
*/
@Bean(value = "getDeadExchange")
public Exchange getDeadExchange(){
return ExchangeBuilder.directExchange(exchange_name_dead).durable(true).build();
}
/**
* 将死信交换机和死信队列进行绑定
* @param deadQueue
* @param directDeadExchange
* @return
*/
@Bean
public Binding bindDeadExchangeAndQueue(
@Qualifier(value = "getDealQueue") Queue deadQueue,
@Qualifier(value = "getDeadExchange") Exchange directDeadExchange
){
return BindingBuilder.bind(deadQueue).to(directDeadExchange).with(routing_key_dead).noargs();
}
}
死信交换机、死信队列也是普通的交换机和队列,只不过是我们人为的将某个交换机和队列来处理死信消息。
延迟队列
是通过 TTL 和 DXL 这两个属性间接实现的。我们需要建立2个队列,一个用于发送消息,一个用于消息过期后的转发目标队列。
然后普通队列中创建集合map用来保存队列属性,设置该队列绑定的死信交换机名称,设置routing key,设置队列延迟时间 10秒。
@Bean
public Queue queueC(){
//创建集合保存队列属性
Map<String, Object> map = new HashMap<>();
//设置该队列绑定的死信交换机名称
map.put("x-dead-letter-exchange", Y_DEAD_LETTER_EXCHANGE);
//设置routing key
map.put("x-dead-letter-routing-key", "YD");
//设置队列延迟时间 10秒
map.put("x-message-ttl", 10000);
return QueueBuilder.durable(QUEUE_C).withArguments(map).build();
}
的将某个交换机和队列来处理死信消息。
延迟队列
是通过 TTL 和 DXL 这两个属性间接实现的。我们需要建立2个队列,一个用于发送消息,一个用于消息过期后的转发目标队列。
然后普通队列中创建集合map用来保存队列属性,设置该队列绑定的死信交换机名称,设置routing key,设置队列延迟时间 10秒。
@Bean
public Queue queueC(){
//创建集合保存队列属性
Map<String, Object> map = new HashMap<>();
//设置该队列绑定的死信交换机名称
map.put("x-dead-letter-exchange", Y_DEAD_LETTER_EXCHANGE);
//设置routing key
map.put("x-dead-letter-routing-key", "YD");
//设置队列延迟时间 10秒
map.put("x-message-ttl", 10000);
return QueueBuilder.durable(QUEUE_C).withArguments(map).build();
}