1.RabbitMq工作原理
2.RabbitMQ工作模式
RabbitMQ有以下几种工作模式 :
2.1 hello world 点对点
生产者发送的消息,到队列后,由一个消费者消费
2.2 Work queues 工作队列
work queues与入门程序相比,多了一个消费端,两个消费端共同消费同一个队列中的消息。
应用场景:对于 任务过重或任务较多情况使用工作队列可以提高任务处理的速度。
2.3 Publish/Subscribe 发布订阅模式
发布订阅模式
:
1、每个消费者监听自己的队列。
2、生产者将消息发给broker,由交换机将消息转发到绑定此交换机的每个队列,每个绑定交换机的队列都将接收到消息
2.4 Routing 路由模式
路由模式:
1、每个消费者监听自己的队列,并且设置routingkey。
2、生产者将消息发给交换机,由交换机根据routingkey来转发消息到指定的队列。
2.5 Topics 统配符模式
统配符模式:
1、每个消费者监听自己的队列,并且设置带统配符的routingkey。
2、生产者将消息发给broker,由交换机根据routingkey来转发消息到指定的队列。
3.5 其它模式
2.6 Header Header模式
header模式与routing不同的地方在于,header模式取消routingkey,使用header中的
key/value(键值对)匹配队列。
案例:
根据用户的通知设置去通知用户,设置接收Email的用户只接收Email,设置接收sms的用户只接收sms,设置两种通知类型都接收的则两种通知都有效。
2.7 RPC
RPC即客户端远程调用服务端的方法 ,使用MQ可以实现RPC的异步调用,基于Direct交换机实现,流程如下:
1、客户端即是生产者就是消费者,向RPC请求队列发送RPC调用消息,同时监听RPC响应队列。
2、服务端监听RPC请求队列的消息,收到消息后执行服务端的方法,得到方法返回的结果 3、服务端将RPC方法 的结果发送到RPC响应队列
4、客户端(RPC调用方)监听RPC响应队列,接收到RPC调用结果。
3.RabbitMq和springboot结合
3.1 导入依赖
<!--RabbitMq 依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
3.2 配置yml文件
server:
port: 8080
spring:
rabbitmq:
host: # 远程连接的ip地址(即:rabbitMq的管理端的ip) 也可以是本地ip localhost
port: 5672 # 内部连接的端口号 ,而15672是管理端的端口号
username: guest
password: guest
3.3 生产者RabbitMqConfig类
RabbitMqConfig
类,本案例主要定义了,队列 交换机、队列与交换机的绑定、routingkey.
springboot 启动的时候,帮助我们把连接创建好,直接使用连接发送就可以了
一对多:
发布订阅FanoutExchange
一对一:
routing模式 DirectExchange
一对一, 一对多:
通配符模式TopicExchange
/**
*
*/
@Configuration
public class RabbitMqConfig {
/* //定义队列
@Bean
public Queue queue(){
return new Queue("hello");
}*/
//发布订阅模式 与Routing路由模式下的两个队列
@Bean(name = "smsQueue")
public Queue smsQueue(){
return new Queue("sms");//短信队列
}
@Bean(name = "emailQueue")
public Queue emailQueue(){
return new Queue("email");//邮箱队列
}
/*//定义交换机 发布订阅的交换机
@Bean
public FanoutExchange fanoutExchange(){
return new FanoutExchange("my-publish-exchange");
}
//将交换机与队列进行绑定
@Bean
Binding bindSmsQueueToFanoutExchange(@Qualifier("smsQueue")Queue queue,FanoutExchange fanoutExchange){
//绑定sms队列 到 交换机上
return BindingBuilder.bind(queue).to(fanoutExchange);
}
@Bean
Binding bindeMailQueueToFanoutExchange(@Qualifier("emailQueue")Queue queue,FanoutExchange fanoutExchange){
//绑定email队列 到 交换机上
return BindingBuilder.bind(queue).to(fanoutExchange);
}*/
//定义routing模式的交换机
/*@Bean
public DirectExchange directExchange(){
return new DirectExchange("my-direct-exchange");
}
//将队列绑定到directExchange交换机上 smsQueue 放在ioc中的 队列的唯一标识
@Bean
public Binding bindSmsQueueToDirectExchange(@Qualifier("smsQueue")Queue queue,DirectExchange directExchange){
//绑定sms队列 到 交换机上
return BindingBuilder.bind(queue).to(directExchange).with("sms");
}
@Bean
public Binding bindMailQueueToDirectExchange(@Qualifier("emailQueue")Queue queue,DirectExchange directExchange){
//绑定email队列 到 交换机上
return BindingBuilder.bind(queue).to(directExchange).with("email");
}*/
/**
* 统配符模式 在routingKey中设置通配符,只要满足routingKey的一部分可以接收到 交换机转发到队列的消息
* @return
*/
@Bean
public TopicExchange topicExchange(){
return new TopicExchange("my-topic-exchange");
}
//将队列绑定到交换机上
@Bean
Binding bindSmsQueueTopicExchange(@Qualifier("smsQueue")Queue queue,TopicExchange topicExchange){
/**
* 统配符:
* 1.* 代表一个字符
* 2.# 代表0个或者多个
*/
return BindingBuilder.bind(queue).to(topicExchange).with("#.send.sms.#");
}
@Bean
Binding bindEmailQueueTopicExchange(@Qualifier("emailQueue")Queue queue,TopicExchange topicExchange){
return BindingBuilder.bind(queue).to(topicExchange).with("#.send.#.email");
}
}
3.4 生产者测试
@RunWith(SpringRunner.class)
@SpringBootTest
public class SendMessage {
//使用springboot提供的模板类
@Autowired
RabbitTemplate rabbitTemplate;
/**
* hello world
* 点对点的消息的发送
* 消息生产者--->消息队列--->消息的消费者
* 没加交换机 routingKey:代表消息队列的名称
*/
@Test
public void testSendHello(){
//使用hello 1.交换机的名称 如果给"" ,会发送给rabbitMq默认的交换机 2.消息队列名称 3.发送的信息的内容
rabbitTemplate.convertAndSend("","hello","哥是这样一个被爱伤害的男人!");
}
//工作队列 在hello world 这种模式 多加启动一个消费端服务
//发布订阅模式 在工作队列的基础上 在加一个交换机
@Test
public void sendPublish(){
/**
* 1.exchange:交换机名称
* 2.规则为null 代表发布订阅模式 代表着所有和交换机绑定的队列,都能接收到消息
* 3.消息内容主题
*/
rabbitTemplate.convertAndSend("my-publish-exchange","","发送到邮件服务和短信服务!");
}
//routing 路由模式
@Test
public void testSendRouting(){
/**
* 1.exchange:交换机名称
* 2.路由的规则,规则与配置类中设置的规则进行匹配,只有满足规则的队列,才能接收到交换机转发的消息
* 3.消息内容主题
*/
rabbitTemplate.convertAndSend("my-direct-exchange","sms","路由模式:发送到短信服务");
rabbitTemplate.convertAndSend("my-direct-exchange","email","路由模式:发送到邮件服务");
}
//Topic 通配符模式
@Test
public void testSendTopic(){
//两个队列都能接收到
rabbitTemplate.convertAndSend("my-topic-exchange","send.sms.email","通配符模式:同时发送短信邮箱服务");
//只有邮箱队列能收到
//rabbitTemplate.convertAndSend("my-topic-exchange","send.email","通配符模式:发送邮箱服务");
//只有短信队列能收到
//rabbitTemplate.convertAndSend("my-topic-exchange","send.sms","通配符模式:发送电信短信服务");
}
}
3.4消费者监听消息队列
消息
生产者
服务与消费者
服务配置文件中的关于RabbitMq配置相同
依赖相同
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;
@Component
public class ConsumerClient {
@RabbitListener(queues = "sms")
public void smsListen(String message){
System.out.println("短信服务监听到的是:"+message);
}
@RabbitListener(queues = "email")
public void emailListen(String message){
System.out.println("邮件服务监到的是:"+message);
}
}
4.0消息Ack确认机制
当消费者端消费消息时,发生异常,我们使用try-catch处理异常后,我们发现,消费者端没接收到消息,而rabbitMQ队列中的消息丢失了,这就有问题了,原因出在那呢?
原因在于RabbitMq有一个消息的自动确认机制,当我们想解决上述问题,需要将
自动确认机制,改为手动确认
RabbitMQ的消息确认有两种。
- 消息发送确认:这种是用来确认
生产者将消息发送给交换器
,交换器传递给队列
的过程中,消息是否成功投递。发送确认分为两步,一是确认是否到达交换器,二是确认是否到达队列。
- 消费接收确认:这种是
确认消费者是否成功消费了队列中的消息
。
4.1 消息发送到交换机的确认
4.1.1 创建一个类ConfirmCallback
ConfirmCallBackHandler.java
在消费者端创建一个类
ConfirmCallBackHandler
,实现RabbitTemplate.ConfirmCallback
通过实现ConfirmCallBack接口,消息发送到交换器Exchange后触发回调。
import org.springframework.amqp.rabbit.support.CorrelationData;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
@Component
public class ConfirmCallBackHandler implements RabbitTemplate.ConfirmCallback {
/**
* 回调的方法
* @param correlationData 消息的唯一标识
* @param b 确认结果
* @param s 失败原因
*/
@Override
public void confirm(CorrelationData correlationData, boolean b, String s) {
System.out.println("消息的唯一标识:"+correlationData);
System.out.println("消息是否被投递到交换机:"+b);
System.out.println("消息失败的原因:"+s);
}
}
4.1.2 在RabbitConfig中加入一下代码
或者将
下面的代码放在ConfirmCallBackHandler 中
@Autowired
RabbitTemplate rabbitTemplate;
//在类加载时最先进行声明
@PostConstruct
public void rabbitTemplate(){
//正确将消息投递到交换机中进行回调
rabbitTemplate.setConfirmCallback(new ConfirmCallBackHandler());
}
4.1.3 在配置文件中加入 消息发送到交换机的确认
#开启消息发送交换机确认
spring.rabbitmq.publisher-confirms = true
4.1.4 controller层
@RestController
public class ConfirmController {
@Autowired
RabbitTemplate rabbitTemplate;
@RequestMapping(value = "/send/{message}")
public String sendMessage(@PathVariable("message") String message){//message 需要发送到交换机上的信息
rabbitTemplate.convertAndSend("my-topic-exchange","send.sms.email",message);
return "success";
}
}
4.1.5 使用postMan 测试
4.1.6 控制台打印
4.2 消息发送到队列的确认
4.2.1 配置文件中配置
# 开启消息由交换机发送队列回调
spring.rabbitmq.publisher-returns = true
4.2.2 类ConfirmCallback 实现RabbitTemplate.ReturnCallback
重写方法
/**
* 消息发送到队列的确认(回调方法)
* @param message 消息的主体
* @param i 消息的应答码
* @param s 失败原因描述
* @param s1 交换机的名称
* @param s2 消息使用的RoutingKey(路由键)
*/
@Override
public void returnedMessage(Message message, int i, String s, String s1, String s2) {
System.out.println("消息的主体====="+message);
System.out.println("消息的应答码====="+i);
System.out.println("失败原因描述====="+s);
System.out.println("交换机的名称====="+s1);
System.out.println("消息使用的RoutingKey====="+s2);
}
4.2.3 设置消息由交换机发送队列的回调(包括消息发送到交换机的回调)
@PostConstruct//在类加载时最先进行声明
public void rabbitTemplate(){
//正确将消息投递到交换机中进行的回调
rabbitTemplate.setConfirmCallback(new ConfirmCallBackHandler());
//设置由交换机将消息转换到队列的回调
//消息发送确认,如果消息从交换器发送到对应队列失败时触发
// (比如根据发送消息时指定的routingKey找不到队列时会触发)
rabbitTemplate.setReturnCallback(new ConfirmCallBackHandler());
}
4.2.4 修改消息发送时的routingKey为**,然后使用postMan,进行测试
测试结果如图所示:
我们发现消息发送到了,交换机,但是没有发送到队列,原因在于没有找到这个路由
4.2.5 将确认回调的配置(除配置文件外)所有配置
@Component
public class ConfirmCallBackHandler implements RabbitTemplate.ConfirmCallback, RabbitTemplate.ReturnCallback {
@Autowired
RabbitTemplate rabbitTemplate;
@PostConstruct//在类加载时最先进行声明
public void rabbitTemplate(){
//正确将消息投递到交换机中进行的回调
rabbitTemplate.setConfirmCallback(new ConfirmCallBackHandler());
//设置由交换机将消息转换到队列的回调
//消息发送确认,如果消息从交换器发送到对应队列失败时触发
// (比如根据发送消息时指定的routingKey找不到队列时会触发)
rabbitTemplate.setReturnCallback(new ConfirmCallBackHandler());
}
/**
* 消息发送到交换机的确认(回调的方法)
* @param correlationData 消息的唯一标识
* @param b 消息是否被投递到交换机
* @param s 消息发送失败的原因
*/
@Override
public void confirm(CorrelationData correlationData, boolean b, String s) {
System.out.println("消息的唯一标识:"+correlationData);
System.out.println("消息是否被投递到交换机:"+b);
System.out.println("消息发送失败的原因:"+s);
}
/**
* 消息发送到队列的确认(回调方法)
* @param message 消息的主体
* @param i 消息的应答码
* @param s 失败原因描述
* @param s1 交换机的名称
* @param s2 消息使用的RoutingKey(路由键)
*/
@Override
public void returnedMessage(Message message, int i, String s, String s1, String s2) {
System.out.println("消息的主体====="+message);
System.out.println("消息的应答码====="+i);
System.out.println("失败原因描述====="+s);
System.out.println("交换机的名称====="+s1);
System.out.println("消息使用的RoutingKey====="+s2);
}
}
4.2.6 消息发送端的配置文件
server:
port: 8080
spring:
rabbitmq:
# 远程连接的ip地址(即:rabbitMq的管理端的ip) 也可以是本地ip localhost
host: 115.29.212.111
# 内部连接的端口号 ,而15672是管理端的端口号
port: 5672
username: guest
password: guest
#消息发送交换机确认回调
publisher-confirms: true
#消息由交换机发送队列确认回调
publisher-returns: true
4.3 消息接收确认(由自动确认改为手动确认)
消费者端发生异常后,会使队列中的消息丢失,所以我们在消费者端,一旦,发生异常,设置,让队列中的消息不会被删除,在消息成功消费后,使用ack确认消息,将队列中的消息删除
(1)确认模式
消息消费者如何通知 Rabbit 消息消费成功?
消息通过 ACK 确认是否被正确接收,每个 Message 都要被确认(acknowledged),可以手动去 ACK 或自动 ACK
自动确认会在消息发送给消费者后立即确认,但存在丢失消息的可能,如果消费端消费逻辑抛出异常,也就是消费端没有处理成功这条消息,那么就相当于丢失了消息
如果消息已经被处理,但后续代码抛出异常,使用 Spring 进行管理的话消费端业务逻辑会进行回滚,这也同样造成了实际意义的消息丢失
如果手动确认则当消费者调用 ack、nack、reject 几种方法进行确认,手动确认可以在业务失败后进行一些操作,如果消息未被 ACK 则会发送到下一个消费者
如果某个服务忘记 ACK 了,则 RabbitMQ 不会再发送数据给它,因为 RabbitMQ 认为该服务的处理能力有限
ACK 机制还可以起到限流作用,比如在接收到某条消息时休眠几秒钟
消息确认模式有:
AcknowledgeMode.NONE:自动确认
AcknowledgeMode.AUTO:根据情况确认
AcknowledgeMode.MANUAL:手动确认
确认消息(局部方法处理消息)
默认情况下消息消费者是自动 ack (确认)消息的,如果要手动 ack(确认)则需要修改确认模式为 manual
(2)配置文件中添加配置
# 开启手动Ack确认
spring.rabbitmq.listener.simple.acknowledge-mode = manual
(3)修改监听邮箱队列
@Component
public class ConsumerClient {
@RabbitListener(queues = "sms")
public void smsListen(String message){
System.out.println("短信服务监听到的是:"+message);
}
/**
*
* @param mess 消息的内容
* @param channel 通道信息
* @param message 消息的唯一标识
*/
@RabbitListener(queues = "email")
public void emailListen(String mess, Channel channel, Message message){
//获取消息的唯一标识
long deliveryTag = message.getMessageProperties().getDeliveryTag();
try{
//int i = 1/0;
System.out.println("邮件服务监到的是:"+mess);
//虽然消息消费成功了,但是队列中的消息还没有被确认,我们需要确认队列中的消息被消费
//2参数 是否开启消息批处理
channel.basicAck(deliveryTag,true);
}catch (Exception e){
//没有正确执行完成,消息不会被删除
//消息没有消费成功,还是在RabbitMQ中
//System.out.println(e.getMessage());
try {
//参数2:是否进行批处理消息
//参数3:为true 代表消息始终在队列中 不会被删除 false 代表队列中的消息删除掉了
channel.basicNack(deliveryTag,true,true);
} catch (IOException e1) {
e1.printStackTrace();
}
//System.out.println(e.getMessage());
}
}
}
(4)消费者端配置文件
server:
port: 8081
spring:
rabbitmq:
host: 115.29.212.111
# 内部连接的端口号 ,而15672是管理端的端口号
port: 5672
username: guest
password: guest
# 开启手动ack的确认
listener:
simple:
acknowledge-mode: manual
需要注意的 basicAck 方法需要传递两个参数
- deliveryTag(唯一标识 ID):当一个消费者向 RabbitMQ 注册后,会建立起一个 Channel ,RabbitMQ 会用 basic.deliver 方法向消费者推送消息,这个方法携带了一个 delivery tag, 它代表了
RabbitMQ 向该 Channel 投递的这条消息的唯一标识 ID,是一个单调递增的正整数,delivery tag 的范围仅限于
Channel- multiple:为了减少网络流量,手动确认可以被批处理,当该参数为 true 时,则可以一次性确认 delivery_tag 小于等于传入值的所有消息
4.4 使用过期时间TTl实现延迟队列
4.4.1 配置
@Configuration
public class RabbitMqDeadConfig {
@Bean(name = "normalQueue")
public Queue queue(){
return new Queue("normal-queue");
}
/**
1、name: 队列的名称;
2、durable: 是否持久化;
3、exclusive: 是否独享、排外的;
4、autoDelete: 是否自动删除;
5.arguments:队列的其他属性参数,
(1)x-message-ttl:消息的过期时间,单位:毫秒;
(2)x-expires:队列过期时间,队列在多长时间未被访问将被删除,单位:毫秒;
(3)x-max-length:队列最大长度,超过该最大值,则将从队列头部开始删除消息;
(4)x-max-length-bytes:队列消息内容占用最大空间,受限于内存大小,超过该阈值则从队列头部开始删除消息;
(5)x-overflow:设置队列溢出行为。这决定了当达到队列的最大长度时消息会发生什么。有效值是drop-head、reject-publish或reject-publish-dlx。仲裁队列类型仅支持drop-head;
(6)x-dead-letter-exchange:死信交换器名称,过期或被删除(因队列长度超长或因空间超出阈值)的消息可指定发送到该交换器中;
(7)x-dead-letter-routing-key:死信消息路由键,在消息发送到死信交换器时会使用该路由键,如果不设置,则使用消息的原来的路由键值
(8)x-single-active-consumer:表示队列是否是单一活动消费者,true时,注册的消费组内只有一个消费者消费消息,其他被忽略,false时消息循环分发给所有消费者(默认false)
(9)x-max-priority:队列要支持的最大优先级数;如果未设置,队列将不支持消息优先级;
(10)x-queue-mode(Lazy mode):将队列设置为延迟模式,在磁盘上保留尽可能多的消息,以减少RAM的使用;如果未设置,队列将保留内存缓存以尽可能快地传递消息;
(11)x-queue-master-locator:在集群模式下设置镜像队列的主节点信息。
*/
@Bean(name = "deadQueue")
public Queue deadQueue(){
//设置私信队列
Map map = new HashMap<>();
//设在私信队列种,当我们得队列过期后,回将消息转发到下方得交换机中
map.put("x-dead-letter-exchange","normal-exchange");
//转发到正常队列时得routingkey
map.put("x-dead-letter-routing-key","123");
return new Queue("dead-queue",true,false,false,map);
}
//声明得死亡交换机
@Bean(name = "dead-topic-exchange")
public TopicExchange topicExchange(){
return new TopicExchange("dead-topic-exchange");
}
//声明正常得交换机
@Bean(name = "normal-topic-exchange")
public TopicExchange normalTopicExchange(){
return new TopicExchange("normal-exchange");
}
@Bean
Binding bindDeadQueueToExchange(@Qualifier("deadQueue")Queue queue,@Qualifier("dead-topic-exchange") TopicExchange topicExchange){
return BindingBuilder.bind(queue).to(topicExchange).with("321");
}
@Bean
Binding bindNoramlQueueToExchange(@Qualifier("normalQueue")Queue queue,@Qualifier("normal-topic-exchange") TopicExchange topicExchange){
return BindingBuilder.bind(queue).to(topicExchange).with("123");
}
}
4.4.2 controller层
@RequestMapping("/send/dead/{message}")
public String sendDeadMessage(@PathVariable("message") String message){
rabbitTemplate.convertAndSend("dead-topic-exchange","321",message, new MessagePostProcessor() {
@Override
public Message postProcessMessage(Message message) throws AmqpException {
//设置消息过期时间10秒 单位毫秒
message.getMessageProperties().setExpiration("10000");
return message;
}
});
return "success";
}
5. 消息总线:Spring Cloud Bus
消息总线的核心思想:
首先请求一个微服务,然后由这个微服务去通知Spring Cloud Bus
Spring Cloud Bus收到消息后,去通知配置了相关配置的微服务
实质上
Spring Cloud Bus
应用的就是RabbiteMQ 的发布订阅模式
,
消息的生产者–>交换机–>转发消息到绑定的队列–>消费者监听到队列,消费
最常用应用的就是,Config配置中心的改造,以达到动态获取配置文件,当然你如果使用的nacos,没必要看了,因为nacos一个注解搞定了…嘟嘟嘟
5.1.1 将依赖导入Config客户端
<!-- 消息总线 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-bus-amqp</artifactId>
</dependency>
5.1.2 配置文件添加
## 开启消息跟踪
management.endpoints.web.exposure.include= bus-refresh
spring.cloud.bus.trace.enabled=true
spring.rabbitmq.host=127.0.0.1
spring.rabbitmq.port=5672
spring.rabbitmq.username=guest
spring.rabbitmq.password=guest
当然上述配置文件内容也可以放在git上,启动会自动拉取
5.2.1 将依赖导入Config-server端
<!-- 消息总线 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-bus-amqp</artifactId>
</dependency>
5.2.2 配置文件加入以下配置
server:
port: 9900
spring:
application:
name: shop-config-server
# config和eureka 配置,自己配
rabbitmq:
host: 127.0.0.1
port: 5672
username: guest
password: guest
management:
endpoints:
web:
exposure:
include: bus-refresh
5.2.3 修改配置文件,请求config-server端
请求路径:
http://localhost:9900/actuator/bus-refresh
去看其他微服务控制台,动态的获取配置信息,正在打印中…
打印信息结束后,去测试…(自己测)
5.2.4 最后在所有controller层加上@RefreshScope
本来想加在启动类上,试了试不行,哎还是换Nacos吧…这不行!!!