RabbitMQ 实战
1.springboot操作RabbitMQ
创建springboot项目后导入pom依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
1.1第一个java程序
编写 一个 消息生产者
package com.yy.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;
public class MsgProducer {
private static final String QUEEN_NAME="hello";
public static void main(String[] args) throws IOException, TimeoutException {
String msg = "Hello World";
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("192.168.56.10");
factory.setUsername("guest");
factory.setPassword("guest");
Connection connection = factory.newConnection();
//创建信道
Channel channel = connection.createChannel();
/***
1:队列名称
2:是否持久化持久化会保存到磁盘,默认是保存到内存
3:是否多个消费者共享消费
4:是否自动删除
5:其他参数,延迟或者死信队列等
***/
channel.queueDeclare(QUEEN_NAME,false,false, false,null);
/**
* 1: 发送到那个交换机
* 2:路由的key值
* 3: 其他参数信息
* 4:参数内容
* **/
channel.basicPublish("",QUEEN_NAME,null,msg.getBytes());
System.out.println("消息發送完畢");
}
}
创建消费者
package com.yy.mq;
import com.rabbitmq.client.*;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
public class MsgConsumer {
private static final String QUEEN_NAME="hello";
public static void main(String[] args) throws IOException, TimeoutException {
String msg = "Hello World";
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("192.168.56.10");
factory.setUsername("guest");
factory.setPassword("guest");
Connection connection = factory.newConnection();
//创建信道
Channel channel = connection.createChannel();
/**
* 1:被消费队列名
* 2:是否自动应答 true 是自动应答 false 手动应答
* 3:消费者未成功消费的回调函数
* 4: 消费者去掉消费的回调
* */
//正常获取消息
DeliverCallback deliverCallback=(consumerTag,message)->{
System.out.println(new String(message.getBody()));
};
//消费消息被中断
CancelCallback cancelCallback=(consumerTag)->{
System.out.println("消息被中断");
};
channel.basicConsume(QUEEN_NAME,true,deliverCallback,cancelCallback);
System.out.println("消息接收完毕");
}
}
1.2work queen模式
一个生产者发送消息
可以有多个消费者 ,但是只有一个消费者可以获取到消息,使用轮询方式来处理消息,消息不可以重复被消费
编写 work01 工作线程,并在idea 中 设置使这个class 可以多个线程执行
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-iFr6Jb8N-1668592760836)(C:\Users\19335\AppData\Roaming\Typora\typora-user-images\image-20221104143730625.png)]
消费者代码
package com.yy.mq;
import com.rabbitmq.client.CancelCallback;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.DeliverCallback;
import com.yy.utils.RabbitMqUtils;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
//消费类
public class Worker01 {
private static final String QUEEN_NAME="hello";
public static void main(String[] args) throws IOException, TimeoutException {
Channel channge = RabbitMqUtils.channge();
/**
* 1:被消费队列名
* 2:是否自动应答 true 是自动应答 false 手动应答
* 3:消费者未成功消费的回调函数
* 4: 消费者去掉消费的回调
* */
DeliverCallback deliverCal=(consumerTag,message)->{
System.out.println("接收到的消息"+new String(message.getBody()));
};
CancelCallback cancelCallback=(consumerTag)->{
System.out.println(consumerTag+"消息被取消回调了");
};
channge.basicConsume(QUEEN_NAME,true,deliverCal,cancelCallback);
}
}
编写生产者代码
package com.yy.mq;
import com.rabbitmq.client.Channel;
import com.yy.utils.RabbitMqUtils;
import java.io.IOException;
import java.util.Scanner;
import java.util.concurrent.TimeoutException;
public class Producer01 {
private static final String QUEEN_NAME = "hello";
public static void main(String[] args) throws IOException, TimeoutException {
Channel channge = RabbitMqUtils.channge();
channge.queueDeclare(QUEEN_NAME, false, false, false, null);
Scanner scanner = new Scanner(System.in);
while (scanner.hasNext()) {
String next = scanner.next();
channge.basicPublish("", QUEEN_NAME, null, next.getBytes());
System.out.println("消息"+next+"发送成功");
}
}
}
执行producer代码发送多个消息,会发现 消费端 会轮流的打印 发送的消息,并且不会重复消费,是轮询的的消费消息的
1.3消息应答机制
在消费者消费完之后 进行应答,只有在获取到消费端消费完的应到之后才删除队列里的消息
避免消息丢失,自动应答并不靠谱 ,特别是在接受到大量消息的时候 ,如果后续处理消息的过程中 发生了异常,可能会导致大量消息丢失
消息应答的方式:
- Channel.basicAck() 用于肯定确认,消息已经肯定处理成功了
- Channel.basicNack() 用于否定确认,不能确定消息已经肯定处理成功了
- Channel.basicReject() 用于拒绝确认,不能确定消息已经肯定处理成功了 比这个 Channel.basicNack() 对一个是否批量处理的参数mutilple手动应答的好处 可以批量应答,你比那个且可以减少网络阻塞,如下图所示当批量应答时 只要channel 中的第一个被应答 ,信道中的其他消息就会一并被应答,而不批量应答只能一个一个的应答
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qkLWFUEb-1668592760837)(C:\Users\19335\AppData\Roaming\Typora\typora-user-images\image-20221104144131221.png)]
消息自动重新入队,开启手动应答
当有多个消费端时 ,如果一个消息已经发送,并且被一个消费端获取了,但是队列并未收到ack ,这时队列并不会删除消息,而是将消息从新入队并将消息发送个另外一个可达的 消费端消费
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-URJ9cW2R-1668592760837)(C:\Users\19335\AppData\Roaming\Typora\typora-user-images\image-20221104144259653.png)]
消息手动应答的代码
package com.yy.mq;
import com.rabbitmq.client.Channel;
import com.yy.utils.RabbitMqUtils;
import java.util.Scanner;
//生产者
public class NotAutoAckProducer {
private static final String NOTAUTOACK_QUEEN_NAME = "ack_queen";
public static void main(String[] args) {
try {
Channel channge = RabbitMqUtils.channge();
/***
1:队列名称
2:是否持久化持久化会保存到磁盘,默认是保存到内存
3:是否多个消费者共享消费
4:是否自动删除
5:其他参数,延迟或者死信队列等
***/
channge.queueDeclare(NOTAUTOACK_QUEEN_NAME,false,false,false,null);
Scanner scanner = new Scanner(System.in);
while (scanner.hasNext()) {
String message = scanner.next();
/**
* 1: 发送到那个交换机
* 2:路由的key值
* 3: 其他参数信息
* 4:参数内容
* **/
channge.basicPublish("",NOTAUTOACK_QUEEN_NAME,null,message.getBytes("UTF-8"));
System.out.println("消息发送完成");
}
} catch (Exception e) {
e.getStackTrace();
}
}
}
消费者1
package com.yy.mq;
import com.rabbitmq.client.CancelCallback;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.DeliverCallback;
import com.yy.utils.RabbitMqUtils;
//消费者
//手动应答,消息不丢失,返回队列个可达的消费者消费
public class NotAutoAckConsumer {
private static final String NOTAUTOACK_QUEEN_NAME = "ack_queen";
public static void main(String[] args) {
try {
Channel channge = RabbitMqUtils.channge();
DeliverCallback deliverCallback = (consumerTag, message) -> {
try {
Thread.sleep(1000);
} catch (Exception e) {
e.printStackTrace();
}
/**
* 手动应答,获取消息标签
* 和不批量应答
*/
channge.basicAck(message.getEnvelope().getDeliveryTag(), false);
System.out.println("NotAutoAckConsumer1接收到消息:" + new String(message.getBody()));
};
CancelCallback cancelCallback = (consumerTag) -> {
System.out.println(consumerTag + "消息被取消回调");
};
boolean autoAck = false;
/**
* 1:被消费队列名
* 2:是否自动应答 true 是自动应答 false 手动应答
* 3:消费者未成功消费的回调函数
* 4: 消费者去掉消费的回调
* */
channge.basicConsume(NOTAUTOACK_QUEEN_NAME, autoAck, deliverCallback, cancelCallback);
} catch (Exception e) {
e.printStackTrace();
}
}
}
消费者2
package com.yy.mq;
import com.rabbitmq.client.CancelCallback;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.DeliverCallback;
import com.yy.utils.RabbitMqUtils;
public class NotAutoAckConsumer2 {
private static final String NOTAUTOACK_QUEEN_NAME="ack_queen";
public static void main(String[] args) throws Exception{
Channel channel = RabbitMqUtils.channge();
DeliverCallback deliverCallback = (consumerTag, message)->{
try {
Thread.sleep(1000*20);
} catch (InterruptedException e) {
e.printStackTrace();
}
channel.basicAck(message.getEnvelope().getDeliveryTag(),false);
System.out.println("NotAutoAckConsumer2接受到的消息:"+ new String(message.getBody()));
};
CancelCallback cancelCallback = (consumerTag)->{
System.out.println(consumerTag+"消息被取消回调");
};
boolean autoAck = false;
channel.basicConsume(NOTAUTOACK_QUEEN_NAME,autoAck,deliverCallback,cancelCallback);
}
}
1.4队列持久化
在消息发送的时候,将durable 设置为true 表示 开启持久化
channel.queueDeclare(NOTAUTOACK_QUEEN_NAME,true,false,false,null);
package com.yy.mq;
import com.rabbitmq.client.Channel;
import com.yy.utils.RabbitMqUtils;
import java.util.Scanner;
//持久化队列
public class PersistProducer {
private static final String NOTAUTOACK_QUEEN_NAME = "persist_queen";
public static void main(String[] args) {
try {
Channel channge = RabbitMqUtils.channge();
// durable true 表示 开启持久化
channge.queueDeclare(NOTAUTOACK_QUEEN_NAME,true,false,false,null);
Scanner scanner = new Scanner(System.in);
while (scanner.hasNext()){
String message = scanner.next();
channge.basicPublish("",NOTAUTOACK_QUEEN_NAME,null,message.getBytes("UTF-8"));
System.out.println("消息"+message+"发送完成");
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
执行代码 去控制台 可以发现 queen 里面的 features 里面的只有个大写的D ,表示持久化了,此时关rabbbitmq 服务再重启,发现队列不会消失了
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0lqYNVgv-1668592760837)(C:\Users\19335\AppData\Roaming\Typora\typora-user-images\image-20221104151103229.png)]
1.5消息持久化
只需要修改生产者在发送消息的时候 将第三个参数修改为
//开启消息持久化 MessageProperties.PERSISTENT_TEXT_PLAIN
channge.basicPublish("",NOTAUTOACK_QUEEN_NAME,MessageProperties.PERSISTENT_TEXT_PLAIN,message.getBytes("UTF-8"));
这里并不能保证绝对的消息不丢失 ,可能会在发送的某个时间点还没完全 处理完 但是 对服务挂了,也不能持久化
package com.yy.mq;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.MessageProperties;
import com.yy.utils.RabbitMqUtils;
import java.util.Scanner;
public class PersistProducer2 {
private static final String NOTAUTOACK_QUEEN_NAME = "persist_queen";
public static void main(String[] args) {
try {
Channel channge = RabbitMqUtils.channge();
// durable true 表示 开启持久化
channge.queueDeclare(NOTAUTOACK_QUEEN_NAME,true,false,false,null);
Scanner scanner = new Scanner(System.in);
while (scanner.hasNext()){
String message = scanner.next();
channge.basicPublish("",NOTAUTOACK_QUEEN_NAME, MessageProperties.PERSISTENT_TEXT_PLAIN,message.getBytes("UTF-8"));
System.out.println("消息"+message+"发送完成");
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
此时执行代码,关闭服务再次进入 控制台检查 发现 消息不会丢失了
不公平分发
在多个消费端的情况下 ,添加如下代码
channel.basicQos(1);
// 设置不公平分发原则,体现一种能者多劳的方式 哪个线程处理的快,就会多处理消息而处理的慢的就会少处理
预取值
在上面的不公平分发的时候 传递给channel.basicQos(1)这个的参数为1 ,多个客户端里面的都是1 ,当 有一种情况我们需要发的数据的消息总数是已知的 ,这时候我们就可以通过改变这个参数来指定不同的客户端 消费 的消息数量, 比如 总共1000条消息,就可以指定 某个消费端 消费 200 条channel.basicQos(200) ,其他消费端消费 800 条 channel.basicQos(1), 这样就不管客户端 消费能力的问题,哪怕 消费端1 的消费能力很强 但是 它也只消费 200条消息, 消费端2 消费能力很差 但是 它也得消费800 条 ,这就是没有了能者多劳特性了
1.6发布确认
虽然已经通过 durable 将队列持久化 和 MessageProperties.PERSISTENT_TEXT_PLAIN 来持久化 消息 ,但是 生产者是不知道 消息是否真的持久化了的,这就需要 RabbitMQ 的应答机制来处理这个场景
RabbitMQ 的应答机制主要有三种
-
单个应答 : 一个一个应答 ,准确性最高 消息不回丢失 但是效率比较低,同步操作
-
批量应答 : 效率比较高 ,但是 当消费端 批量消费的过程中 ,如果还没处理完就出现异常了,那么被取出的数据中 还未处理完的那部分消息就会丢失,同步操作
-
异步批量应答 :采用多线程的方式 使用监听器,在发送消息的同时也在监控没有发送成功的数据
package com.yy.mq;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.ConfirmCallback;
import com.yy.utils.RabbitMqUtils;
import java.io.IOException;
import java.util.UUID;
import java.util.concurrent.TimeoutException;
/**
* 发布确认
* 1.单个确认
* 2.批量确认
* 3.异步确认
*/
public class ConfirmProducer {
public static final int MESSAGE_COUNT=800;
public static void main(String[] args) throws Exception {
ConfirmProducer.publishMsgSingle();//发布800个消耗时间335ms
ConfirmProducer.publishMsgBatch();//发布800个消息耗时49ms,当出现消息未确认时无法知道那个消息没有被确认
ConfirmProducer.publishMsgSynchBatch();//19ms 异步时间
}
//1.单个确认
public static void publishMsgSingle(){
try {
Channel channge = RabbitMqUtils.channge();
String queeName = UUID.randomUUID().toString();
channge.queueDeclare(queeName,true,false,false,null);
channge.confirmSelect();//开启发布确认
long l = System.currentTimeMillis();
for (int i=0;i<MESSAGE_COUNT; i++){
String message="msg"+i;
channge.basicPublish("",queeName,null,message.getBytes());
boolean b = channge.waitForConfirms();//等候确认
if (b){
System.out.println("第"+i+"个消息发送成功");
}
}
long end = System.currentTimeMillis();
System.out.println("发布"+MESSAGE_COUNT+"个消耗时间"+(end-l)+"ms");
}catch (Exception e) {
e.printStackTrace();
}
}
//批量确认
public static void publishMsgBatch() throws Exception{
Channel channel = RabbitMqUtils.channge();
String queenName = UUID.randomUUID().toString();
channel.queueDeclare(queenName,true,false,false,null);
channel.confirmSelect(); // 开启发布确认
//批量确认的大小 假设 200条
int batchSize = 200;
long start = System.currentTimeMillis();
for (int i = 0; i < MESSAGE_COUNT; i++) {
String message = "msg"+i;
channel.basicPublish("",queenName,null,message.getBytes());
if(i%batchSize == 0 ){
channel.waitForConfirms();
}
}
long end = System.currentTimeMillis();
System.out.println("发布"+MESSAGE_COUNT+"个消息耗时"+(end-start)+"ms");
}
//异步确认
public static void publishMsgSynchBatch() throws IOException, TimeoutException {
Channel channge = RabbitMqUtils.channge();
String queeName = UUID.randomUUID().toString();
channge.queueDeclare(queeName,true,false,false,null);
channge.confirmSelect();//开启发布确认
long start = System.currentTimeMillis();
//消息成功发送回调函数 deliveryTag 消息标记 multiple 是否批量确认
ConfirmCallback ackCallback=(deliveryTag,multiple)->{
System.out.println("正确确认的消息"+deliveryTag);
};
//消息失败回调函数 deliveryTag 消息标记 multiple是否批量
ConfirmCallback nackCallback=(deliveryTag,multiple)->{
System.out.println("未确认的消息"+deliveryTag);
};
//创建监听器在发送消息之前,监听消息的成功和失败
channge.addConfirmListener(ackCallback,nackCallback);
for (int i = 0; i < MESSAGE_COUNT; i++) {
String message="msg"+i;
channge.basicPublish("",queeName,null,message.getBytes());
}
long end = System.currentTimeMillis();
System.out.println("发布"+MESSAGE_COUNT+"个消息耗时"+(end-start)+"ms");
}
}
采用异步方式的时候 怎样去处理未被及时确认的信息呢 ?
最常用的一种方式是将为被确认的消息放到一个基于内存的可以被发布线程访问的队列
比如 ConcurrentLinkedQueen ,这个队列可以在 confirm callbacks 与发布线程之间进行消息的传递
异步确认处理未被确认消息的处理逻辑