一、消息队列MQ的三大功能
1.异步处理
2.应用解耦:A系统的代码需要调用B系统的接口才能完成一个任务,假设B两个系统崩溃了,那么A系统就会出现超时,导致整个系统不可用。如果使用消息队列可以将A调用B接口的参数放到队列里面,A代码里面不再直接去调用B系统的接口,而是让B系统订阅消费A发送过来的的消息(参数),B系统根据这些消息(参数)修改自己的逻辑即可,B系统维护时,不会影响A系统的使用。
3.流量控制:系统最高能承受100万并发,超出了就用队列进行排队。
二、高级消息队列交换机模型:
1. P2P(点对点)----- direct exchange 路由键精确匹配连接队列
2.pub/sub (发布/订阅) 以路由键匹配规则不同,划分出不同类型
|----- fanout exchange 没有路由键概念,扇出交换机直连绑定的队列
|------ topic exchange 路由键由.分割,模糊匹配连接队列* 匹配一个单词,#匹配0个或多个单词
|------ headers exchange
|------ system exchange
补充:
消息可靠投递:要消息要不丢失,保证三个阶段不出现问题,如图:
方案1:结合Rabbitmq 确认回调和回退回调处理
<<<<<<<<注意:发送消息前要将消息保存到Mysql起来>>>>>>>>
package com.atguigu.rabbitmq.springbootrabbitmq.config;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.connection.CorrelationData;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.lang.Nullable;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import javax.annotation.PostConstruct;
/**
* User: ldj
* Date: 2023/3/13
* Time: 20:19
* Description: No Description
*/
@Slf4j
@Component
public class RabbitmqCallbackConfig {
@Autowired
private RabbitTemplate rabbitTemplate;
//第二种写法(推荐)
@PostConstruct
public void initRabbitTemplate() {
rabbitTemplate.setConfirmCallback((@Nullable CorrelationData correlationData, boolean ack, @Nullable String cause) -> {
if (!ack) {
//TODO 消息没成功抵达Broker,修改Mysql消息状态标识,并从缓存获取再次发送
}
});
rabbitTemplate.setReturnCallback((Message message, int replyCode, String replyText, String exchange, String routingKey) -> {
if (!StringUtils.isEmpty(replyText)) {
//TODO 消息没成功抵达Queue,修改Mysql消息状态标识,弄一个定时任务读取数据mysql并向Rabbitmq发送消息
}
});
}
}
方案2:备用交换机也能很好解决第二阶段问题
三、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
#5671、5672 (AMQP端口)
#15672(web可视管理后台端口)
#4369、25672(Erlang发现&集群端口)
#61613、61614(STOMP协议端口)
#1883、8883(MQTT协议端口)
#开机自启
docker update rabbitmq --restart=always
四、springBoot整合rabbitmq 以及测试
#1.引入rabbitmq启动依赖
<!--rabbitMQ-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
#2.简单配置文件application.properties,必须以spring.rabbitmq开头
spring.rabbitmq.host=192.168.xxx.xxx
spring.rabbitmq.virtual-host=/
spring.rabbitmq.port=5672
#默认账号密码
spring.rabbitmq.username=guest
spring.rabbitmq.password=guest
#3.启动类加上@EnableRabbit
配置文件 application.properties
#rabbitmq安装在虚拟机的地址
spring.rabbitmq.host=192.168.126.131
#rabbitmq又进行虚拟服务器划分,默认是/
spring.rabbitmq.virtual-host=/
#默认端口
spring.rabbitmq.port=5672
#默认账号密码
spring.rabbitmq.username=guest
spring.rabbitmq.password=guest
#开启消息确认回调(PB)
spring.rabbitmq.publisher-confirm-type=correlated
#确认消息传达队列
spring.rabbitmq.publisher-returns=true
#异步发送优先回调返回确认
spring.rabbitmq.template.mandatory=true
#手动应答ack,自动模式宕机会导致消息丢失
spring.rabbitmq.listener.simple.acknowledge-mode=manual
#限制消费者每秒从队列拉取的消息的数量
spring.rabbitmq.listener.simple.prefetch=1000
1.组件准备
package com.atguigu.rabbitmq.springbootrabbitmq.config;
import com.atguigu.rabbitmq.springbootrabbitmq.config.properties.RabbitmqProperties;
import org.springframework.amqp.core.*;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.HashMap;
/**
* User: ldj
* Date: 2023/3/13
* Time: 17:56
* Description: No Description
*/
@Configuration
public class PublishConfirmConfig {
public static final String CONFIRM_EXCHANGE_NAME = "CF-EXC";
public static final String BACKUP_EXCHANGE_NAME = "BU-EXC";
public static final String CONFIRM_QUEUE_NAME = "CF-QUE";
public static final String BACKUP_QUEUE_NAME = "BU-QUE";
public static final String WARNING_QUEUE_NAME = "WAR-QUE";
/*
* P ----> CF-EXC --(b)--> CF-QUE ----> confirmConsumer |
* | ^
* v |
* BU-EXC ----> BU-QUE----------------
* |
* v
* WAR-QUE -------------------> warningConsumer
*/
/**
* 声明队列
*/
@Bean("confirmQueue")
public Queue confirmQueue() {
return QueueBuilder.durable(CONFIRM_QUEUE_NAME).build();
}
@Bean("backupQueue")
public Queue backupQueue() {
return QueueBuilder.durable(BACKUP_QUEUE_NAME).build();
}
@Bean("warningQueue")
public Queue warningQueue() {
return QueueBuilder.durable(WARNING_QUEUE_NAME).build();
}
/**
* 声明交换机
*/
@Bean("confirmExchange")
public TopicExchange confirmExchange() {
HashMap<String, Object> arguments = new HashMap<>();
return ExchangeBuilder
.topicExchange(CONFIRM_EXCHANGE_NAME)
.durable(true)
.alternate(BACKUP_EXCHANGE_NAME) //备份交换机
.withArguments(arguments)
.build();
}
@Bean("backupExchange")
public FanoutExchange backupExchange() {
return new FanoutExchange(BACKUP_EXCHANGE_NAME, true, false, new HashMap<>());
}
/**
* 交换机与队列捆绑
*/
@Bean
public Binding confirmQueueToConfirmExchange(@Qualifier("confirmQueue") Queue confirmQueue,
@Qualifier("confirmExchange") TopicExchange confirmExchange,
RabbitmqProperties properties) {
return BindingBuilder.bind(confirmQueue).to(confirmExchange).with(properties.getConfirmBingingKey());
}
@Bean
public Binding backupQueueToBackupExchange(@Qualifier("backupQueue") Queue confirmQueue,
@Qualifier("backupExchange") FanoutExchange confirmExchange) {
return BindingBuilder.bind(confirmQueue).to(confirmExchange);
}
@Bean
public Binding warningQueueToBackupExchange(@Qualifier("warningQueue") Queue confirmQueue,
@Qualifier("backupExchange") FanoutExchange confirmExchange) {
return BindingBuilder.bind(confirmQueue).to(confirmExchange);
}
}
2.消费者1
package com.atguigu.rabbitmq.springbootrabbitmq.listener;
import com.atguigu.rabbitmq.springbootrabbitmq.config.PublishConfirmConfig;
import com.rabbitmq.client.Channel;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitHandler;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;
/**
* User: ldj
* Date: 2023/3/13
* Time: 18:45
* Description: No Description
*/
@Slf4j
@Component
@RabbitListener(queues = {PublishConfirmConfig.CONFIRM_QUEUE_NAME,PublishConfirmConfig.BACKUP_QUEUE_NAME})
public class ConfirmListener {
@RabbitHandler
public void receiveConfirmMsg(Message message, Channel channel, String msg) {
log.info("接收到队列CF-QUE/BU-QUE消息:{}", msg);
}
}
消费者2
package com.atguigu.rabbitmq.springbootrabbitmq.listener;
import com.atguigu.rabbitmq.springbootrabbitmq.config.PublishConfirmConfig;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitHandler;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;
import java.net.InetAddress;
/**
* User: ldj
* Date: 2023/3/14
* Time: 8:32
* Description: No Description
*/
@Slf4j
@Component
@RabbitListener(queues = {PublishConfirmConfig.WARNING_QUEUE_NAME})
public class WarningListener {
@RabbitHandler
public void receiveConfirmMsg(Message message, Channel channel, String msg) {
Connection connection = channel.getConnection();
InetAddress address = connection.getAddress();
String hostAddress = address.getHostAddress();
log.warn("警告! [CF-EXC <{}>] 收到不可路由消息,已转发给备份交换机[BU-EXC]:{}", hostAddress, msg);
}
}
3.生产者
package com.atguigu.rabbitmq.springbootrabbitmq.controller;
import com.atguigu.rabbitmq.springbootrabbitmq.config.PublishConfirmConfig;
import com.atguigu.rabbitmq.springbootrabbitmq.config.properties.RabbitmqProperties;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.core.MessageProperties;
import org.springframework.amqp.rabbit.connection.CorrelationData;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.nio.charset.StandardCharsets;
import java.util.UUID;
/**
* User: ldj
* Date: 2023/3/13
* Time: 18:26
* Description: No Description
*/
@Slf4j
@RestController
@RequestMapping("/rabbitmq/confirm")
public class ConfirmMessageController {
@Autowired
private RabbitTemplate rabbitTemplate;
// http://localhost:8089/rabbitmq/confirm/sendMsg/发布确认可靠消息
@GetMapping(value = "/sendMsg/{message}")
public void sendConfirmMsg(@PathVariable String message) {
log.info("[sendConfirmMsg]要开始发消息:{}", message);
MessageProperties messageProperties = new MessageProperties();
messageProperties.setAppId("666888");
Message msg = new Message(message.getBytes(StandardCharsets.UTF_8), messageProperties);
//封装数据给确认回调接口
CorrelationData correlationData = new CorrelationData();
correlationData.setId(UUID.randomUUID().toString().replace("-", ""));
correlationData.setReturnedMessage(msg);
rabbitTemplate.convertAndSend(PublishConfirmConfig.CONFIRM_EXCHANGE_NAME, RabbitmqProperties.confirmRoutingKey + 1, message, correlationData);
}
}
package com.atguigu.rabbitmq.springbootrabbitmq.config.properties;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
/**
* User: ldj
* Date: 2023/3/12
* Time: 18:17
* Description: No Description
*/
@Data
@Component
@ConfigurationProperties("spring.rabbitmq.properties")
public class RabbitmqProperties {
private String normal1BingingKey;
private String normal2BingingKey;
private String normal3BingingKey;
private String delayedBingingKey;
private String deadLetterBingingKey;
private String confirmBingingKey;
private Integer normal1QueueTtl = 10000;
private Integer normal2QueueTtl = 30000;
public static String confirmRoutingKey;
public void setConfirmRoutingKey(String confirmRoutingKey) {
RabbitmqProperties.confirmRoutingKey = confirmRoutingKey;
}
}
测试发现消息是对象时接收到消息不是JSON串,属性是Date类型也没有格式化,所以配上消息转换配置
package com.atguli.common.config;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.module.SimpleModule;
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter;
import org.springframework.amqp.support.converter.MessageConverter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.text.SimpleDateFormat;
/**
* User: ldj
* Date: 2022/10/10
* Time: 17:07
* Description: rabbitmq序列化配置
*/
@Configuration
public class MyRabbitConfig {
@Bean
public MessageConverter messageConverter() {
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.setDateFormat(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
//Long -> String 解决返回前端id是Long类型精度降低,后位数变成0
SimpleModule simpleModule = new SimpleModule();
simpleModule.addSerializer(Long.TYPE, ToStringSerializer.instance);
simpleModule.addSerializer(Long.class, ToStringSerializer.instance);
objectMapper.registerModule(simpleModule);
//Include.NON_EMPTY 属性为 空("") 或者为 NULL 都不序列化,忽略该字段
objectMapper.setSerializationInclusion(JsonInclude.Include.NON_EMPTY);
// 允许出现单引号
objectMapper.configure(JsonParser.Feature.ALLOW_SINGLE_QUOTES, true);
return new Jackson2JsonMessageConverter(objectMapper);
}
}