消息中间件之RabbitMQ
1、消息中间件概述
1.1 什么是消息中间件
MQ:全称为Message Queue,消息队列是应用程序和应用程序之间的通信方法。
-
为什么使用MQ?
在项目中,可将一些无需即使返回且耗时的操作提取出来,进行异步处理,而这种异步处理的方式大大节省了服务器的请求的响应时间,从而提高了系统的吞吐量
-
开发中消息队列通常有如下应用场景
1、任务异步处理
将不需要同步处理的并且耗时长的操作由消息队列通知消息接收方进行异步处理。提高了应用程序的响应时间。
2、应用程序解耦合
MQ相当于一个中介,生产方通过MQ与消费方交互,它将应用程序进行解耦合
3、削峰填谷
如订单系统,在下单的时候就会往数据库写数据,但是数据库只能支撑每秒1000左右的并发写入,并发量再高就容易宕机。低峰期的时候并发也就100多个,但是在高峰期的时候,并发量会突然激增到5000以上,这个时候数据库肯定就容易卡死。
-
消息被MQ保存起来了,然后系统就可以按照自己的消费能力来消费,比如每秒1000个数据,这样慢慢写入数据库,这样就不会卡死数据库了。
但是使用了MQ以后呢,限制消费信息的速度为1000,但是这样一来,高峰期产生的数据势必被积压在MQ中,高峰就被削掉了。但是因为消息积压,在高峰期过去的一段时间内,消费信息的速度还是会维持在1000QPS(每秒查询率),直到消费积压得的消息,这就叫做“填谷”。
1.2 AMQP和JMS
MQ是消息通信的模型,实现MQ的方式大致有2种 :AMQP和 JMS 。
1.2.1 AMQP
AMQP(高级消息队列协议)是一种协议,更准确的来说是一种 binary wire-level protocol(链接协议)。这是其和 JMS 的本质区别,AMQP不从API层进行限定(使用什么语言都无所谓),而是直接定义网络交换的数据格式(就可以进行通信)。
1.2.2 JMS
JMS即Java消息服务(JavaMessage Service)应用程序接口,是一个Java平台关于面向消息中间件(MOM)的API,用在两个应用程序之间,或分布式系统之间发送消息,进行异步通信。
1.2.3 AMQP和JMS的区别
-
JMS是定义了统一接口,来对消息操作进行统一;AMQP是通过规定协议来统一数据交互的格式
-
JMS限定了必须使用Java语言;AMQP只是一种协议,不规定实现的方式,因此是跨语言的。
-
JMS规定了两种消息模式;而AMQP的消息模式更加的丰富
1.3 消息队列产品
-
ActiveMQ:基于JMS
-
ZeroMQ:基于C语言开发
-
RabbitMQ:基于AMQP协议,erlang语言开发,稳定性好
-
RocketMQ:基于JMS 阿里巴巴产品
-
Kafka:类型MQ的产品,分布式消息系统,高吞吐量(大数据方向用的比较多)
1.4 RabbitMQ
RabbitMQ是erlang语言开发,基于AMQP(Advanced Message Queue 高级消息队列协议)实现的消息队列,它是一种应用程序之间的通信方法,消息队列在分布式系统开发中应用非常广泛。
RabbitMQ提供了6种模式,简单模式,work模式,Public/Subscribe发布与订阅模式,Routing路由模式,Topics主题模式,RPC远程调用模式(远程调用,不太算MQ)
官网模式介绍:https://www.rabbitmq.com/getstarted.html
2、安装跟配置RabbitMQ
1、安装Socat
yum install socat -y
2、安装Erlang
mkdir /rabbitmq && cd /rabbitmq
# 上传 erlang-22.0.7-1.el7.x86_64.rpm 安装包上传
# 安装
rpm -ivh erlang-22.0.7-1.el7.x86_64.rpm
3、安装RabbitMQ
cd /rabbitmq
# 上传 rabbitmq-server-3.7.17-1.el7.noarch.rpm 安装包
上传
# 安装
rpm -ivh rabbitmq-server-3.7.17-1.el7.noarch.rpm
4、开启权限管理界面
# 开启管理界面
rabbitmq-plugins enable rabbitmq_management
# 配置远程可使用guest登录mq
cd /usr/share/doc/rabbitmq-server-3.7.17
cp rabbitmq.config.example /etc/rabbitmq/rabbitmq.config
# 修改配置文件
vi /etc/rabbitmq/rabbitmq.config
修改/etc/rabbitmq/rabbitmq.config
配置文件:
5、启动
systemctl start rabbitmq-server
6、配置
RabbitMQ在安装好后,可以访问http://ip地址:15672
;其自带了guest/guest的用户名和密码;如果需要创建自定义用户;那么也可以登录管理界面后,如下操作:
角色说明:
1、 超级管理员(administrator)
可登陆管理控制台,可查看所有的信息,并且可以对用户,策略(policy)进行操作。
2、 监控者(monitoring)
可登陆管理控制台,同时可以查看rabbitmq节点的相关信息(进程数,内存使用情况,磁盘使用情况等)
3、 策略制定者(policymaker)
可登陆管理控制台, 同时可以对policy进行管理。但无法查看节点的相关信息(上图红框标识的部分)。
4、 普通管理者(management)
仅可登陆管理控制台,无法看到节点信息,也无法对策略进行管理。
5、 其他
无法登陆管理控制台,通常就是普通的生产者和消费者。
3、RabbitMQ简单架构
3.1 官方简单架构图
-
Publisher - 生产者 (发布消息到RabbitMQ中的Exchange)
-
Consumer - 消费者(拿到发布的消息,监听RabbitMQ中的Queue的消息)
-
Exchange - 交换机(和生产者建立连接,拿到生产者的消息)
-
Queue - 队列(交换机把消息以一定的策略放在队列中,Queue和消费者进行交互)
-
Routes - 路由(交换机以什么样的策略将消息发布到Queue中)
3.2 RabbitMQ的完整架构图
3.3 查看图形化界面并创建一个Virtual Host
创建一个全新的用户和全新的Virtual Host ,并且将在test用户设置上可以操作/test权限
4、RabbitMQ的使用
4.1 RabbitMQ的通讯方式
4.2 Java去连接RabbitMQ
1、创建Maven项目
2、导入依赖
<!-- https://mvnrepository.com/artifact/com.rabbitmq/amqp-client -->
<dependency>
<groupId>com.rabbitmq</groupId>
<artifactId>amqp-client</artifactId>
<version>5.8.0</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
</dependency>
3、创建工具类连接RabbitMQ
//获取连接
public static Connection getConnection() {
//通过ConnectionFactory 来创建Connection
ConnectionFactory connectionFactory = new ConnectionFactory();
//设置ConnectionFactory的一些属性
connectionFactory.setHost("192.168.65.128");
connectionFactory.setVirtualHost("/test");
connectionFactory.setUsername("test");
connectionFactory.setPassword("test");
connectionFactory.setPort(5672);//这是RabbitMQ服务的端口,15672是图形界面的端口
Connection connection = null;
try {
connection = connectionFactory.newConnection();
} catch (IOException e) {
e.printStackTrace();
} catch (TimeoutException e) {
e.printStackTrace();
}
//返回拿到的Connection
return connection;
}
4.3 Hello-World模式
一个生产者,一个默认的交换机,一个队列,一个消费者
1、创建生产者,创建一个Channel,发布消息到exchange,指定路由规则
@Test
public void publish() throws Exception {
//1.获取连接Connection
Connection connection = RabbitMQClient.getConnection();
//2.创建Channel
Channel channel = connection.createChannel();
String msg = "Hello-World";
//3.发布消息到exchange,同时指定路由的规则
//参数1:指定exchange,使用""
//参数2:指定路由规则,使用具体的队列名称。
//参数3:指定传递的消息所携带的properties ,使用NULL
//参数4:指定发布的具体消息,byte[] 类型
channel.basicPublish("","HelloWorld",null,msg.getBytes());
//ps:exchange 是不会帮你把消息持久化到本地的,Queue才会帮你将消息持久化
System.out.println("生产者发布消息成功!");
//4.释放资源
channel.close();
connection.close();
}
2、创建消费者,创建一个Channel,创建一个队列,并且去消费当前队列
@Test
public void consume() throws Exception {
//1.获取连接
Connection connection = RabbitMQClient.getConnection();
//2.创建Channel
Channel channel = connection.createChannel();
//3.声明队列-HelloWorld 要和生产者 使用的队列名字相同
//参数1:queue - 指定队列名称
//参数2:durable - 当前队列是否需要持久化(true 为需要持久化)
//参数3:exclusive - 是否排外(conn.close()当前队列会被自动删除,当前队列只能被一个消费者消费)
//参数4:autoDelete - 如果这个队列没有消费者在消费,那么这个队列自动被删除
//参数5:arguments - 指定当前队列的其他信息
channel.queueDeclare("HelloWorld",true,false,false,null);
//4.开启监听Queue
DefaultConsumer consumer = 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,我这个消费者把这个消息给消费掉了)
// (false,必须要手动告诉RabbitMQ,消费完毕)
//参数3:callback - 指定消费回调
channel.basicConsume("HelloWorld",true,consumer);
System.out.println("消费者开始监听队列.....");
//System.in.read() 只有键盘录入之后才会停下
System.in.read();
//5.释放资源
connection.close();
channel.close();
}
4.4 Work模式
一个生产者,一个默认的交换机,一个队列,两个消费者
只要在消费者端,添加Qos能力,以及更改为手动ack即可让消费者,根据自己的能力去消费指定的信息,而不是默认的情况下由RabbitMQ平均分配了
//3.5 指定当前消费者一次消费多少个消息
channel.basicQos(1);
//4.开启监听Queue
DefaultConsumer consumer = new DefaultConsumer(channel){
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("消费者1号接收到消息->"+new String(body,"UTF-8"));
//功能:手动ACK
channel.basicAck(envelope.getDeliveryTag(),false);
}
};
//参数1:queue - 要消费的队列名称
//参数2:autoAck - 指定是否自动ACK
// (true,接收到消息后,会立即告诉RabbitMQ,我这个消费者把这个消息给消费掉了)
// (false,必须要手动告诉RabbitMQ,消费完毕)
//参数3:callback - 指定消费回调
//5.指定手动ACK
channel.basicConsume("Work",false,consumer);
4.5 Publish/Subscribe模式
一个生产者,一个交换机,两个队列,两个消费者
声明一个Fanout类型的exchange,并将exchange和queue绑定在一起,绑定的方式就是直接绑定
1、让生产者创建一个exchange,并且和一个或多个队列绑定在一起
2、消费者还是正常的监听某一个队列即可
//3.创建exchange,通过exchange去绑定某一个队列
//参数1:exchange的名称
//参数1:指定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","");
4.6 Routing模式
一个生产者,一个交换机,两个队列,两个消费者
创建DIRECT类型的exchange,并且根据RoutingKey去绑定指定的队列
1、生产者在创建DIRECT类型的exchange后,去绑定响应的队列,并且在发送消息时,指定消息具体的RoutingKey即可。
//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());
2、消费者基本无变化,只监听自己的队列即可
4.7 Topic模式
一个生产者,一个交换机,两个队列,两个消费者
1、生产者创建Topic的exchange并且绑定到队列中,这次绑定可以通过 * 和 # 关键字去指定RoutingKey内容,在编写时注意格式xxx.xxx.xxx去编写,* 代表一个xxx ,#代表多个xxx,在发送消息时,就必须要指定具体的routingkey到底时什么。
//3.创建exchange,绑定队列topic-queue-1 topic-queue-2
//routingkey的套路 eg:动物信息<speed><color><what>
//比如对红色动物感兴趣 :(*.red.*) ->*占位符
//对速度快的动物感兴趣:(fast.#) -> # 通配符
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","*.*.cat");
//4.发布消息到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());
2、消费还是监听我们的队列,没什么变化
5、SpringBoot整合RabbitMQ
5.1 SpringBoot整合RabbitMQ
1、创建springboot工程
2、导入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
3、编写配置文件(让Springboot 连接上RabbitMQ)
spring:
rabbitmq:
host: 192.168.65.128
username: test
password: test
port: 5672
virtual-host: /test
4、编写配置类,声明exchange和queue,并绑定(binding)在一起
@Configuration
public class RabbitMQConfig {
//1.创建exchange(交换机) -topic方式
@Bean
public TopicExchange getTopicExchange(){
return new TopicExchange("boot-topic-exchange",true,false);
}
//2.创建queue队列
//exclusive:不排外
@Bean
public Queue getQueue(){
return new Queue("boot-queue",true,false,false,null);
}
//3.将exchange与队列绑定在一起
@Bean
public Binding getBinding(TopicExchange topicExchange,Queue queue){
return BindingBuilder.bind(queue).to(topicExchange).with("*.red.*");
}
}
5、发布消息到RabbitMQ
@Autowired
private RabbitTemplate rabbitTemplate;
@Test
void contextLoads() {
rabbitTemplate.convertAndSend("boot-topic-exchange","slow.red.dog","红色大狼狗");
}
6、消费者监听发布的消息
@Component
public class Consumer {
@RabbitListener(queues = "boot-queue")
public void getMessage(Object message){
System.out.println("接收到消息:"+message);
}
}
5.2 如何实现手动ACK
1、修改配置文件
spring:
rabbitmq:
host: 192.168.65.128
username: test
password: test
port: 5672
virtual-host: /test
listener:
simple:
acknowledge-mode: manual
2、在消费消息的位置,修改方法,再手动ACK
@Component
public class Consumer {
@RabbitListener(queues = "boot-queue")
public void getMessage(String msg , Channel channel, Message message) throws IOException {
System.out.println("接收到消息:"+msg);
//手动ACK
channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);
}
}
6、关于RabbitMQ的其他操作
6.1 目的:为了消息的可靠性
6.1 .1Confrim机制
RabbitMQ的事务:事务可以保证消息100%传递,可以通过事务的回滚去记录日志,后面定时再次发送当前消息。
事务的操作,效率太低,加了事务操作以后,他比平时操作的效率要低上100倍以上。
RabbitMQ除了事务,还提供了Confirm机制,这个效率要比事务更高很多。
1、普通Confirm方式
//3.发布消息到exchange,同时指定路由规则
//3.1 开启confirm
channel.confirmSelect();
//3.2 发送消息
String msg = "Hello-World";
channel.basicPublish("","HelloWorld",null,msg.getBytes());
//ps:exchange 是不会帮你把消息持久化到本地的,Queue才会帮你将消息持久化
//3.3 判断消息是否发送成功
if(channel.waitForConfirms()){
System.out.println("消息发送成功!");
}else {
System.out.println("发送消息失败!");
}
2、批量Confirm方式
//3.发布消息到exchange,同时指定路由规则
//3.1 开启confirm
channel.confirmSelect();
//3.2 批量发送消息
for (int i = 0; i < 1000; i++) {
String msg = "Hello-World"+i;
channel.basicPublish("","HelloWorld",null,msg.getBytes());
//ps:exchange 是不会帮你把消息持久化到本地的,Queue才会帮你将消息持久化
}
//3.3 判断批量操作是否成功
channel.waitForConfirmsOrDie(); //当你发送的消息有一个失败的时候,就全部失败。抛出IO异常
3、异步Confirm方式
//3.发布消息到exchange,同时指定路由规则
//3.1 开启confirm
channel.confirmSelect();
//3.2 批量发送消息
for (int i = 0; i < 1000; i++) {
String msg = "Hello-World"+i;
channel.basicPublish("","HelloWorld",null,msg.getBytes());
//ps:exchange 是不会帮你把消息持久化到本地的,Queue才会帮你将消息持久化
}
//3.3 开启异步回调
channel.addConfirmListener(new ConfirmListener() {
public void handleAck(long deliveryTag, boolean multiple) throws IOException {
System.out.println("消息发送成功!标识:"+deliveryTag+",批量操作:"+multiple);
}
public void handleNack(long deliveryTag, boolean multiple) throws IOException {
System.out.println("消息发送失败!标识:"+deliveryTag+",批量操作:"+multiple);
}
});
System.in.read();
6.1.2 Return机制
confirm只能保证消息到达exchange,无法保证消息可以被exchange分发到指定的queue。
而且exchange是不能持久化消息的,只有queue可以持久化。
采用return机制,来监听消息是否从exchange送到了指定的queue中
开启Return机制
//开启return机制
channel.addReturnListener(new ReturnListener() {
public void handleReturn(int replyCode, String replyText, String exchange, String routingKey, AMQP.BasicProperties properties, byte[] body) throws IOException {
//当消息没有送达queue时才会执行,没有送达就不会执行。
System.out.println(new String(body,"utf-8")+"消息没有送达到Queue中!");
}
});
//在发送消息的时候,一定要把发送消息的mandatory设置为true
channel.basicPublish("","HelloWorld",true,null,msg.getBytes());
6.1.3 SpringBoot实现confirm机制和return机制
1、编写配置文件,开启confirm以及return机制
spring:
rabbitmq:
host: 192.168.65.128
username: test
password: test
port: 5672
virtual-host: /test
listener:
simple:
acknowledge-mode: manual
publisher-confirm-type: simple #开启confirm机制
publisher-returns: true #开启return机制
2、指定RabbitTemplate对象,开启Confirm和Return,并且编写回调方法
package com.wei.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 //spring中的init-method 在构建类对象的时候 会执行这个方法
public void initMethod(){
rabbitTemplate.setConfirmCallback(this);
rabbitTemplate.setReturnCallback(this);
}
@Override
public void confirm(CorrelationData correlationData, boolean ack, String s) {
if (ack){
System.out.println("消息已经送达到了exchange");
}else {
System.out.println("消息没有送达到exchange");
}
}
@Override
public void returnedMessage(Message message, int i, String s, String s1, String s2) {
System.out.println("消息没有送达到队列Queue中!");
}
}
生产者和消费者基本没有什么变化
6.2 消息重复消费
重复消费消息,会对我们的非幂等性操作出现问题。
重复消费的原因是,消费者没有个RabbitMQ一个ack
为了解决消息重复消费的问题,可以采用Redis,在消费者消费消息之前,现将消息的id放到Redis中,
id-0(正在执行业务)
id-1(执行业务成功)
如果ack失败,在RabbitMQ将消息交给其他消费者时,先执行setnx,如果key已经存在,获取他的值,如果是0,当前消费者就什么都不干,如果是1,直接ack。
极端情况:第一个消费者在执行业务时,出现了死锁,在setnx基础上,再给key设置一个生存时间。
1、生产者,发送消息时,指定massageId
package com.wei.confirm;
import com.rabbitmq.client.*;
import com.wei.config.RabbitMQClient;
import org.junit.Test;
import java.io.IOException;
import java.util.UUID;
public class Publisher {
@Test
public void publish() throws Exception {
//1.获取连接Connection
Connection connection = RabbitMQClient.getConnection();
//2.创建Channel
Channel channel = connection.createChannel();
//开启return机制
channel.addReturnListener(new ReturnListener() {
public void handleReturn(int replyCode, String replyText, String exchange, String routingKey, AMQP.BasicProperties properties, byte[] body) throws IOException {
//当消息没有送达queue时才会执行,没有送达就不会执行。
System.out.println(new String(body,"utf-8")+"消息没有送达到Queue中!");
}
});
//3.发布消息到exchange,同时指定路由规则
//3.1 开启confirm
channel.confirmSelect();
channel.addConfirmListener(new ConfirmListener() {
public void handleAck(long deliveryTag, boolean multiple) throws IOException {
System.out.println("消息发送成功!标识:"+deliveryTag+",批量操作:"+multiple);
}
public void handleNack(long deliveryTag, boolean multiple) throws IOException {
System.out.println("消息发送失败!标识:"+deliveryTag+",批量操作:"+multiple);
}
});
AMQP.BasicProperties properties =new AMQP.BasicProperties().builder()
.deliveryMode(1) //消息是否需要持久化,1-则需要持久化 2-不需要
.messageId(UUID.randomUUID().toString())
.build();
String msg = "Hello-World";
channel.basicPublish("","HelloWorld",true,properties,msg.getBytes());
//ps:exchange 是不会帮你把消息持久化到本地的,Queue才会帮你将消息持久化
System.in.read();
System.out.println("生产者消息发送成功!");
//4.释放资源
channel.close();
connection.close();
}
}
2、消费者,在消费消息时,根据具体业务去操作redis
package com.wei.helloworld;
import com.rabbitmq.client.*;
import com.wei.config.RabbitMQClient;
import org.junit.Test;
import redis.clients.jedis.Jedis;
import java.io.IOException;
public class Consumer {
@Test
public void consume() throws Exception {
//1.获取连接
Connection connection = RabbitMQClient.getConnection();
//2.创建Channel
final Channel channel = connection.createChannel();
//3.声明队列-HelloWorld 要和生产者 使用的队列名字相同
//参数1:queue - 指定队列名称
//参数2:durable - 当前队列是否需要持久化(true 为需要持久化)
//参数3:exclusive - 是否排外(conn.close()当前队列会被自动删除,当前队列只能被一个消费者消费)
//参数4:autoDelete - 如果这个队列没有消费者在消费,那么这个队列自动被删除
//参数5:arguments - 指定当前队列的其他信息
channel.queueDeclare("HelloWorld",true,false,false,null);
//4.开启监听Queue
DefaultConsumer consumer = new DefaultConsumer(channel){
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
//重点 消费者操作redis
Jedis jedis = new Jedis("192.168.65.128",6379);
String messageId = properties.getMessageId();
//1.setnx到Redis中,默认指定value为0
String result = jedis.set(messageId, "0", "NX", "EX", 10);
if (result!=null && result.equalsIgnoreCase("OK")){
System.out.println("接收到消息->"+new String(body,"UTF-8"));
//2.消费成功,set messageId = 1 证明消费完毕
jedis.set(messageId,"1");
channel.basicAck(envelope.getDeliveryTag(),false);
}else {
//3.如果1中的setnx失败,获取key对应的value,如果是0,return,如果是1,直接ack回调
String s = jedis.get(messageId);
if ("1".equalsIgnoreCase(s)){
channel.basicAck(envelope.getDeliveryTag(),false);
}
}
}
};
//参数1:queue - 要消费的队列名称
//参数2:autoAck - 指定是否自动ACK
// (true,接收到消息后,会立即告诉RabbitMQ,我这个消费者把这个消息给消费掉了)
// (false,必须要手动告诉RabbitMQ,消费完毕)
//参数3:callback - 指定消费回调
channel.basicConsume("HelloWorld",true,consumer);
System.out.println("消费者开始监听队列.....");
//System.in.read() 只有键盘录入之后才会停下
System.in.read();
//5.释放资源
connection.close();
channel.close();
}
}
6.2.2 sprinboot如何实现
1、导入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
2、修改配置文件
spring:
rabbitmq:
host: 192.168.65.128
username: test
password: test
port: 5672
virtual-host: /test
listener:
simple:
acknowledge-mode: manual
publisher-confirm-type: simple
publisher-returns: true
redis:
host: 127.0.0.1
port: 6379
3、修改生产者
@SpringBootTest
class SpringbootRabbitmqApplicationTests {
@Autowired
private RabbitTemplate rabbitTemplate;
@Test
void contextLoads() throws IOException {
CorrelationData messageId = new CorrelationData(UUID.randomUUID().toString());
rabbitTemplate.convertAndSend("boot-topic-exchange","slow.black.dog","红色大狼狗",messageId);
System.in.read();
}
}
4、修改消费者
@Component
public class Consumer {
@Autowired
private StringRedisTemplate redisTemplate;
@RabbitListener(queues = "boot-queue")
public void getMessage(String msg , Channel channel, Message message) throws IOException {
//0.先拿到生产者的消息id
String messageId = message.getMessageProperties().getHeader("spring_returned_message_correlation");
//1.设置key到redis
Boolean flag = redisTemplate.opsForValue().setIfAbsent(messageId, "0", 10, TimeUnit.SECONDS);
if (flag){
//2.消费信息
System.out.println("接收到消息:"+msg);
//3.设置手动value为1
redisTemplate.opsForValue().set(messageId,"1");
//4.手动ACK
channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);
}else {
//5.获取redis中的value即可,如果是1 ,手动akc 如果是0 正在消费,不做任何操作
//如果没有接收到消息,如果messageId为1,则说明已经消费过了,直接手动ack
if ("1".equalsIgnoreCase(redisTemplate.opsForValue().get(messageId))){
channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);
}
}
}
}
7、RabbitMQ的应用
场景题,没有写过项目,卒~