1,初始MQ
MQ全称为Message Queue:消息队列。消息队列是在消息的传输过程中保存消息的容器。
它是典型的生产者,消费者模型。生产不断向消息队列中生产消息,消费者不断从队列中获取消息
因为消息的生产和消费是异步的,而且只关心消息的发送与接收,没有业务的侵入,解耦。
同步与异步
同步:指一个进程在执行某个请求时,若该请求需要一段时间才能返回,呢么该进程会一直等待,直到收到返回信息才会执行别的。事情必须一件一件做,前一件做完了才能做下一件。
例子:吃饭时候不能玩手机,吃完之后再去玩。
异步:指进程不需要一直等下去,继续执行下面操作,不管其他进程状态,当有消息返回时系统会通知进程进行处理,提高了执行的效率。
例子:边吃饭,边玩手机,边说话。
区别:请求发出后,是否需要等待响应,才能继续执行其他操作。
阻塞与非阻塞
阻塞:需要做一件事情能不能立即得到返回应答,如果不能立刻获得返回,需要等待,呢就阻塞了。进程或者线程就阻塞在哪里,不能做其他事情。
非阻塞:需要做一件事情能不能立即得到返回应答,如果不能立刻获得返回,需要等待,等待的时候还可以做其他事情,这是非阻塞。
概念
微服务:一个大项目拆分好的一个个小项目。
分布式:
分布式架构:有人管理拆分好的小项目
以购买商品为例: 用户支付成功后,需要调用订单服务完成订单状态修改,调用物流服务,从仓库分配响应的库存准备发货。
2,安装RabbitMQ
//1,下载镜像 在线拉取
docker pull rabbitmq:3-management
//本地下载之后 进行加载
docker load -i mq.tar
//2,安装
docker run \
-e RABBITMQ_DEFAULT_USER=root \
-e RABBITMQ_DEFAULT_PASS=123456 \
-v mq-plugins:/plugins \
--name mq \
--hostname zhaibx\
-p 15672:15672 \
-p 5672:5672 \
-d \
rabbitmq:3.9-management
MQ基本结构
3,RabbitMQ消息模型
环境搭建
Linux环境中安装rabbitMq 运行 rabbitMq 访问地址RabbitMQ Management进行登录,创建一个队列,生产消息与消费消息时需要该队列。
创建父子工程项目,用父工程依赖来管理子工程项目依赖,两个子工程 一个为Publisher生产消息,一个为Consumer消费消息。
1,基本消息队列
Publisher生产消息 消息队列 Consumer消费消息
写法1:原始项目写法
生产信息
package com.zbx.mq;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
/**
* @author zbx
* @date 2023/5/6&10:57
*/
public class FirstMQ {
public static void main(String[] args) throws IOException, TimeoutException {
/**
* 创建连接
* 隔离环境
* 获取连接
* 声明队列
* 发送消息
* 关闭连接和channel
*/
//1,创建连接
ConnectionFactory factory = new ConnectionFactory();
//隔离环境
factory.setVirtualHost("/");
factory.setUsername("root");
factory.setPassword("123456");
factory.setPort(5672);
factory.setHost("192.168.29.100");
//获取连接
Connection connection = factory.newConnection();
//获取通道
Channel channel = connection.createChannel();
//发送消息
for (int i = 0; i <100; i++) {
channel.basicPublish("","basic.queue",null,("张佳乐"+i).getBytes());
}
//释放资源
channel.close();
connection.close();
}
}
生产信息工程配置文件 application.yaml
spring:
rabbitmq:
host: 192.168.29.100
port: 5672
password: 123456
username: root
virtual-host: /
消费信息
package com.zbx.mq;
import com.rabbitmq.client.*;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
/**
* @author zbx
* @date 2023/5/6&11:13
*/
public class FirstMQ {
public static void main(String[] args) throws IOException, TimeoutException {
/**
* 创建连接
* 隔离环境
* 获取连接
* 声明队列
* 发送消息
* 关闭连接和channel
*/
//1,创建连接
ConnectionFactory factory = new ConnectionFactory();
//隔离环境
factory.setVirtualHost("/");
factory.setUsername("root");
factory.setPassword("123456");
factory.setPort(5672);
factory.setHost("192.168.29.100");
//获取连接
Connection connection = factory.newConnection();
//获取通道
Channel channel = connection.createChannel();
//消费消息
channel.basicConsume("basic.queue",true,new DefaultConsumer(channel){
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("body = " + new String(body));
}
});
//释放资源
/*channel.close();
connection.close();*/
}
}
消费信息配置文件application.yaml
spring:
rabbitmq:
host: 192.168.29.100
port: 5672
password: 123456
username: root
virtual-host: /
listener:
simple:
prefetch: 1
server:
port: 8081
首先执行生产消息类的main方法,在网站中查看队列中是否有消息产生。
然后再执行消费信息类的main方法,打印查看消费信息主体信息,在网站中查看队列消息是否被消费。
写法2:使用Spring提供的RabbitTemplate来完成消息生产与消费
二者application.yaml配置文件写法不变
生产消息:发送一百条消息
@SpringBootTest
public class TestPublisher {
@Autowired
private RabbitTemplate rabbitTemplate;
@Test
public void send(){
for (int i = 0; i <100; i++) {
//发送消息 队列名
rabbitTemplate.convertAndSend("basic.work.queue","xx"+i);
}
}
}
消费消息:
@Component
public class BasicQueueListener {
//使用Spring提供的rabbit监听器 监听队列
@RabbitListener(queues = "basic.queue")
public void receive(String message){
System.out.println("message = " + message);
}
}
使用Spring提供的rabbit模板 首先启动消费消息的项目,当队列有消息需要消费时,就会监听到,回调并打印输出。然后通过测试类来进行消息的生产。
2,工作消息队列
publisher生产消息 队列 多个消费消息
使用Spring提供的rabbit模板进行消息的生产与消费
生产消息
@SpringBootTest
public class TestPublisher {
@Autowired
private RabbitTemplate rabbitTemplate;
@Test
public void send(){
for (int i = 0; i <100; i++) {
rabbitTemplate.convertAndSend("basic.work.queue","xx"+i);
}
}
}
生产消息配置文件application.yaml
spring:
rabbitmq:
host: 192.168.29.100
port: 5672
password: 123456
username: root
virtual-host: /
消费消息
@Component
public class WorkQueueListener {
@RabbitListener(queues = "basic.work.queue")
public void receive1(String message) throws InterruptedException {
Thread.sleep(50);
System.out.println("receive1 = " + message);
}
@RabbitListener(queues = "basic.work.queue")
public void receive2(String message) throws InterruptedException {
Thread.sleep(500);
System.out.println("receive2 = " + message);
}
@RabbitListener(queues = "basic.work.queue")
public void receive3(String message) throws InterruptedException {
Thread.sleep(1000);
System.out.println("receive3 = " + message);
}
}
消费信息配置文件application.yaml
spring:
rabbitmq:
host: 192.168.29.100
port: 5672
password: 123456
username: root
virtual-host: /
server:
port: 8081
使用Spring提供的rabbit模板进行消息生产与消费。首先运行消费信息的项目,同时有三个消费进行对该队列的监听,一旦队列中有消息来进行消费。然后使用生产消息的测试类进行生产消息,发送到队列中,然后三个消费同时进行消费,同时控制台打印消费信息。
面试题:怎么从队列中获取消息
当队列中消息量大时,默认走一个预分配策略,默认为250条消息,且有多个消费者时,每人都分配250条消息进行消费。
当队列中消息量少时,会进行消息均分,每个消费者消费的消息量都一样。
这两种情况都存在一些的问题,消息生产出来后分发至不同的消费者,消息虽然在队列中,但是消息已经被消费者占用。如果这个时候消费出现异常,会导致这一片消息出问题。
由于每个消费者性能不一致,有的处理快,有的处理慢,所以用一种分配策略,能者多劳,消费者在队列中拿一条消息,消费一条,不进行预分配。
配置能者多劳
listener:
simple:
prefetch: 1 #每次只能获取一条消息,处理完成才能获取下一个消息
3,发布/订阅(交换机)
订阅模型中,多了一个exchange角色,而且过程略有变化:
交换机作用:接收生产者产生的消息,将消息传递到队列中。如果没有队列与交换机绑定,则消息会丢失。FanoutExchange会将消息路由到每个绑定的队列。
案例1:广播:将消息交给所有绑定到交换机的队列
1,声明交换机 队列 以及绑定交换机与队列的关系
package com.zbx.configuration;
import org.springframework.amqp.core.*;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* @author zbx
* @date 2023/5/6&19:56
* 定义交换机以及绑定交换机和队列
*/
@Configuration
public class FanoutBind {
//声明交换机
@Bean
public FanoutExchange fanoutExchange(){
return new FanoutExchange("fanout");
}
//声明队列1
@Bean
public Queue queue1(){
return new Queue("queue1");
}
//声明队列2
@Bean
public Queue queue2(){
return new Queue("queue2");
}
//交换机与队列绑定关系 通过监听器 监听所有的队列 加载队列
@Bean
//此时 参数中的 queue1 与 fanoutExchange 是从ioc容器中获取的 这两个名字一定要与上面声明队列与声明交换机的名字一致
//因为默认IOC是将方法名 转为对象名
public Binding binding1(Queue queue1,FanoutExchange fanoutExchange){
return BindingBuilder.bind(queue1).to(fanoutExchange);
}
//交换机与队列绑定关系 通过监听器 监听所有的队列 加载队列
@Bean
//此时 参数中的 queue1 与 fanoutExchange 是从ioc容器中获取的 这两个名字一定要与上面声明队列与声明交换机的名字一致
//因为默认IOC是将方法名 转为对象名
public Binding binding2(Queue queue2,FanoutExchange fanoutExchange){
return BindingBuilder.bind(queue2).to(fanoutExchange);
}
}
查看RabbitMQ Management网站中交换机是否声明出来,与队列的关系是否有绑定。
2,生产者生产消息
@SpringBootTest
public class FanoutTest {
@Autowired
private RabbitTemplate rabbitTemplate;
@Test
public void testFanout(){
for (int i = 0; i <10; i++) {
//发送信息 交换机名称 声明队列 不写代表广播 信息内容
rabbitTemplate.convertAndSend("fanout", "", "哈哈哈"+i);
}
}
}
3,消费者消费信息
@Component
public class FanoutListener {
@RabbitListener(queues = "queue1")
public void receive1(String message){
System.out.println("queue1 = " + message);
}
@RabbitListener(queues = "queue2")
public void receive2(String message){
System.out.println("queue2 = " + message);
}
}
//打印结果
queue2 = 哈哈哈
queue1 = 哈哈哈
监听这两个队列是否有消息,进行打印。
当两个队列,同时增加消费者时
当消费者多了时,队列只有两个时,只会选择其中一个接收。
因为广播是从交换机广播到队列而不是广播到消费者。。
@Component
public class FanoutListener {
@RabbitListener(queues = "queue1")
public void receive1(String message){
System.out.println("queue1 = " + message);
}
@RabbitListener(queues = "queue1")
public void receive11(String message){
System.out.println("queue11 = " + message);
}
@RabbitListener(queues = "queue2")
public void receive2(String message){
System.out.println("queue2 = " + message);
}
@RabbitListener(queues = "queue2")
public void receive22(String message){
System.out.println("queue22 = " + message);
}
}
//打印结果
queue2 = 哈哈哈0
queue1 = 哈哈哈0
queue2 = 哈哈哈1
queue2 = 哈哈哈2
queue1 = 哈哈哈1
queue1 = 哈哈哈2
queue2 = 哈哈哈3
queue1 = 哈哈哈3
queue2 = 哈哈哈4
queue1 = 哈哈哈4
queue2 = 哈哈哈5
queue1 = 哈哈哈5
queue2 = 哈哈哈6
queue2 = 哈哈哈7
queue1 = 哈哈哈6
queue2 = 哈哈哈8
queue1 = 哈哈哈7
queue2 = 哈哈哈9
queue1 = 哈哈哈8
queue1 = 哈哈哈9
声明队列Bean:Queue 交换机Bean:FanoutExchange 绑定关系Bean:Binding
案例2:Direct:通过指定路由key的方式,由路由向队列发送消息时并指定路由key,将消息发送到带有指定key的队列中。
生产者 交换机 路由key 队列 消费者
生产者:
@Test
public void testDirect(){
//发送信息 交换机名称 指定路由key名称 信息内容
rabbitTemplate.convertAndSend("fanout","xx","哈哈哈");
}
通过测试类方式,向交换机发送消息,由交换机发送消息到指定路由key队列。
消费者:
@Component
public class DirectListener {
//使用注解模式 对交换机与队列进行绑定 Direct Exchange:路由
@RabbitListener(bindings = @QueueBinding(
value = @Queue(name = "fanout.queue1"),
exchange = @Exchange(name ="fanout",type = ExchangeTypes.DIRECT),
key ="xx" //指定队列的路由key
))
public void receive1(String message) {
System.out.println("fanout.queue1.xx = " + message);
}
@RabbitListener(bindings = @QueueBinding(
value = @Queue(name = "fanout.queue2"),
exchange = @Exchange(name ="fanout",type = ExchangeTypes.DIRECT),
key = "ww" //指定队列的路由key
))
public void receive2(String message) {
System.out.println("fanout.queue2.ww = " + message);
}
}
//结果打印
fanout.queue1.xx = 哈哈哈
总结:交换机通过路由key方式,将消息发送到指定的路由key队列中,如果配置多个队列相同的路由key,则功能与广播相同。
案例3:Topic 使用通配符规则方式,由交换机将消息发送到不同队列。是Fanout与Direct的结合
通配符规则:#:匹配一个或多个词,*:匹配刚好一个词
举例:item.#:能匹配item.123与item.123.123
item.*:只能匹配item.123
生产者:
@Test
public void testTopic2(){
//发送信息 交换机名称 指定路由key名称 信息内容 输出结果
rabbitTemplate.convertAndSend("fanout","xx.123","哈哈哈");fanout.queue2.xx.# = 哈哈哈
rabbitTemplate.convertAndSend("fanout","123.xx","哈哈哈");fanout.queue1.#.xx = 哈哈哈
rabbitTemplate.convertAndSend("fanout","","哈哈哈");
rabbitTemplate.convertAndSend("fanout","123.xx","哈哈哈");fanout.queue1.#.xx = 哈哈哈
}
消费者:
@Component
public class TopicListener {
@RabbitListener(bindings = @QueueBinding(
value = @Queue(name = "fanout.queue1"),
exchange = @Exchange(name = "fanout",type = ExchangeTypes.TOPIC),
key = "#.xx"
))
public void receive1(String message){
System.out.println("fanout.queue1.#.xx = " + message);
}
@RabbitListener(bindings = @QueueBinding(
value = @Queue(name = "fanout.queue2"),
exchange = @Exchange(name = "fanout",type = ExchangeTypes.TOPIC),
key = "xx.*"
))
public void receive2(String message){
System.out.println("fanout.queue2.xx.# = " + message);
}
}
总结:Topic为路由key与广播的结合。不指定key为广播。
消息转化器
当消息发送时,发送对象类型,消费者监听到之后,将消息进行消费打印。消息在队列中为json类型,消费时打印接收为String类型,转换会出错。配置json转化器。
//依赖引入
<dependency>
<groupId>com.fasterxml.jackson.dataformat</groupId>
<artifactId>jackson-dataformat-xml</artifactId>
<version>2.9.10</version>
</dependency>
在项目的启动类下添加一个Bean即可。
@Bean
public MessageConverter jsonMessageConverter(){
return new Jackson2JsonMessageConverter();
}
4,消息可靠性
在消息的发送过程中,会有情况导致消息丢失。
生产者在发送过程中,消息未到达交换机。发送到交换机后,消息未发送到队列。
MQ宕机,队列将消息丢失。 消费者接收到消息后未消费就宕机。
生产者消息确认
spring:
rabbitmq:
publisher-confirm-type: correlated
publisher-returns: true
template:
mandatory: true
2,生产者编写
@Test
public void testTopic(){
Student student = new Student(1,"123","123");
//定义生产者消息确认异步回调对象
CorrelationData correlationData = new CorrelationData(UUID.randomUUID().toString());
//添加回调方法
correlationData.getFuture().addCallback(result -> {
//判断是否成功
if(result.isAck()){
//ack
log.info("消息投递成功,消息id{}",correlationData.getId());
}else{
//nack
log.warn("消息投递到交换机失败! 消息id{}",correlationData.getId());
}
},
ex -> {
log.error("消息发送失败!",ex);
});
//发送信息 交换机名称 声明路由key 信息内容 回调对象
rabbitTemplate.convertAndSend("fanout","",student,correlationData);
}
3,配置全局的消息失败Callback方法
@Log4j2
@Configuration
public class PublisherReturnConf implements ApplicationContextAware {
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
//获取rabbitTemplate对象
RabbitTemplate rabbitTemplate = applicationContext.getBean(RabbitTemplate.class);
//配置ReturnCallback 发送消息 是否路由 路由名称 路由key
rabbitTemplate.setReturnCallback((message, replyCode, replyText, exchange, routingKey) -> {
System.out.println("message = " + message);
System.out.println("getReplyCode = " + replyCode);
System.out.println("ReplyText = " + replyText);
System.out.println("Exchange = " + exchange);
System.out.println("RoutingKey = " + routingKey);
// 判断是否是延迟消息
log.error("消息发送到队列失败,响应码:{}, 失败原因:{}, 交换机: {}, 路由key:{},消息: {}",
replyCode, replyText, exchange, routingKey, message.toString());
// 判断是否是延迟消息
Integer receivedDelay = message.getMessageProperties().getReceivedDelay();
if (receivedDelay != null && receivedDelay > 0) {
// 是一个延迟消息,忽略这个错误提示
return;
}
});
}
}
4,打印结果
//正确发送消息到路由
2023-05-08 20:25:32.771 INFO 22924 --- [168.29.100:5672] com.zbx.FanoutTest : 消息投递成功,消息id51f2c748-44e8-468e-9abc-51bc8b500c54
//正确发送到交换机与队列 并消费
fanout.queue2.xx.# = Student(id=1, name=123, hobbies=123)
//路由发送消息到队列失败
2023-05-08 20:25:32.771 ERROR 22924 --- [nectionFactory1] com.zbx.config.PublisherReturnConf : 消息发送到队列失败,响应码:312, 失败原因:NO_ROUTE, 交换机: fanout, 路由key:,消息: (Body:'{"id":1,"name":"123","hobbies":"123"}' MessageProperties [headers={spring_returned_message_correlation=51f2c748-44e8-468e-9abc-51bc8b500c54, __TypeId__=com.zbx.pojo.Student}, contentType=application/json, contentEncoding=UTF-8, contentLength=0, receivedDeliveryMode=PERSISTENT, priority=0, deliveryTag=0])
交换机持久化
在查看交换机状态时,Features选中的D 为DURable持久的。
当我们自己创建交换机,并给他设置不持久化。
方式1:通过代码创建,通过SpringBoot启动加载到容器中
@Configuration
public class ExchangeEX {
@Bean
public TopicExchange topicExchange(){
//代码创建交换机 交换机名称 持久化 默认 true 自动删除 true
TopicExchange topicExchange = new TopicExchange("临时交换机",true,true);
return topicExchange;
}
}
方式2:在消费者中通过监听器创建交换机以及队列时进行配置
@Component
public class TopicListener {
@RabbitListener(bindings = @QueueBinding(
value = @Queue(name = "fanout.queue1"),
exchange = @Exchange(name = "fanout",type = ExchangeTypes.TOPIC,durable = "true",autoDelete = "true"),
key = "#.xx"
))
public void receive1(Student message){
System.out.println("fanout.queue1.#.xx = " + message.toString());
}
@RabbitListener(bindings = @QueueBinding(
value = @Queue(name = "fanout.queue2"),
exchange = @Exchange(name = "fanout",type = ExchangeTypes.TOPIC,durable = "true",autoDelete = "true"),
key = "xx.*"
))
public void receive2(Student message){
System.out.println("fanout.queue2.xx.# = " + message.toString());
}
}
我们创建的交换机,druable属性默认为true,为持久化的,注解也一样。
autoDelete表示当前交换机如果没有队列绑定是否自动删除,默认false为不删除。
不需要重启mq,设置了antoDelete ,只要解除了当前交换机所有队列的绑定,则交换机自动删除
队列持久化
代码创建队列
@Bean
public Queue queue(){
Queue queue = new Queue("队列",true,false,false);
return queue;
}
在消费者中通过监听器创建队列
@RabbitListener(bindings = @QueueBinding(
// 是否持久化队列 是否为排它队列 自动删除
value = @Queue(name = "fanout.queue1",durable = "false",exclusive = "false",autoDelete = "true"),
exchange = @Exchange(name = "fanout",type = ExchangeTypes.TOPIC,durable = "true",autoDelete = "true"),
key = "#.xx"
))
排它队列:当消费者与该队列进行绑定时,该队列只允许该消费者连接,第二次连接或者其他消费者连接时,不允许连接。
但是会导致原队列消息丢失。
消息持久化
在发送消息前对消息进行持久化配置
@Test
public void testTopic(){
Student student = new Student(1,"123","123");
//定义生产者消息确认异步回调对象
CorrelationData correlationData = new CorrelationData(UUID.randomUUID().toString());
//添加回调方法
correlationData.getFuture().addCallback(result -> {
//判断是否成功
if(result.isAck()){
//ack
log.info("消息投递成功,消息id{}",correlationData.getId());
}else{
//nack
log.warn("消息投递到交换机失败! 消息id{}",correlationData.getId());
}
},
ex -> {
log.error("消息发送失败!",ex);
});
String ms = "12312312313";
//持久化消息配置
Message message = MessageBuilder.withBody(ms.getBytes(StandardCharsets.UTF_8))
.setDeliveryMode(MessageDeliveryMode.NON_PERSISTENT)//不持久化
.setDeliveryMode(MessageDeliveryMode.PERSISTENT)//持久化
.setContentType(MessageProperties.CONTENT_TYPE_JSON)//Json传输
.build();
//发送信息 交换机名称 声明队列 不写代表广播 信息内容
rabbitTemplate.convertAndSend("fanout","xx.123",message,correlationData);
}
发送消息代码封装,方便与业务处理
@Log4j2
public class RabbitTemplateConfig {
// 外部传入 交换机名称 路由key 编号 信息
public void confirm(String exchange, String routingKey, String id,RabbitTemplate rabbitTemplate, String message){
//定义生产者消息确认异步回调对象
CorrelationData correlationData = new CorrelationData(id);
//添加回调方法
correlationData.getFuture().addCallback(result -> {
//判断是否成功
if(result.isAck()){
//ack
log.info("消息投递成功,消息id{}",id);
}else{
//nack
log.warn("消息投递到交换机失败! 消息id{}",id);
}
},
ex -> {
log.error("消息发送失败!",ex);
});
//持久化消息配置
Message ms = MessageBuilder.withBody(message.getBytes(StandardCharsets.UTF_8))
//.setDeliveryMode(MessageDeliveryMode.NON_PERSISTENT)//不持久化
.setDeliveryMode(MessageDeliveryMode.PERSISTENT)//持久化
.setContentType(MessageProperties.CONTENT_TYPE_JSON)//Json传输
.build();
//发送信息 交换机名称 声明队列 不写代表广播 信息内容
rabbitTemplate.convertAndSend(exchange,routingKey,ms,correlationData);
}
}
业务层调用
@Service
public class PubService {
@Autowired
private RabbitTemplate rabbitTemplate;
public void pay() {
RabbitTemplateConfig rabbitTemplateConfig = new RabbitTemplateConfig();
rabbitTemplateConfig.confirm("topic","", UUID.randomUUID().toString(),this.rabbitTemplate,"666666");
}
}
消费者信息确认
当消息在生产者方面以及交换机队列方面正常的话,如果消费者操作错误也会导致消息丢失。
listener:
simple:
prefetch: 1 #每次只能获取一条消息,处理完成才能获取下一个消息
acknowledge-mode: auto //自动ACK 自动监听代码是否有异常 返回ack与nack
消费失败重试机制
spring:
rabbitmq:
host: 192.168.29.100
port: 5672
password: 123456
username: root
virtual-host: fanout
listener:
simple:
prefetch: 1 #每次只能获取一条消息,处理完成才能获取下一个消息
acknowledge-mode: auto
retry:
enabled: true #开启重试机制
initial-interval: 1000
# 初始化的失败等待时长为1秒
multiplier: 2
#下次重试的时长倍数,下次等待时长 multiplier * last-interval111/24 8
max-attempts: 3
#最大重试次数
stateless: true # 无状态 false 有状态,如果业务中包含事务,必须是false这样在重试的时候保留事务不失效
消费者失败消息处理策略:将消息发送到专属的处理错误消息的交换机以及绑定的队列中。
由MessageRecoverer接口处理。
@Configuration
@Log4j2
public class DLXBindListener {
//绑定错误消息的交换机以及队列
@RabbitListener(
bindings = @QueueBinding(
value = @Queue(value = "error.queue"),
exchange =@Exchange(value = "error.direct",type =
ExchangeTypes.DIRECT),
key = {"error"}
))
//监听消费 错误队列中的消息
public void error(String message){
log.debug("叮,苦逼的运维你好,你有新的消息要人工处理了{}",message);
}
//消息处理
@Bean
public MessageRecoverer republishMessageRecoverer(RabbitTemplate rabbitTemplate){
return new RepublishMessageRecoverer(rabbitTemplate,"error.direct","error");
}
}
当异常消息时,默认是丢弃消息,我们通过MessageRecoverer将消息保存至处理异常的交换机队列中,由人工处理。
保证RabbitMQ消息可靠性:
5,死信交换机
当一个队列中消息出现以下情况时,成为死信
1,消息被消费者reject或者返回nack
2,消息超时未消费
3,队列满了
如果该队列中配置了dead-lletter-exchange属性,制定了一个交换机,呢么这个队列中的死信就会投递到这个交换机中,这个交换机称为死信交换机。
非注解形式创建
Queue queue = QueueBuilder.durable().deadLetterExchange("123123").deadLetterRoutingKey("ww").build();
注解形式创建
@RabbitListener(bindings = @QueueBinding(
value = @Queue(name = "topic.queue1",arguments = {
//配置死信交换机以及队列 和routing-key
@Argument(name = "x-dead-letter-exchange",value = "dlx"),
@Argument(name = "x-dead-letter-routing-key",value = "dlx.key")
}),
exchange = @Exchange(name = "topic",type = ExchangeTypes.TOPIC)
))
public void receive(String message){
System.out.println("topic.queue1 = " + message);
}
死信交换机实际就是一个普通定义的交换机和队列。只是在普通的队列创建时设置当消息出现错误时将消息投递到死信交换机。
TTL:消息存活时间,当一个队列中的消息TTL结束仍未消费,会变为死信。
给队列设置消息超时时间
@RabbitListener(bindings = @QueueBinding(
value = @Queue(name = "topic.queue1",arguments = {
//配置死信交换机以及队列 和routing-key
@Argument(name = "x-dead-letter-exchange",value = "dlx"),
@Argument(name = "x-dead-letter-routing-key",value = "dlx.key"),
//配置队列消息超时时间
@Argument(name = "x-message-ttl",value = "10000",type = "java.lang.Integer"),
}),
exchange = @Exchange(name = "topic",type = ExchangeTypes.TOPIC)
))
public void receive(String message){
System.out.println("topic.queue1 = " + message);
}
发送消息时给消息设置超时时间
@Log4j2
public class RabbitTemplateConfig {
// 外部传入 交换机名称 路由key 编号 信息
public void confirm(String exchange, String routingKey, String id,RabbitTemplate rabbitTemplate, String message){
//定义生产者消息确认异步回调对象
CorrelationData correlationData = new CorrelationData(id);
//添加回调方法
correlationData.getFuture().addCallback(result -> {
//判断是否成功
if(result.isAck()){
//ack
log.info("消息投递成功,消息id{}",id);
}else{
//nack
log.warn("消息投递到交换机失败! 消息id{}",id);
}
},
ex -> {
log.error("消息发送失败!",ex);
});
//持久化消息配置
Message ms = MessageBuilder.withBody(message.getBytes(StandardCharsets.UTF_8))
//.setDeliveryMode(MessageDeliveryMode.NON_PERSISTENT)//不持久化
.setDeliveryMode(MessageDeliveryMode.PERSISTENT)//持久化
.setContentType(MessageProperties.CONTENT_TYPE_JSON)//Json传输
.setExpiration("5000")//设置消息过期时间
.build();
//发送信息 交换机名称 声明队列 不写代表广播 信息内容
rabbitTemplate.convertAndSend(exchange,routingKey,ms,correlationData);
}
}
6,延迟队列
//1,查看卷
docker volume list
docker volume inspect mq-plugins
//2,打开挂载目录
"/var/lib/docker/volumes/mq-plugins/_data"
//3,将下载好的插件上传
//4,进入容器内部安装
docker exec -it mq bash
rabbitmq-plugins enable rabbitmq_delayed_message_exchange
@Configuration
@Log4j2
public class DelayExchange {
@RabbitListener(bindings = @QueueBinding(
value = @Queue(name = "delay.queue1"),
exchange = @Exchange(name = "delay.exchange",type = ExchangeTypes.TOPIC,
delayed = "true"),
key = "delay"
))
public void receive(String message){
System.out.println("收到延迟消息 = " + message);
}
}
发送消息设置延时
@Log4j2
public class RabbitTemplateConfig {
// 外部传入 交换机名称 路由key 编号 信息
public void confirm(String exchange, String routingKey, String id,RabbitTemplate rabbitTemplate, String message){
//定义生产者消息确认异步回调对象
CorrelationData correlationData = new CorrelationData(id);
//添加回调方法
correlationData.getFuture().addCallback(result -> {
//判断是否成功
if(result.isAck()){
//ack
log.info("消息投递成功,消息id{}",id);
}else{
//nack
log.warn("消息投递到交换机失败! 消息id{}",id);
}
},
ex -> {
log.error("消息发送失败!",ex);
});
//持久化消息配置
Message ms = MessageBuilder.withBody(message.getBytes(StandardCharsets.UTF_8))
//.setDeliveryMode(MessageDeliveryMode.NON_PERSISTENT)//不持久化
.setDeliveryMode(MessageDeliveryMode.PERSISTENT)//持久化
.setContentType(MessageProperties.CONTENT_TYPE_JSON)//Json传输
//.setExpiration("5000")//设置消息过期时间
.setHeader("x-delay",10000)//设置延时发送
.build();
//发送信息 交换机名称 声明队列 不写代表广播 信息内容
rabbitTemplate.convertAndSend(exchange,routingKey,ms,correlationData);
}
}
步骤:1,声明交换机 设置delayed属性为true
2,发送消息时,添加x-delay头,值为超时时间
7,消息堆积与惰性队列
消息堆积
解决消息堆积的思路1,增加更多消费者,提高消费速度。2,扩大队列溶剂,提高堆积上限。3,在消费者内开启线程池加快处理速度。
惰性队列
从RabbitMQ的3.6.0版本开始,增加了惰性队列概念
什么是惰性队列:
1,接收到消息之后直接存入本地磁盘而不是内存中。
2,消费者消费消息时,才会将消息从本地磁盘中读取加载到内存中。
3,支持百万条级别消息存储。
作用:解决消息堆积。
原始不配置惰性队列的话,队列中消息达到上限后,消息会丢弃,配置了惰性队列会往本地存数据。
基于命令行设置惰性队列
//先进入容器后
docker exec -it mq bash
//进入容器后 执行命令
rabbitmqctl set_policy Lazy "^lazy-queue$" '{"queue-mode":"lazy"}' --apply-to queues
rabbitmqctl : RabbitMQ 的命令行工具set_policy :添加一个策略Lazy :策略名称,可以自定义"^lazy-queue$" :用正则表达式匹配队列的名字'{"queue-mode":"lazy"}' :设置队列模式为 lazy 模式--apply-to queues :策略的作用对象,是所有的队列
使用注解设置惰性队列
@Configuration
@Log4j2
public class LazyListener {
@RabbitListener(bindings = @QueueBinding(
value = @Queue(name = "lazy.queue1",arguments = {
@Argument(name = "x-queue-mode",value = "lazy")
}),
exchange = @Exchange(name = "lazy.exchange",type = ExchangeTypes.TOPIC,
delayed = "true"),
key = "lazy"
))
public void receive(String message){
System.out.println("收到惰性队列消息 = " + message);
}
}
测试惰性队列
public String lazy() {
for (int i = 0; i <10000 ; i++) {
RabbitTemplateConfig rabbitTemplateConfig = new RabbitTemplateConfig();
rabbitTemplateConfig.confirm("lazy.exchange","lazy", UUID.randomUUID().toString(),this.rabbitTemplate,"lazylazylazy");
}
return "成功";
}
惰性队列的数据存在内存中的是非常少的,是 IO 出来准备消费的,其他的都在硬盘中存储,而普通队列所有信息都在内存中,及其消耗内存。
消息堆积问题的解决方案?1,队列上绑定多个消费者,提高消费速度2,使用惰性队列,可以再 mq 中保存更多消息
惰性队列的优点有哪些?1,基于磁盘存储,消息上限高2,没有间歇性的 page-out ,性能比较稳定
惰性队列的缺点有哪些?1,基于磁盘存储,消息时效性会降低2,性能受限于磁盘的 IO