目录
5.2.3 创建工具类连接RabbitMQ,连接工厂指定参数,新建连接来得
6.1.1 创建SpringBoot工程,名字叫springboot-rabbitmq,勾选web依赖
6.1.3 yml配置文件配置mq五大参数,除了hostip和端口,补上用户名密码和虚拟主机/test
6.1.4 Java配置类,声明exchange、queue,并且绑定它们
6.1.5 写一个测试类来模拟生产者发布消息到RabbitMQ,以后这个单元测试类是一个另一个发送消息的工程代码↓
6.2.1 yml文件配置手动ack,而不是默认的自动ack
6.2.2 代码手动ack,修改消费者的监听方法,增加client包的Channel接口和和amqp包的Message↓
7. SpringBoot实现confirm确认机制和return返回机制↓保证消息不丢失
7.1.1 编写配置文件,增加confirm和return↓
7.1.2 开启Confirm和Return的Java代码配置,核心用到了RabbitTemplate来搞定↓
7.2.4 修改消费者,利用redis和setnx方法的特点和id控制标记法,解决消息被消费者重复消费的问题
一、引言
模块之间的耦合度多高,导致一个模块宕机后,全部功能都不能用了,
并且同步通讯的成本过高,用户体验差。
二、RabbitMQ介绍
市面上比较火爆的几款MQ:
ActiveMQ,RocketMQ,Kafka,RabbitMQ。
语言的支持:ActiveMQ,RocketMQ只支持Java语言,Kafka可以支持多们语言,RabbitMQ支持多种语言。
效率方面:ActiveMQ,RocketMQ,Kafka效率都是毫秒级别,RabbitMQ是微秒级别的。
消息丢失,消息重复问题: RabbitMQ针对消息的持久化,和重复问题都有比较成熟的解决方案。
学习成本:RabbitMQ非常简单。
RabbitMQ是由Rabbit公司去研发和维护的,最终是在Pivotal。
RabbitMQ严格的遵循AMQP协议,高级消息队列协议,帮助我们在进程之间传递异步消息。
三、RabbitMQ安装
1.编写docker-compose.yml
version: "3.1"
services:
rabbitmq:
image: daocloud.io/library/rabbitmq:management
restart: always
container_name: rabbitmq
ports:
- 5672:5672
- 15672:15672
volumes:
- ./data:/var/lib/rabbitmq
2.创建mq容器,并且启动↓
[root@localhost ~]# cd /opt
[root@localhost opt]# mkdir docker_rabbitmq
[root@localhost opt]# cd docker_rabbitmq/
[root@localhost docker_rabbitmq]# vi docker-compose.yml
[root@localhost docker_rabbitmq]# docker-compose up -d
Creating rabbitmq ... done[root@localhost docker_rabbitmq]# docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
e319dc75bf44 daocloud.io/library/rabbitmq:management "docker-entrypoint.s…" 30 seconds ago Up 28 seconds 4369/tcp, 5671/tcp, 0.0.0.0:5672->5672/tcp, :::5672->5672/tcp, 15671/tcp, 15691-15692/tcp, 25672/tcp, 0.0.0.0:15672->15672/tcp, :::15672->15672/tcp rabbitmq
3.登录管理页面,用户名和密码默认都是guest↓
四、RabbitMQ架构
4.1 官方的简单架构图,巧记,pe,r,qc↓
Publisher - 生产者:发布消息到RabbitMQ中的Exchange
Consumer - 消费者:监听RabbitMQ中的Queue中的消息
Exchange - 交换机:和生产者建立连接并接收生产者的消息
Queue - 队列:Exchange会将消息分发到指定的Queue,Queue和消费者进行交互
Routes - 路由:交换机以什么样的策略将消息发布到Queue
一句话总结:生产者消息发布交给交换机,交换机把消息路由给队列,队列被消费者监听到有消息,消息被消费掉↑
4.2完整架构图
生产者和消费者都要先和虚拟主机Virtual Host建立连接,然后分别跟交换机和队列通过管道Channel来传输数据↑
五、RabbitMQ的使用
RabbitMQ的7种通讯方式↓
5.2 Java连接RabbitMQ
5.2.1 创建maven项目,名字叫rabbitmq
5.2.2 导入依赖
仓库https://mvnrepository.com/搜索amqp-client来下载java客户端依赖,版本一般找最多人用的即可↓
<dependencies>
<dependency>
<groupId>com.rabbitmq</groupId>
<artifactId>amqp-client</artifactId>
<version>5.6.0</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
</dependency>
</dependencies>
5.2.3 创建工具类连接RabbitMQ,连接工厂指定参数,新建连接来得
package com.zt.utils;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
public class RabbitMQClient {
public static Connection getConnection(){
// 创建Connection工厂
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("192.168.200.129");//这里要注意换成你自己虚拟机的ip,因为mq装在虚拟机上
factory.setPort(5672);//注意图形化界面端口才是15672
factory.setUsername("test");
factory.setPassword("test");
factory.setVirtualHost("/test");
// 创建Connection
Connection conn = null;
try {
conn = factory.newConnection();
} catch (Exception e) {
e.printStackTrace();
}
// 返回Connection
return conn;
}
}
5.2.4 测试连接
package com.zt.test;
import com.qf.utils.RabbitMQClient;
import com.rabbitmq.client.Connection;
import org.junit.Test;
import java.io.IOException;
public class TestConnection {
@Test
public void test1(){
Connection connection = RabbitMQClient.getConnection();
System.out.println(connection);//amqp://test@192.168.200.129:5672//test
try {
connection.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
5.3 Hello-World模式
一个生产者,一个默认的交换机,一个队列,一个消费者
创建生产者,创建一个channel,发布消息到exchange,指定路由规则
package com.zt.helloworld;
import com.zt.utils.RabbitMQClient;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import org.junit.Test;
public class Publisher {
@Test
public void publish() throws Exception {
//1. 获取Connection
Connection connection = RabbitMQClient.getConnection();
//2. 创建Channel
Channel channel = connection.createChannel();
//3. 发布消息到exchange,同时指定路由的规则
String msg = "Hello-World!";
// 参数1:指定exchange,使用"",使用默认的交换机
// 参数2:指定路由的规则,使用具体的队列名称。
// 参数3:指定传递的消息所携带的properties属性,暂时用null。
// 参数4:指定发布的具体消息,byte[]类型
channel.basicPublish("","HelloWorld",null,"hellomsg".getBytes());
// Ps:exchange是不会帮你将消息持久化到本地的,Queue才会帮你持久化消息。
System.out.println("生产者发布消息成功!");
//4. 释放资源
channel.close();
connection.close();
}
}
创建消费者,创建一个channel,创建一个队列,并且去消费当前队列
package com.zt.helloworld;
import com.zt.utils.RabbitMQClient;
import com.rabbitmq.client.*;
import org.junit.Test;
import java.io.IOException;
public class Consumer {
@Test
public void consume() throws Exception {
//1. 获取连接对象
Connection connection = RabbitMQClient.getConnection();
//2. 创建channel
Channel channel = connection.createChannel();
//3. 声明队列-HelloWorld
//参数1:queue - 指定队列的名称
//参数2:durable - 当前队列是否需要持久化(true,设置false后MQ重启后队列全部删除)
//参数3:exclusive - 是否排外(conn.close()当前队列会被自动删除,还有当前队列只能被一个消费者消费)
//参数4:autoDelete - 如果这个队列没有消费者在消费,队列自动删除
//参数5:arguments - 指定当前队列的其他信息
channel.queueDeclare("HelloWorld",true,false,false,null);
//4. 开启监听Queue
DefaultConsumer consume = new DefaultConsumer(channel){//重写方法做接收消息
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
System.out.println("接收到消息:" + new String(body,"UTF-8"));
}
};
//参数1:queue - 指定消费哪个队列
//参数2:autoAck - 指定是否自动ACK (true,接收到消息后,会立即告诉RabbitMQ,MQ就会进行删除)
//参数3:consumer - 指定消费回调
channel.basicConsume("HelloWorld",true,consume);
System.out.println("消费者开始监听队列!");
// System.in.read();
System.in.read();//让程序不停止,可以理解为死循环即可
//5. 释放资源
channel.close();
connection.close();
}
}
测试,先启动消费者监听,再启动生产者发布消息,然后消费者监听到消息并消费打印出来↓
5.4 Work
一个生产者,一个默认的交换机,一个队列,两个消费者
结构图
默认情况下,如有有两个消费者,生产者for循环生产10条消息,RabbitMQ平均分配,每个消费者接收5条↓
拷贝hellowork包的生产者到work包,简单修改为发布10条消息,修改队列名为Work即可↓
package com.zt.work;
import com.zt.utils.RabbitMQClient;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import org.junit.Test;
public class Publisher {
@Test
public void publish() throws Exception {
//1. 获取Connection
Connection connection = RabbitMQClient.getConnection();
//2. 创建Channel
Channel channel = connection.createChannel();
//3. 发布消息到exchange,同时指定路由的规则
//10.for
for (int i = 0; i < 10; i++) {
String msg = "Hello-World!"+ i;
channel.basicPublish("","Work",null,msg.getBytes());
}
System.out.println("生产者发布消息成功!");
//4. 释放资源
channel.close();
connection.close();
}
}
拷贝hellowork包的消费者到work包,修改打印信息为消费者1和队列为名Work即可↓
package com.zt.work;
import com.zt.utils.RabbitMQClient;
import com.rabbitmq.client.*;
import org.junit.Test;
import java.io.IOException;
public class Consumer1 {
@Test
public void consume() throws Exception {
//1. 获取连接对象
Connection connection = RabbitMQClient.getConnection();
//2. 创建channel
Channel channel = connection.createChannel();
//3. 声明队列-HelloWorld
channel.queueDeclare("Work",true,false,false,null);
//4. 开启监听Queue
DefaultConsumer consume = new DefaultConsumer(channel){//重写方法做接收消息
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
System.out.println("消费者1号接收到消息:" + new String(body,"UTF-8"));
}
};
channel.basicConsume("Work",true,consume);
System.out.println("消费者开始监听队列!");
// System.in.read();
System.in.read();//让程序不停止,只有键盘录入数据才会往下走
//5. 释放资源
channel.close();
connection.close();
}
}
拷贝hellowork包的消费者到work包,修改打印信息为消费者2和队列为名Work即可↓
package com.zt.work;
import com.zt.utils.RabbitMQClient;
import com.rabbitmq.client.*;
import org.junit.Test;
import java.io.IOException;
public class Consumer2 {
@Test
public void consume() throws Exception {
//1. 获取连接对象
Connection connection = RabbitMQClient.getConnection();
//2. 创建channel
Channel channel = connection.createChannel();
//3. 声明队列-HelloWorld
channel.queueDeclare("Work",true,false,false,null);
//4. 开启监听Queue
DefaultConsumer consume = new DefaultConsumer(channel){//重写方法做接收消息
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
System.out.println("消费者2号接收到消息:" + new String(body,"UTF-8"));
}
};
channel.basicConsume("Work",true,consume);
System.out.println("消费者开始监听队列!");
// System.in.read();
System.in.read();//让程序不停止,只有键盘录入数据才会往下走
//5. 释放资源
channel.close();
connection.close();
}
}
测试效果,先分别启动两个消费者,然后再启动生产者,确实看到消费者平均了,每人消费5条消息↓
不想默认的消息平均分配,只需要在消费者端,添加Qos能力以及更改为手动ack即可让消费者,根据自己的能力去消费指定的消息,生产者不变
5.5 Publish/Subscribe,广播模式
结构图
声明一个Fanout类型的exchange,并且将exchange和queue绑定在一起,绑定的方式就是直接绑定。让生产者创建一个exchange并且指定类型,和一个或多个队列绑定到一起。
//3. 创建exchange - 绑定某一个队列
//参数1: exchange的名称
//参数2: 指定exchange的类型 FANOUT - pubsub方式用 , DIRECT - Routing方式用 , TOPIC - Topics方式用
channel.exchangeDeclare("pubsub-exchange", BuiltinExchangeType.FANOUT);//FANOUT扇形广播
channel.queueBind("pubsub-queue1","pubsub-exchange","");
channel.queueBind("pubsub-queue2","pubsub-exchange","");
新建一个publish包,拷贝生产者和消费者过来修改,生产者修改一下交换机和绑定队列名↓
package com.zt.publish;
import com.zt.utils.RabbitMQClient;
import com.rabbitmq.client.BuiltinExchangeType;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import org.junit.Test;
public class Publisher {
@Test
public void publish() throws Exception {
//1. 获取Connection
Connection connection = RabbitMQClient.getConnection();
//2. 创建Channel
Channel channel = connection.createChannel();
//增加的部分↓
//3. 创建exchange - 绑定某一个队列
//参数1: exchange的名称
//参数2: 指定exchange的类型 FANOUT - pubsub方式用 , DIRECT - Routing方式用 , TOPIC - Topics方式用
channel.exchangeDeclare("pubsub-exchange", BuiltinExchangeType.FANOUT);
channel.queueBind("pubsub-queue1","pubsub-exchange","");
channel.queueBind("pubsub-queue2","pubsub-exchange","");
//增加的部分↑
//3. 发布消息到exchange,同时指定路由的规则
//10.for
for (int i = 0; i < 10; i++) {
String msg = "Hello-World!"+ i;
channel.basicPublish("pubsub-exchange","Work",null,msg.getBytes());//
}
System.out.println("生产者发布消息成功!");
//4. 释放资源
channel.close();
connection.close();
}
}
消费者把之前的队列名改成跟生产者绑定的一致即可↓
测试结果,生产者生产10条消息,两个消费者都能收到10条消息↓
5.6 Routing
一个生产者,一个交换机,两个队列,两个消费者
结构图
生产者在创建DIRECT类型的exchange后,根据RoutingKey去绑定相应的队列,并且在发送消息时,指定消息的具体RoutingKey即可。
//3. 创建exchange, routing-queue-error,routing-queue-info,
channel.exchangeDeclare("routing-exchange", BuiltinExchangeType.DIRECT);//DIRECT定向
channel.queueBind("routing-queue-error","routing-exchange","ERROR");
channel.queueBind("routing-queue-info","routing-exchange","INFO");
//4. 发布消息到exchange,同时指定路由的规则
channel.basicPublish("routing-exchange","ERROR",null,"ERROR".getBytes());
channel.basicPublish("routing-exchange","INFO",null,"INFO1".getBytes());
channel.basicPublish("routing-exchange","INFO",null,"INFO2".getBytes());
channel.basicPublish("routing-exchange","INFO",null,"INFO3".getBytes());
消费者没有变化
新建一个routing包,拷贝生产者和消费者过来修改,生产者修改一下交换机和绑定队列名和路由键名字↓
package com.zt.routing;
import com.zt.utils.RabbitMQClient;
import com.rabbitmq.client.BuiltinExchangeType;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import org.junit.Test;
public class Publisher {
@Test
public void publish() throws Exception {
//1. 获取Connection
Connection connection = RabbitMQClient.getConnection();
//2. 创建Channel
Channel channel = connection.createChannel();
//增加的部分↓
//3. 创建exchange, routing-queue-error,routing-queue-info,
channel.exchangeDeclare("routing-exchange", BuiltinExchangeType.DIRECT);
channel.queueBind("routing-queue-error","routing-exchange","ERROR");
channel.queueBind("routing-queue-info","routing-exchange","INFO");
//4. 发布消息到exchange,同时指定路由的规则
channel.basicPublish("routing-exchange","ERROR",null,"ERROR".getBytes());
channel.basicPublish("routing-exchange","INFO",null,"INFO1".getBytes());
channel.basicPublish("routing-exchange","INFO",null,"INFO2".getBytes());
channel.basicPublish("routing-exchange","INFO",null,"INFO3".getBytes());
//增加的部分↑
System.out.println("生产者发布消息成功!");
//4. 释放资源
channel.close();
connection.close();
}
}
消费者把之前的队列名改成跟生产者绑定的一致即可↓
测试结果,生产者生产的路由键对应的消息,对应的路由键的队列都能给对应的消费者消费↓
5.7 Topic,巧记,话题,啥啥话题都可以通通匹配
一个生产者,一个交换机,两个队列,两个消费者
结构图
生产者创建Topic的exchange并且绑定到队列中,这次绑定可以通过*和#关键字,对指定RoutingKey内容,编写时注意格式 xxx.xxx.xxx去编写, * 代表一个xxx,而#代表多个xxx.xxx,在发送消息时,指定具体的RoutingKey到底是什么。说白了就是匹配一个单词的情况,一般使用#号匹配0个或者多个单词,比如red是一个单词,这里说的是单词而不是字母!!!
生产者↓
package com.zt.topic;
import com.zt.utils.RabbitMQClient;
import com.rabbitmq.client.BuiltinExchangeType;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import org.junit.Test;
public class Publisher {
@Test
public void publish() throws Exception {
//1. 获取Connection
Connection connection = RabbitMQClient.getConnection();
//2. 创建Channel
Channel channel = connection.createChannel();
//增加的部分↓
//2. 创建exchange并指定绑定方式,巧记,闪耀明星只有一个,落井下石太多了↓
channel.exchangeDeclare("topic-exchange", BuiltinExchangeType.TOPIC);
channel.queueBind("topic-queue-1","topic-exchange","*.red.*");
channel.queueBind("topic-queue-2","topic-exchange","fast.#");
channel.queueBind("topic-queue-2","topic-exchange","*.*.rabbit");
//3. 发布消息到exchange,同时指定路由的规则
channel.basicPublish("topic-exchange","fast.red.monkey",null,"红快猴子".getBytes());
channel.basicPublish("topic-exchange","slow.black.dog",null,"黑漫狗".getBytes());
channel.basicPublish("topic-exchange","fast.white.cat",null,"快白猫".getBytes());
//增加的部分↑
System.out.println("生产者发布消息成功!");
//4. 释放资源
channel.close();
connection.close();
}
}
消费者只是监听队列,没变化。
消费者把之前的队列名改成跟生产者绑定的一致即可↓
结果如下↓
1)Topic模式是最常用的模式,灵活,方便,强大;
2)使用Topic模式生产者在声明队列时需要制定消息到达队列方式为topic;
3)路由键和某模式匹配,主要有两种模糊匹配:
*匹配一个单词的情况,一般使用#号匹配0个或者多个单词,巧记,闪耀明星只有一个,落井下石太多了↓
六、RabbitMQ整合SpringBoot
6.1 SpringBoot整合RabbitMQ↓
6.1.1 创建SpringBoot工程,名字叫springboot-rabbitmq,勾选web依赖
6.1.2 导入mq依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
6.1.3 yml配置文件配置mq五大参数,除了hostip和端口,补上用户名密码和虚拟主机/test
spring:
rabbitmq:
host: 192.168.200.129
port: 5672
username: test
password: test
virtual-host: /test
6.1.4 Java配置类,声明exchange、queue,并且绑定它们
package com.zt.config;
import org.springframework.amqp.core.Binding;
import org.springframework.amqp.core.BindingBuilder;
import org.springframework.amqp.core.Queue;
import org.springframework.amqp.core.TopicExchange;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class RabbitMQConfig {
//1. 创建exchange - topic
@Bean
public TopicExchange getTopicExchange(){
return new TopicExchange("boot-topic-exchange",true,false);
}
//2. 创建queue
@Bean
public Queue getQueue(){
return new Queue("boot-queue",true,false,false,null);//建议交换机和队列构造都选最长的,持久化赋值true,不认识给默认值
}
//3. 绑定在一起
@Bean
public Binding getBinding(TopicExchange topicExchange, Queue queue){
return BindingBuilder.bind(queue).to(topicExchange).with("*.red.*");
}
//4. 上面要注意的是交换机的名字和队列的名字不能是topic-exchange和topic-queue否则跟内部冲突启动失败
}
6.1.5 写一个测试类来模拟生产者发布消息到RabbitMQ,以后这个单元测试类是一个另一个发送消息的工程代码↓
package com.zt;
import org.junit.jupiter.api.Test;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest
class SpringbootRabbitmqApplicationTests {
@Autowired
private RabbitTemplate rabbitTemplate;
@Test
void contextLoads() {
rabbitTemplate.convertAndSend("boot-topic-exchange","slow.red.dog","红色大狼狗!!");
}
}
6.1.6 创建消费者监听消息
package com.zt.listen;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;
@Component
public class Consumer {
@RabbitListener(queues = "boot-queue")
public void getMessage(Object message){
System.out.println("接收到消息:" + message);
}
}
先启动应用程序监听消息,然后启动单元测试类发消息,控制台输出监听接收到的消息,说明整合成功↓
6.2 手动Ack,yml配置增加和java监听增加↓
执行一个任务可能需要花费几秒钟,你可能会担心如果一个消费者在执行任务过程中挂掉了。一旦RabbitMQ将消息分发给了消费者,就会从内存中删除。在这种情况下,如果正在执行任务的消费者宕机,会丢失正在处理的消息和分发给这个消费者但尚未处理的消息。
但是,我们不想丢失任何任务,如果有一个消费者挂掉了,那么我们应该将分发给它的任务交付给另一个消费者去处理。
为了确保消息不会丢失,RabbitMQ支持消息应答。消费者发送一个消息应答,告诉RabbitMQ这个消息已经接收并且处理完毕了。RabbitMQ就可以删除它了。
如果一个消费者挂掉却没有发送应答,RabbitMQ会理解为这个消息没有处理完成,然后交给另一个消费者去重新处理。这样,你就可以确认即使消费者偶尔挂掉也不会丢失任何消息了。
没有任何消息超时限制;只有当消费者挂掉时,RabbitMQ才会重新投递。即使处理一条消息会花费很长的时间。
消息应答是默认打开的。我们通过显示的设置autoAsk=true可关闭这种机制。现即自动应答开,一旦我们完成任务,消费者会自动发送应答。通知RabbitMQ消息已被处理,可以从内存删除。如果消费者因宕机或链接失败等原因没有发送ACK(不同于ActiveMQ,在RabbitMQ里,消息没有过期的概念),则RabbitMQ会将消息重新发送给其他监听队列的下一个消费者。
6.2.1 yml文件配置手动ack,而不是默认的自动ack
# 记不住就抄,或者打个ack选择简单哪个即可↓
spring:
rabbitmq:
host: 192.168.200.129
port: 5672
username: test
password: test
virtual-host: /test
listener:
simple:
acknowledge-mode: manual
6.2.2 代码手动ack,修改消费者的监听方法,增加client包的Channel接口和和amqp包的Message↓
package com.zt.listen;
import com.rabbitmq.client.Channel;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;
import java.io.IOException;
@Component
public class Consumer {
@RabbitListener(queues = "boot-queue")
public void getMessage(String msg, Channel channel, Message message) throws IOException {
System.out.println("接收到消息:" + msg);
//int i = 1 / 0;
//手动ack
//channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);
}
}
为啥要改为手动ack?因为如果是自动ack,消费者即使出了异常,没有正常完成任务,mq也收到自动应答表示完成了,就删除mq中的消息,但是如果改为手动ack配置,当消费者出现异常就中断了,没有走后面手动ack的代码,就没有正确应答,mq不会把消息删除,如果消费者没有出现异常,即调用手动ack代码,给mq应答正常,删除消费消息
7. SpringBoot实现confirm确认机制和return返回机制↓保证消息不丢失
7.1.1 编写配置文件,增加confirm和return↓
spring:
rabbitmq:
host: 192.168.200.129
port: 5672
username: test
password: test
virtual-host: /test
listener:
simple:
acknowledge-mode: manual
publisher-confirm-type: simple
publisher-returns: true
7.1.2 开启Confirm和Return的Java代码配置,核心用到了RabbitTemplate来搞定↓
package com.zt.config;
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.stereotype.Component;
import javax.annotation.PostConstruct;
@Component
public class PublisherConfirmAndReturnConfig implements RabbitTemplate.ConfirmCallback ,RabbitTemplate.ReturnCallback {
@Autowired
private RabbitTemplate rabbitTemplate;
@PostConstruct//构造本类对象后,调用初始化方法,设置回调来触发下面确认方法和返回方法的调用↓
public void initMethod(){
rabbitTemplate.setConfirmCallback(this);
rabbitTemplate.setReturnCallback(this);
}
@Override
public void confirm(CorrelationData correlationData, boolean ack, String cause) {
if(ack){
System.out.println("消息已经送达到Exchange");
}else{
System.out.println("消息没有送达到Exchange");
}
}
@Override
public void returnedMessage(Message message, int replyCode, String replyText, String exchange, String routingKey) {
System.out.println("消息没有送达到Queue");
}
}
测试流程和结果如下↓
增加yml文件配置修改和java代码配置增加↓
生产者增加System.in.read()来不停止好不断观察效果,消费者无需改动↓
生产者故意设置不对的路由键找不到对应的队列来触发返回机制回调↓
生产者改动如下↓
package com.zt;
import org.junit.jupiter.api.Test;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import java.io.IOException;
@SpringBootTest
class SpringbootRabbitmqApplicationTests {
@Autowired
private RabbitTemplate rabbitTemplate;
@Test
void contextLoads() throws IOException {
//rabbitTemplate.convertAndSend("boot-topic-exchange",
// "slow.red.dog","红色大狼狗!!");
//生产者增加System.in.read()来不停止好不断观察效果,消费者无需改动↓
//System.in.read();
//生产者故意设置不对的路由键找不到对应的队列来触发返回机制回调↓
rabbitTemplate.convertAndSend("boot-topic-exchange",
"slow.white.dog","红色大狼狗!!");
//生产者增加System.in.read()来不停止好不断观察效果,消费者无需改动↓
System.in.read();
}
}
消费者不变↓
package com.zt.listen;
import com.rabbitmq.client.Channel;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;
import java.io.IOException;
@Component
public class Consumer {
@RabbitListener(queues = "boot-queue")
public void getMessage(String msg, Channel channel, Message message) throws IOException {
System.out.println("接收到消息:" + msg);
//int i = 1 / 0;
//手动ack
channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);
}
}
原来的连接Java配置类不变↓
package com.zt.config;
import org.springframework.amqp.core.Binding;
import org.springframework.amqp.core.BindingBuilder;
import org.springframework.amqp.core.Queue;
import org.springframework.amqp.core.TopicExchange;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class RabbitMQConfig {
//1. 创建exchange - topic
@Bean
public TopicExchange getTopicExchange(){
return new TopicExchange("boot-topic-exchange",true,false);
}
//2. 创建queue
@Bean
public Queue getQueue(){
return new Queue("boot-queue",true,false,false,null);
}
//3. 绑定在一起
@Bean
public Binding getBinding(TopicExchange topicExchange, Queue queue){
return BindingBuilder.bind(queue).to(topicExchange).with("*.red.*");
}
}
7.2 避免消息重复消费↓
重复消费消息的原因是,消费者没有给RabbitMQ一个ack↓
重复消费消息,会对非幂等行操作造成问题↓
幂等性操作:比如数据库删除,操作一次和多次效果是一样的。
非幂等性操作:比如添加,而且主键是自增的。
为了解决消息重复消费的问题,可以采用Redis,在消费者消费消息之前,先将消息的id放到Redis中,比如
id为0表示(正在执行业务)
id为1表示(执行业务成功)
如果ack失败,在RabbitMQ将消息交给其他的消费者时,先执行setnx,如果key已经存在,获取他的值,如果是0,当前消费者就什么都不做,如果是1,直接ack。
极端情况:第一个消费者在执行业务时,出现了死锁,在setnx的基础上,再给key设置一个生存时间。
7.2.1 导入data-redis依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
7.2.2 编写配置文件,增加redis的ip和端口配置
spring:
rabbitmq:
host: 192.168.200.129
port: 5672
username: test
password: test
virtual-host: /test
listener:
simple:
acknowledge-mode: manual
publisher-confirm-type: simple
publisher-returns: true
redis:
host: 192.168.200.129
port: 6379
7.2.3 修改生产者
@Test
void contextLoads() throws IOException {
CorrelationData messageId = new CorrelationData(UUID.randomUUID().toString());//data,id的d巧记
rabbitTemplate.convertAndSend("boot-topic-exchange","slow.red.dog","红色大狼狗!!",messageId);//增加一个data即封装id的对象,方法重载
System.in.read();
}
7.2.4 修改消费者,利用redis和setnx方法的特点和id控制标记法,解决消息被消费者重复消费的问题
@Autowired
private StringRedisTemplate redisTemplate;//换类StringRedisTemplate是RedisTemplate的子类,更加强大
@RabbitListener(queues = "boot-queue")
public void getMessage(String msg, Channel channel, Message message) throws IOException {
//0. 通过消息属性的头,或者spring被动返回的消息相关,数据比如获取MessageId
String messageId = message.getMessageProperties().getHeader("spring_returned_message_correlation");
//1. 设置key到Redis
if(redisTemplate.opsForValue().setIfAbsent(messageId,"0",10, TimeUnit.SECONDS)) {//原来是10秒我改为1000秒测试redis中是否有消费完消息后id为1的数据,设置为10秒太快消失不便于下面的测试查看
//2. 消费消息,暂时先打印消息来模拟消费,至于消息以后用来干啥得看具体的需求↓
System.out.println("接收到消息:" + msg);
//3. 设置key的value为1
redisTemplate.opsForValue().set(messageId,"1",10,TimeUnit.SECONDS);
//4. 手动ack
channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);
}else {
//5. 获取Redis中的value即可 如果是1,手动ack
if("1".equalsIgnoreCase(redisTemplate.opsForValue().get(messageId))){
channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);
}
}
}
这样消息就不会被重复消费了