生产者将信道设置成 confirm 模式,一旦信道进入 confirm 模式,所有在该信道上面发布的消息都将会被指派一个唯一的 ID(从 1 开始),一旦消息被投递到所有匹配的队列之后,broker就会发送一个确认给生产者(包含消息的唯一 ID),这就使得生产者知道消息已经正确到达目的队列了,如果消息和队列是可持久化的,那么确认消息会在将消息写入磁盘之后发出,broker 回传给生产者的确认消息中 delivery-tag 域包含了确认消息的序列号,此外 broker 也可以设置basic.ack 的 multiple 域,表示到这个序列号之前的所有消息都已经得到了处理。
confirm 模式最大的好处在于他是异步的,一旦发布一条消息,生产者应用程序就可以在等信道返回确认的同时继续发送下一条消息,当消息最终得到确认之后,生产者应用便可以通过回调方法来处理该确认消息,如果 RabbitMQ 因为自身内部错误导致消息丢失,就会发送一条 nack 消息,生产者应用程序同样可以在回调方法中处理该 nack 消息。
1、如何开启发布确认?
2、接下来介绍我们的三种发布确认策略:
1️⃣ 单个确认发布
这是一种简单的确认方式,它是一种同步确认发布的方式,也就是发布一个消息之后只有它被确认发布,后续的消息才能继续发布,**waitForConfirmsOrDie(long)**这个方法只有在消息被确认的时候才返回,如果在指定时间范围内这个消息没有被确认那么它将抛出异常。
这种确认方式有一个最大的缺点就是:发布速度特别的慢,因为如果没有确认发布的消息就会阻塞所有后续消息的发布,这种方式最多提供每秒不超过数百条发布消息的吞吐量。当然对于某些应用程序来说这可能已经足够了。
可以了理解为一手交钱一手交货,当未得到发出消息的确认,那么就不会再向其发送消息
//同步确认
public static void publishMessageIndividually() throws Exception{
Channel channel = RabbitMqUtils.getChannel();
//开启发布确认
channel.confirmSelect();
//队列的声明
String queueName = UUID.randomUUID().toString();
channel.queueDeclare(queueName, true, false, false, null);
//获取当前系统时间
long begin = System.currentTimeMillis();
//循环发送1k条数据
for (int i = 0; i < MESSAGE_COUNT; i++) {
String s = i + "";
channel.basicPublish("", queueName, null, s.getBytes("UTF-8"));
boolean flag = channel.waitForConfirms();
if(flag){
System.out.println("消息发送成功!");
}
}
//获取结束时间
long end = System.currentTimeMillis();
System.out.println("发送" + MESSAGE_COUNT + "个单独确认消息,耗时 " + (end - begin) +" ms");
}
2️⃣ 批量确认发布
先发布一批消息然后一起确认可以极大地提高吞吐量,当然这种方式的缺点就是:
//批量确认
public static void publishMessageBatch() throws Exception{
Channel channel = RabbitMqUtils.getChannel();
//开启发布确认
channel.confirmSelect();
//队列的声明
String queueName = UUID.randomUUID().toString();
channel.queueDeclare(queueName, true, false, false, null);
//获取当前系统时间
long begin = System.currentTimeMillis();
//批量确认数据的大小
int batchSize = 100;
for (int i = 0; i < MESSAGE_COUNT; i++) {
String message = i + "";
channel.basicPublish("", queueName, null, message.getBytes("UTF-8"));
//每发送100条确认一次
if(i % batchSize == 0){
channel.waitForConfirms();
}
}
//获取结束时间
long end = System.currentTimeMillis();
System.out.println("发送" + MESSAGE_COUNT + "个批量确认消息,耗时 " + (end - begin) +" ms");
}
3️⃣ 异步确认发布
异步确认虽然编程逻辑比上两个要复杂,但是性价比最高,无论是可靠性还是效率都没得说,他是利用回调函数来达到消息可靠性传递的,这个中间件也是通过函数回调来保证是否投递成功,
//异步确认
public static void publishMessageAsync() throws Exception{
Channel channel = RabbitMqUtils.getChannel();
//创建一个保存消息的集合
ConcurrentSkipListMap<Long, String> outstandingConfirms = new ConcurrentSkipListMap<>();
//开启发布确认
channel.confirmSelect();
//队列的声明
String queueName = UUID.randomUUID().toString();
channel.queueDeclare(queueName, true, false, false, null);
//获取当前系统时间
long begin = System.currentTimeMillis();
//实现那两个接口,ackCallback消息确认成功回调函数,noAckCallback 消息确认失败回调函数
ConfirmCallback ackCallback = (deliveryTag, multiple) ->{
//消息确认成功就将其移除集合 [注意是否为批量处理]
if(multiple){
//获取小于当前序号的确认消息集合
ConcurrentNavigableMap<Long, String> confirmed = outstandingConfirms.headMap(deliveryTag, true);
//清除该部分确认消息
confirmed.clear();
}else{
outstandingConfirms.remove(deliveryTag);
}
System.out.println("确认的消息: " + deliveryTag);
};
ConfirmCallback noAckCallback = (deliverTag, multiple) ->{
System.out.println("发布的消息"+outstandingConfirms.get(deliverTag)+"未被确认消息的标记"+deliverTag);
};
//设置监听器,第一个参数监听哪些消息成功了,第二个参数监听哪些消息失败了
channel.addConfirmListener(ackCallback, noAckCallback);
for (int i = 0; i < MESSAGE_COUNT; i++) {
String message = "消息:" + i;
channel.basicPublish("", queueName, null, message.getBytes("UTF-8"));
//将消息都放到集合中
outstandingConfirms.put(channel.getNextPublishSeqNo(), message);
}
//获取结束时间
long end = System.currentTimeMillis();
System.out.println("发送" + MESSAGE_COUNT + "个异步确认消息,耗时 " + (end - begin) +" ms");
}
3、如何处理异步未确认消息?
4、比较以上三种发布确认策略的速度
public class ConfirmMessage {
//准备发送1k条信息
public static final int MESSAGE_COUNT = 1000;
public static void main(String[] args) throws Exception{
publishMessageIndividually();//发送1000个单独确认消息,耗时 75658 ms
publishMessageBatch();//发送1000个批量确认消息,耗时 1560 ms
publishMessageAsync();//发送1000个异步确认消息,耗时 248 ms
}
1、什么是交换机(Exchange)?
2、交换机有哪些类型呢?
总共有四种类型:直接(direct)、扇出(fanout)、主题(topic)、标题(headers)
当然也支持自定义类型:alternate-exchange,这部分的内容会在备份交换机涉及
对于之前的案例我们都没有提及到交换机,我们一直使用的是默认的交换机
channel.basicPublish("", QUEUE_NAME, null, message.getBytes());
每当我们连接到 Rabbit 时,我们都需要一个全新的空队列,
通过 String queueName = channel.queueDeclare().getQueue();
指令可以获得临时队列
package com.atguigu.rabbitmq.five;
import com.atguigu.rabbitmq.utils.RabbitMqUtils;
import com.rabbitmq.client.Channel;
import java.util.Scanner;
/**
* @author Bonbons
* @version 1.0
*/
public class EmitLog {
public static final String EXCHANGE_NAME = "logs";
public static void main(String[] args) throws Exception{
Channel channel = RabbitMqUtils.getChannel();
channel.exchangeDeclare(EXCHANGE_NAME, "fanout");
Scanner scanner = new Scanner(System.in);
while(scanner.hasNext()){
String message = scanner.next();
channel.basicPublish(EXCHANGE_NAME, "", null, message.getBytes("UTF-8"));
System.out.println("发送消息: " + message);
}
}
}
2️⃣ 消费者1
package com.atguigu.rabbitmq.five;
import com.atguigu.rabbitmq.utils.RabbitMqUtils;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.DeliverCallback;
/**
* @author Bonbons
* @version 1.0
* 消息接收,演示Fanout
*/
public class ReceiveLog01 {
public static final String EXCHANGE_NAME = "logs";
public static void main(String[] args) throws Exception{
Channel channel = RabbitMqUtils.getChannel();
//创建交换机 [交换机名、交换机类型]
channel.exchangeDeclare(EXCHANGE_NAME, "fanout");
//创建一个临时队列,名字是随机的,当消费者断开连接就会自动删除
String queueName = channel.queueDeclare().getQueue();
//将交换机和队列绑定起来 [队列、交换机、routingKey(可以为空串)]
channel.queueBind(queueName, EXCHANGE_NAME, "");
System.out.println("等待接收消息,把接收到的消息打印到屏幕上......");
DeliverCallback deliverCallback = (consumerTag, message) -> {
System.out.println("ReceiveLog01控制台打印接收到的消息: " + new String(message.getBody(), "UTF-8"));
};
channel.basicConsume(queueName, true, deliverCallback, consumerTag ->{});
}
}
3️⃣ 消费者2
package com.atguigu.rabbitmq.five;
import com.atguigu.rabbitmq.utils.RabbitMqUtils;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.DeliverCallback;
/**
* @author Bonbons
* @version 1.0
* 消息接收,演示Fanout
*/
public class ReceiveLog02 {
public static final String EXCHANGE_NAME = "logs";
public static void main(String[] args) throws Exception{
Channel channel = RabbitMqUtils.getChannel();
//创建交换机 [交换机名、交换机类型]
channel.exchangeDeclare(EXCHANGE_NAME, "fanout");
//创建一个临时队列,名字是随机的,当消费者断开连接就会自动删除
String queueName = channel.queueDeclare().getQueue();
//将交换机和队列绑定起来 [队列、交换机、routingKey(可以为空串)]
channel.queueBind(queueName, EXCHANGE_NAME, "");
System.out.println("等待接收消息,把接收到的消息打印到屏幕上......");
DeliverCallback deliverCallback = (consumerTag, message) -> {
System.out.println("ReceiveLog02控制台打印接收到的消息: " + new String(message.getBody(), "UTF-8"));
};
channel.basicConsume(queueName, true, deliverCallback, consumerTag ->{});
}
}
1、回顾
在上一节中,我们构建了一个简单的日志记录系统。我们能够向许多接收者广播日志消息。在本节我们将向其中添加一些特别的功能-比方说我们只让某个消费者订阅发布的部分消息。例如我们只把严重错误消息定向存储到日志文件(以节省磁盘空间),同时仍然能够在控制台上打印所有日志消息。
我们再次来回顾一下什么是 bindings,绑定是交换机和队列之间的桥梁关系。也可以这么理解:队列只对它绑定的交换机的消息感兴趣。绑定用参数:routingKey 来表示也可称该参数为 binding key,创建绑定我们用代码:channel.queueBind(queueName, EXCHANGE_NAME, "routingKey");
绑定之后的意义由其交换类型决定
2、什么是 Direct Exchange?
上一节中的我们的日志系统将所有消息广播给所有消费者,对此我们想做一些改变,例如我们希望将日志消息写入磁盘的程序仅接收严重错误(errros),而不存储哪些警告(warning)或信息(info)日志消息避免浪费磁盘空间。
Fanout 这种交换类型并不能给我们带来很大的灵活性-它只能进行无意识的广播,在这里我们将使用 direct 这种类型来进行替换,这种类型的工作方式是,消息只去到它绑定的routingKey 队列中去。
和扇出类型交换机的区别在于:指定了我们的路由 key,交换机会根据消息的路由key 将消息发送给与交换机绑定的且routingKey与这个key相同的队列
在上面这张图中,我们可以看到 X 绑定了两个队列,绑定类型是 direct
在这种绑定情况下,生产者发布消息到 exchange 上
3、再介绍一下什么是多重绑定?
4、接下来通过案例来演示 直接类型交换机(也称为路由模式)的使用
package com.atguigu.rabbitmq.six;
import com.atguigu.rabbitmq.utils.RabbitMqUtils;
import com.rabbitmq.client.Channel;
import java.util.Scanner;
/**
* @author Bonbons
* @version 1.0
*/
public class DirectLogs {
public static final String EXCHANGE_NAME = "direct_logs";
public static void main(String[] args) throws Exception{
Channel channel = RabbitMqUtils.getChannel();
Scanner scanner = new Scanner(System.in);
while(scanner.hasNext()){
String message = scanner.next();
String routingKey = scanner.next();
channel.basicPublish(EXCHANGE_NAME, routingKey, null, message.getBytes("UTF-8"));
}
}
}
2️⃣ 消费者1
package com.atguigu.rabbitmq.six;
import com.atguigu.rabbitmq.utils.RabbitMqUtils;
import com.rabbitmq.client.BuiltinExchangeType;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.DeliverCallback;
/**
* @author Bonbons
* @version 1.0
*/
public class ReceiveLogsDirect01 {
public static final String EXCHANGE_NAME = "direct_logs";
public static void main(String[] args) throws Exception{
Channel channel = RabbitMqUtils.getChannel();
channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.DIRECT);
channel.queueDeclare("console", false, false, false, null);
channel.queueBind("console", EXCHANGE_NAME, "info");
channel.queueBind("console", EXCHANGE_NAME, "warning");//多重绑定
//接收消息回调
DeliverCallback deliverCallback = (consumerTag, message) -> {
System.out.println("ReceiveLogsDirect01控制台接收到的消息: " + new String(message.getBody(), "UTF-8"));
};
channel.basicConsume("console", deliverCallback, consumerTag -> {});
}
}
3️⃣ 消费者2
package com.atguigu.rabbitmq.six;
import com.atguigu.rabbitmq.utils.RabbitMqUtils;
import com.rabbitmq.client.BuiltinExchangeType;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.DeliverCallback;
/**
* @author Bonbons
* @version 1.0
*/
public class ReceiveLogsDirect02 {
public static final String EXCHANGE_NAME = "direct_logs";
public static void main(String[] args) throws Exception{
Channel channel = RabbitMqUtils.getChannel();
channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.DIRECT);
channel.queueDeclare("disk", false, false, false, null);
channel.queueBind("disk", EXCHANGE_NAME, "error");
channel.queueBind("disk", EXCHANGE_NAME, "info");
//接收消息回调
DeliverCallback deliverCallback = (consumerTag, message) -> {
System.out.println("ReceiveLogsDirect02控制台接收到的消息: " + new String(message.getBody(), "UTF-8"));
};
channel.basicConsume("disk", deliverCallback, consumerTag -> {});
}
}
1、之前的交换机存在一定的局限性:
2、对于主题交换机的要求?
*(星号)
可以代替一个单词#(井号)
可以替代零个或多个单词3、Topic 匹配案例:
*.orange.*
)*.*.rabbit
)lazy.#
)4、接下来通过案例演示如何使用 Topic 交换机?
quick.orange.rabbit 被队列 Q1Q2 接收到
lazy.orange.elephant 被队列 Q1Q2 接收到
quick.orange.fox 被队列 Q1 接收到
lazy.brown.fox 被队列 Q2 接收到
lazy.pink.rabbit 虽然满足两个绑定但只被队列 Q2 接收一次
quick.brown.fox 不匹配任何绑定不会被任何队列接收到会被丢弃
quick.orange.male.rabbit 是四个单词不匹配任何绑定会被丢弃
lazy.orange.male.rabbit 是四个单词但匹配 Q2
1️⃣ 生产者
package com.atguigu.rabbitmq.seven;
import com.atguigu.rabbitmq.utils.RabbitMqUtils;
import com.rabbitmq.client.Channel;
import java.util.HashMap;
import java.util.Map;
/**
* @author Bonbons
* @version 1.0
* 生产者
*/
public class EmitLogTopic {
public static final String EXCHANGE_NAME = "topic_logs";
public static void main(String[] args) throws Exception{
Channel channel = RabbitMqUtils.getChannel();
//交换机的声明我们在消费者里面定义了,执行一次就行,所以此处就不写了
Map<String, String> bindingKeyMap = new HashMap<>();
bindingKeyMap.put("quick.orange.rabbit","被队列 Q1Q2 接收到");
bindingKeyMap.put("lazy.orange.elephant","被队列 Q1Q2 接收到");
bindingKeyMap.put("quick.orange.fox","被队列 Q1 接收到");
bindingKeyMap.put("lazy.brown.fox","被队列 Q2 接收到");
bindingKeyMap.put("lazy.pink.rabbit","虽然满足两个绑定但只被队列 Q2 接收一次");
bindingKeyMap.put("quick.brown.fox","不匹配任何绑定不会被任何队列接收到会被丢弃");
bindingKeyMap.put("quick.orange.male.rabbit","是四个单词不匹配任何绑定会被丢弃");
bindingKeyMap.put("lazy.orange.male.rabbit","是四个单词但匹配 Q2");
for(Map.Entry<String, String> entry : bindingKeyMap.entrySet()){
String routingKey = entry.getKey();
String message = entry.getValue();
channel.basicPublish(EXCHANGE_NAME, routingKey, null, message.getBytes("UTF-8"));
System.out.println("生产者发出消息: " + message);
}
}
}
2️⃣ 消费者1
package com.atguigu.rabbitmq.seven;
import com.atguigu.rabbitmq.utils.RabbitMqUtils;
import com.rabbitmq.client.Channel;
/**
* @author Bonbons
* @version 1.0
* 消费者C1,声明主题交换机与相关队列
*/
public class ReceiveLogsTopic01 {
public static final String EXCHANGE_NAME = "topic_logs";
public static void main(String[] args) throws Exception{
Channel channel = RabbitMqUtils.getChannel();
channel.exchangeDeclare(EXCHANGE_NAME, "topic");
channel.queueDeclare("Q1", false, false, false, null);
channel.queueBind("Q1", EXCHANGE_NAME, "*.orange.*");
System.out.println("等待接收消息......");
channel.basicConsume("Q1", (consumersTag, message) ->{
System.out.println("Q1控制台打印接收到的信息: " + new String(message.getBody(), "UTF-8")+ " 绑定键: " + message.getEnvelope().getRoutingKey());
},consumersTag -> {});
}
}
3️⃣ 消费者2
package com.atguigu.rabbitmq.seven;
import com.atguigu.rabbitmq.utils.RabbitMqUtils;
import com.rabbitmq.client.Channel;
/**
* @author Bonbons
* @version 1.0
* 消费者C1,声明主题交换机与相关队列
*/
public class ReceiveLogsTopic02 {
public static final String EXCHANGE_NAME = "topic_logs";
public static void main(String[] args) throws Exception{
Channel channel = RabbitMqUtils.getChannel();
channel.exchangeDeclare(EXCHANGE_NAME, "topic");
channel.queueDeclare("Q2", false, false, false, null);
channel.queueBind("Q2", EXCHANGE_NAME, "*.*.rabbit");
channel.queueBind("Q2", EXCHANGE_NAME, "lazy.#");
System.out.println("等待接收消息......");
channel.basicConsume("Q2", (consumersTag, message) ->{
System.out.println("Q2控制台打印接收到的信息: " + new String(message.getBody(), "UTF-8")+ " 绑定键: " + message.getEnvelope().getRoutingKey());
},consumersTag -> {});
}
}