文章目录
概念
在微服务架构中项目之间项目A调用项目B 项目B调用项目C项目C调用项目D。。
用户必须等待项目之间内容依次的运行结束后才会给用户返回结果
这是一个同步的调用,用户等待时间可能比较长,用户体验度比较差
消息中间件: 可以理解成一个队列,把用户需要处理的任务交给放到队列,任务在队列中进行排队等待执行,用户无需等待代码的执行的时间.
管理界面简介
1.Overview: 此面板为RabbitMQ基础信息展示面板,列举了服务器的信息,如:节点名称、内存占用、磁盘占用等。
2.Connections: 此面板中展示所有连接到RabbitMQ的客户端链接。只展示基于5672端口的链接。
3.Channels: 此面板中展示各链接中的具体信道。标记方式为链接(编号),如:192.168.91.1:12345(1)。
4.Exchanges: 此面板中展示RabbitMQ中已有的交换器,并注明交换器名称、类型等基本信息。其中只有direct交换器有默认交换器(AMQP default),当使用direct交换器时,如果没有明确指定名称,使用AMQP default交换器,也可明确指定名称。但是其他类型交换器没有默认的,都需要指定名称。
5.Queues: 此面板展示RabbitMQ中的队列信息。
创建队列
一.type:
1.Classic队列适用于简单的消息传递
2.Quorum队列提供高可用性和数据持久性,适用于关键应用
3.Stream队列用于处理事件流数据,适用于大规模的事件处理。
二.name: 自定义
三.Durability:
Durable: 消息保存到硬盘,这样消息的安全性更高。重启系统依然队列中数据依然存在
Transient: 消息或队列不会在系统重启后保留,可能会丢失。
四.Auto delete: 是,则所有的消费者都断开连接后删除自己
五.Arguments:
1.x-message-ttl:设置消息的过期时间(以毫秒为单位)。消息如果在指定的时间内没有被消费者消费,将被自动删除。
2.x-max-length:设置队列的最大长度。一旦队列中的消息数量达到该值,新的消息将被拒绝或丢弃,具体行为取决于其他参数的配置。
创建交换机
1.Name: 自定义
2.Type:
Direct Exchange(直接交换机):将消息通过精确的路由键发送到与路由键完全匹配的队列。适合点对点通信,路由键与队列名一一对应。
Fanout Exchange(扇出交换机):
将消息广播到绑定到它的所有队列,忽略路由键。用于广播消息给多个消费者,无需关心路由键。
Topic Exchange(主题交换机):
使用通配符匹配路由键,允许更灵活的消息路由。适合根据多个条件过滤消息,支持复杂的消息匹配。
Headers Exchange(头交换机):
根据消息头的键值对进行匹配,而不是路由键。用于复杂的消息过滤,根据消息的属性进行匹配。
3.autodelete:参数用于设置 RabbitMQ 交换机的自动删除行为:
autodelete=true:如果没有队列与交换机绑定,也没有活跃的连接或通道在使用该交换机时,它会自动删除。
autodelete=false(默认值):交换机不会自动删除,即使没有队列与其绑定或没有活跃的连接使用它。通常在需要长期存在的交换机上使用。
4.Internal的意思是内部的意思,在交换机这里设置为“Yes”之后,表示当前Exchange是RabbitMQ内部使用,用户所创建的Queue不会消费该类型交换机下的消息,既然是为了RabbitMQ系统所用,作为用户,我们就没有必要创建该类型的Exchange,当然默认也是选择No.
5.Arguments:
alternate-exchange:指定备用交换机,用于处理无法路由的消息。
x-delayed-type:创建延迟交换机,用于处理延迟消息。
x-dead-letter-exchange:定义死信消息应该发送到的交换机。
x-dead-letter-routing-key:定义死信消息的路由键。
x-max-priority:启用消息优先级,设置队列支持的最大优先级。
4种常见交换器类型
1.Direct交换器:
direct会根据路由键把消息放入到指定的队列中。
在Queues界面创建队列q1,在Exchanges创建direct交换机fs.direct绑定交换机q1,指定路由key,fs.q1
一.生产者
1.导入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
2.配置yml
# 配置RabbitMQ相关信息
# 当创建RabbitMQ容器的时候,不提供用户名和密码配置,自动创建用户guest,密码guest。
# guest用户只能本地访问RabbitMQ。
spring:
rabbitmq:
host: localhost # RabbitMQ服务器的IP。默认localhost
port: 32769 # RabbitMQ服务器的端口。
username: guest # RabbitMQ的访问用户名。默认guest。
password: guest # RabbitMQ的访问密码。默认guest
# 注意生产者一定要配置这个,消费者不用
virtual-host: / # 连接RabbitMQ中的哪一个虚拟主机。默认 /
3.在测试包下发送消息:
类型可以是: AmqpTemplate(顶级接口), RabbitOperations(专用子接口),RabbitTemplate(具体实现)
建议使用接口: 优先级是 RabbitOperations > AmqpTemplate
Spring AMQP可以发送的消息类型必须是Message类型。
Spring AMQP可以帮助程序员自动封装消息类型Message对象(默认封装),自动转换封装的消息体类型是Object,只要类型可序列化即可。
消息发送到队列后,无需等待,是异步操作,生产者接着往下执行.
@SpringBootTest
class RabbitmqApplicationTests {
@Autowired
private RabbitOperations rabbitOperations;
@Test
void contextLoads() {
//仅发送,不需要等待返回值,异步,推荐
rabbitOperations.convertAndSend("fs.dire"(交换机名称),"xy.q1"(路由key),"你好 rabbitmq"(消息内容));
//发送,有返回值,使得rabbitmq变成同步的了,需要等待返回值,失去了rabbitmq的意义
//rabbitOperations.convertSendAndReceive("","","");
}
}
二.消费者
1.pom.xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
2.yml
spring:
rabbitmq:
host: localhost
port: 32769
username: guest
password: guest
3.监听队列
修饰符: public
返回值: 异步消息必须是void
方法名: 自定义
参数表: 一个参数,类型可以是Message或者具体的消息体类型(Object)。message是Spring AMQP中消息的唯一类型。代表完整消息,有头和体组成。如果对消息头没有任何处理要求,则直接定义消息体具体类型即可。
方法实现: 根据具体要求,定义即可。
注意:方法可以抛出任意类型的异常。只要抛出异常,则代表消费错误,RabbitMQ不删除队列中的消息。
注解是RabbitListener。
如果有多个消费监听,默认采用轮询消费,消费监听不需要相互等待,并发执行.
@Component
public class StringMessageConsumer {
@RabbitListener(queues = {"q1"})(队列名称可以指定多个)
public void received1(String m){
System.out.println(m);
}
@RabbitListener(queues = {"q1"})(队列名称可以指定多个)
public void received2(String m){
System.out.println(m);
}
}
2.Fanout交换器
扇形交换器: 会把消息发送给所有的绑定在当前交换器上的队列,可以不写路由键
1.在Queues界面创建队列f1,f2,在Exchanges创建fanout交换机fs.fanout绑定交换机f1,f2,不指定路由
2.编写测试代码
@Autowired
private RabbitOperations rabbitOperations;
@Test
void contextLoad1s() {
//没有绑定路由键,设为null即可
rabbitOperations.convertAndSend("fs.fanout",null,"你好 rabbitmq");
}
3.f1,f2队列都多了一个消息
3.Topic交换器
主题交换器: 路由键可包括特殊字符实现通配。特殊字符包括: 星号和 ‘#’。
星号 : 代表一个单词。多个单词使用’.'分割。
‘#’ : 代表0~n个字符,即任意字符串。//如abc.# 代表以 abc. 开头的所有路由key
1.在Queues界面创建队列t1,t2,t3在Exchanges创建Topic交换机fs.topic绑定交换机t1,t2,t3
2.指定t1路由键: abc.t1,指定t2路由键: abc.*,指定t3路由键: abc.#
3.编写测试代码
@Autowired
private RabbitOperations rabbitOperations;
@Test
void contextLoawd1s() {
rabbitOperations.convertAndSend("fs.topic","abc.t1","你好 rabbitmq");//t1,t2,t3
rabbitOperations.convertAndSend("fs.topic","abc.123","你好 rabbitmq");//t2,t3
rabbitOperations.convertAndSend("fs.topic","abc.123.234","你好 rabbitmq");//t3
}
4.headers交换器
headers交换器和direct交换器的主要区别是在传递消息时可以传递header部分消息
生产消息
在Queues界面创建队列h1,在Exchanges创建headers交换机fs.headers绑定交换机h1,指定路由key,fs.h1
@Autowired
private RabbitOperations rabbitOperations;
@Test
void conwtextLoawd1s() {
MessageProperties messageProperties = new MessageProperties();
messageProperties.setHeader("fs","java");
Message message = new Message("我是消息".getBytes(), messageProperties);
rabbitOperations.convertAndSend("fs.headers","fs.h1",message);
}
消费消息
@RabbitListener(queues = "header")
public void headerMsg(Message msg) {
System.out.println(new String(msg.getBody()));
Map<String, Object> map = msg.getMessageProperties().getHeaders();
// 如果没有指定key返回null
System.out.println(map.get("cou"));
}
对象类型消息传递
1.生产者
创建User类
注意要继承序列化接口Serializable
同时指定序列化版本号serialVersionUID且和消费者一致(如果不指定,消费者的User类必须和生产者的User类完全一致,否则,不会认为是统一类型)
User类在生产者和消费者中的包路径必须一致
@Data
@NoArgsConstructor
@AllArgsConstructor
public class User implements Serializable {
private final Long serialVersionUID = 1L;
private String name;
private Integer age;
}
发送消息
@Autowired
private RabbitOperations rabbitOperations;
User user = new User("张三",18);
rabbitOperations.convertAndSend("fs.direct","xy.q1",user);
2.消费者
在消费者中创建User类
注意要继承序列化接口Serializable
同时指定序列化版本号serialVersionUID且和消费者一致(如果不指定,消费者的User类必须和生产者的User类完全一致,否则,不会认为是统一类型)
User类在生产者和消费者中的包路径必须一致
直接用对应类型接收
@Component
public class StringMessageConsumer {
@RabbitListener(queues = {"q1"})
public void received(User(类型和发送来的一致即可) user){
System.out.println(user);
}
}
统一用Message对象接收,常用语Headers交换器
@Component
public class StringMessageConsumer {
@RabbitListener(queues = {"q1"})
public void received(Message message) throws IOException, ClassNotFoundException {
byte[] body = message.getBody();
ByteArrayInputStream bai = new ByteArrayInputStream(body);
ObjectInputStream ois = new ObjectInputStream(bai);
Object object = ois.readObject();
if(object instanceof User){
User user = (User) object;
System.out.println(user);
}
}
}
同步等待
等待结果返回,才能往下执行
默认等待5s,可以配置,超时返回null
yml配置
spring:
rabbitmq:
host: localhost # RabbitMQ服务器的IP。默认localhost
port: 32769 # RabbitMQ服务器的端口。
username: guest # RabbitMQ的访问用户名。默认guest。
password: guest # RabbitMQ的访问密码。默认guest
virtual-host: / # 连接RabbitMQ中的哪一个虚拟主机。默认 /
生产者:
@Test
void contextLowads() {
//发送,有返回值,使得rabbitmq变成同步的了,需要等待返回值,等待结果返回,才能往下执行
String aa = (String)rabbitOperations.convertSendAndReceive("fs.direct", "xy.q1", "aa");
System.out.println(aa);
}
消费者:
@Component
public class StringMessageConsumer {
@RabbitListener(queues = {"q1"})
public String received(Message message) {
return "hello";
}
}
使用代码创建队列
一.在生成者端创建队列
是发送消息时创建,而不是启动项目时。
@Configuration
public class RabbitMQConfig {
// 发送消息时如果不存在这个队列,会自动创建这个队列。
// 注意:是发送消息时,而不是启动项目时。
// 相当于:可视化操作时创建一个队列
// 如果队列创建完成后,没有绑定(没有另外两个方法),默认绑定到AMQP default交换器
@Bean
public Queue queue(){
return new Queue("queue.second");
}
// 如果没有这个交换器,在发送消息创建这个交换器
// 配置类中方法名就是这个类型的实例名。相当于<bean id="" class="">的id属性,返回值相当于class
@Bean
public DirectExchange directExchange(){
return new DirectExchange("direct.first.ex");
}
// 配置类中方法参数,会由Spring 容器自动注入
@Bean
public Binding directBingding(DirectExchange directExchange,Queue queue){
// with(“自定义路由键名称”)
return BindingBuilder.bind(queue).to(directExchange).with("routing.key.2");
// withQueueName() 表示队列名就是路由键名称
// return BindingBuilder.bind(queue).to(directExchange).withQueueName();
}
}
二.消费者
@RabbitListener(bindings = {
@QueueBinding(
value = @Queue(name = "queue.second",autoDelete = "false", durable = "true"),
exchange = @Exchange(name = "direct.second.ex",autoDelete = "false", type = ExchangeTypes.DIRECT),
key = {"routing.key.second.1"}
)
})
public void onMessage(String messageBody){
System.out.println("第二个消息消费者监听,处理消息:" + messageBody);
}
开启生产者确认消息发送到交换机和队列
当Publisher发送消息时,需要先连接到Broker(RabbitMQ实例),再进入到virtual-host(虚拟主机),最后到达Exchange(交换器)。如果无法到达交换器,RabbitMQ会触发RabbitTemplate中的回调机制,通知Publisher消息发送的失败原因。
1.yml配置
spring:
rabbitmq:
host: 192.168.91.128 # RabbitMQ服务器的IP。默认localhost
port: 5672 # RabbitMQ服务器的端口。
username: bjsxt # RabbitMQ的访问用户名。默认guest。
password: bjsxt # RabbitMQ的访问密码。默认guest
virtual-host: / # 连接RabbitMQ中的哪一个虚拟主机。默认 /
确认消息是否发送到交换机,默认不开启
publisher-confirm-type: correlated # 开启到达交换器确认机制。默认值:none,不开启确认机制。
确认消息是否发送到队列,因为队列和交换机绑定唯一路由,默认不开启
publisher-returns: true
生产者添加配置类
@Component
public class RabbitMQConfig implements RabbitTemplate.ConfirmCallback,RabbitTemplate.ReturnCallback{
@Autowired
private RabbitTemplate rabbitTemplate;
/**
* @PostConstruct 是 Spring Framework 中的一个注解,通常用于标记一个方法,
* 在对象被创建后(执行构造方法后)立即执行该方法。
*/
@PostConstruct
public void init(){
//设置RabbitTemplate中的回调逻辑
this.rabbitTemplate.setConfirmCallback(this);
this.rabbitTemplate.setReturnCallback(this);
}
@Override
public void confirm(CorrelationData correlationData, boolean b, String s) {
//无论消息是否到达交换机该方法都会执行
System.out.println("消息唯一标记 : " + correlationData);
System.out.println("是否确认到达交换器 : " + b);
System.out.println("不能到达交换器的原因 : " + s);
}
/**
交换机消息成功到队列不执行该方法,否则执行,消息没有到达交换机,也不执行
*/
@Override
public void returnedMessage(Message message, int replyCode, String replyText, String exchange, String routingKey) {
System.out.println("交换器 : " + exchange);
System.out.println("路由键 : " + routingKey);
System.out.println("路由失败编码 : " + replyCode);
System.out.println("路由失败描述 : " + replyText);
System.out.println("消息 : " + message);
}
}
生产消息:
rabbitOperations.convertAndSend("fs.direct","fs.fs_direct","hello world");
成功发送到交换机但交换机没有发送到消息队列:
消息唯一标记 : null
是否确认到达交换器 : true
不能到达交换器的原因 : null
交换器 : fs.direct
路由键 : fs.fs_direct111
路由失败编码 : 312
路由失败描述 : NO_ROUTE
消息 : (Body:'hello world' MessageProperties [headers={}, contentType=text/plain, contentEncoding=UTF-8, contentLength=0, receivedDeliveryMode=PERSISTENT, priority=0, deliveryTag=0])
--------------------------------------------------------
没有成功发送到交换机:
消息唯一标记 : null
是否确认到达交换器 : false
不能到达交换器的原因 : channel error; protocol method: #method<channel.close>(reply-code=404, reply-text=NOT_FOUND - no exchange 'fs.direct1' in vhos
消费者:
@RabbitListener(queues = {"fs_direct"})
public void cu(String str){
System.out.println(str);
}
开启生产者消息消费确认
在Spring AMQP中,消费者(Consumer)默认的ACK机制是自动确认,即消费代码正常执行结束,立刻确认消息已消费;消费代码发送异常,相当于消息未消费。如果希望关闭自动ACK机制,可使用两种处理方案实现。
1.重试消费
可以在消费者中基于配置开启重试机制,并设置重试消费次数(默认一直重试)。当消费消息发生错误,导致未确认(NACK)时,消费者尝试重复消费消息;当重复消费次数到达设置阈值后,强制确认(ACK),RabbitMQ会移除队列中的消息。
spring:
rabbitmq:
host: 192.168.91.128
port: 5672
username: bjsxt
password: bjsxt
listener:
simple:
retry:
enabled: true # 开启重试机制
max-attempts: 2 # 重试消费2次
消费者:
@Component
public class StringMessageConsumer {
@RabbitListener(queues = {"queue.first"})
public void onMessage(String messageBody){
System.out.println("第一个消息消费者监听,处理消息:" + messageBody);
int i = 1/0; // 模拟消费失败
}
}
此时重试两次后会移除队列中的消息。
2.手工确认ACK
yml配置
spring:
rabbitmq:
host: 192.168.91.128
port: 5672
username: bjsxt
password: bjsxt
listener:
simple:
acknowledge-mode: manual # 手工确认。 默认AUTO,自动确认
/**
* 消费方法。实现手工ACK确认
* @param messageBody 消息内容
* @param channel 信道对象,当一个消费者向 RabbitMQ 注册后,会建立起一个 Channel ,
* RabbitMQ 会用 basic.deliver 方法向消费者推送消息。
* @param deliveryTag RabbitMQ向该Channel投递的这条消息的唯一标识 ID,
* 是一个递增的正整数,delivery tag 的范围仅限于 Channel
*/
@RabbitListener(queues = {"fs_direct"})
@GetMapping("/cu")
public void cu(String str, Channel channel, @Header(AmqpHeaders.DELIVERY_TAG) long deliveryTag){
try {
System.out.println(str);
int a = 1/0;
/**
* 参数1 - 消息的唯一标识
* 参数2 - 是否批量提交。手动确认可以被批处理,当该参数为 true 时,则可以一次性确认 deliveryTag 小于等于传入值的所有消息
*/
channel.basicAck(deliveryTag,true);
} catch (Exception e) {
try {
if(deliveryTag < 3){
/*
* 参数1 - 消息的唯一标识
* 参数2 - 是否批量提交
* 参数3 - 是否重新发出消息。
*/
channel.basicNack(deliveryTag, false, true);
System.out.println("重新发送消息次数:" + deliveryTag);
}else {
/**
*参数1 - 消息的唯一标识
*参数2 - 是否重新发出消息。false则废弃此消息。
*/
channel.basicReject(deliveryTag,false);
System.out.println("强制ACK确认");
}
} catch (IOException ex) {
ex.printStackTrace();
}
}
}
创建死信队列和死信交换机并使用
当消息变成死信后,它会被重新发送到另一个交换器(DLX:死信交换机),然后根据路由规则投递到另一个队列(DLQ:死信队列)。
消息变成放入死信队列的几种情况:
- 消息被拒绝(basicNack 或 basicReject)时,且未重新排队(requeue = false)。参考ACK确认机制。
- 消息过期。即消息头中的TTL过期。
- 队列达到最大长度时。
1.DLX就是一个普通的交换器为,最好是Topic ->sx.channel
2.创建DLQ(死信队列)并绑定到DLX(死信交换器)上,DLQ就是一个普通的队列 -> sx.queue
3.创建任意交换器,:必须设置队列参数 x-dead-letter-exchange,此参数用于绑定死信处理逻辑,即信息成为死信后,投递到哪个死信交换器。-> dd.queue
4.创建任意队列并绑定到交换器 -> dd.channel
5…发送消息:
public void show(){
String messageBody = "投递到test.queue队列中的消息,等待超时后投递到DLX中再处理";
MessageProperties messageProperties =
new MessageProperties();
// 设置消息持久化
messageProperties.setDeliveryMode(MessageDeliveryMode.PERSISTENT);
// 设置消息体字符集
messageProperties.setContentEncoding("UTF-8");
// 设置消息超时时间,单位毫秒,参数类型是字符串
messageProperties.setExpiration("7000");
// 基于字符串消息体内容和消息参数,创建要传递的消息对象。
Message message = new Message(messageBody.getBytes(), messageProperties);
rabbitOperations.convertAndSend("abc","dd",message);
}
5.观察到rabbitmq队列中dd.queue中有一条消息,过7秒后,该消息转移到了sx.queue中.
面试题
RabbitMQ为什么需要信道?为什么不是TCP直接通信?
1.TCP的创建和销毁开销大,需要3次握手和4次挥手。
2.没有信道,每个应用程序都需要单独建立TCP连接,会导致资源浪费和性能瓶颈。
3.信道允许多线程共享单个TCP连接,提高效率,避免性能问题。
RabbitMQ如何保证消息的可靠性和消息正常的传递?
发送者:
1、发送者到交换器之间的消息安全保证 Confirm模式 ConfirmCallback
2、交换器和队列之间消息的安全保证 Returns模式 ReturnsCallback
消费者:
1.默认:自动开启ACK机制 消费者(Consumer)默认的ACK机制是自动确认,即消费代码正常执行结束,立刻确认消息已消费
2.重试策略: 我们可以通过yml中配置的方式 指定重试次数 到达重试次数后强制的进行ACK确认 这个时候队列中消息就没了
3.手动配置ACK机制:
channel.basicAck(deliveryTag, false); //ACK确认
channel.basicNack(deliveryTag, false, true); //NACK
channel.basicReject(deliveryTag, false);//拒绝: 强制的ACK确认
RabbitMQ的服务:队列 和交换器 都实现了持久化方案,自动把MQ中信息持久化到磁盘中
如何保证消息幂等性/如何保证消息不被重复消费?
保证消息不重复提交
mp.setMessageId(UUID.randomUUID().toString());
保证消息不重复消费
使用Redis保存每一次的消息头中id 每一次调用之前判断redis中是否存在指定的数据
如果redis中对应的id不存在的,证明没有被消费 直接消费即可
如果redis中对应的id 存在的 证明当前消息已经被消费了 ,直接返回 /提示 /拒绝即可
如何保证消息的顺序性?【面试题】
方案A:拆分多个queue,每个queue一个consumer,就是多一些queue而已,确实是麻烦
方案B:就一个queue但是对应一个consumer,然后这个consumer内部用内存队列做排队,然后分发给底层不同的worker来处理。
有几百万消息持续积压几小时怎么解决?【面试题】
一般这个时候,只能操作临时紧急扩容了,具体操作步骤和思路如下:
- 先修复consumer的问题,确保其恢复消费速度,然后将现有cnosumer都停掉。
- 新建一个topic,partition是原来的10倍,临时建立好原先10倍或者20倍的queue数量。
- 然后写一个临时的分发数据的consumer程序,这个程序部署上去消费积压的数据,消费之后不做耗时的处理,直接均匀轮询写入临时建立好的10倍数量的queue。
- 接着临时征用10倍的机器来部署consumer,每一批consumer消费一个临时queue的数据。
- 这种做法相当于是临时将queue资源和consumer资源扩大10倍,以正常的10倍速度来消费数据。等快速消费完积压数据之后,得恢复原先部署架构,重新用原先的consumer机器来消费消息。
消息队列过期失效问题?
这个情况下,就不是说要增加consumer消费积压的消息,因为实际上没啥积压,而是丢了大量的消息。我们可以采取一个方案,
就是批量重导,这个我们之前线上也有类似的场景干过。就是大量积压的时候,我们当时就直接丢弃数据了,然后等过了高峰期
以后,这个时候我们就开始写程序,将丢失的那批数据,写个临时程序,一点一点的查出来,然后重新灌入mq里面去,把白天丢的数据
给他补回来。也只能是这样了。假设1万个订单积压在mq里面,没有处理,其中1000个订单都丢了,你只能手动写程序把那1000个
订单给查出来,手动发到mq里去再补一次.