消息队列MQ基础篇二
一.发布确认
- 问题产生:虽然实现持久化主要是设置队列和消息持久化,但还可能出现一种情况导致持久化失败,就是实现以上两种持久化之前就宕机,从而造成消息丢失,即未真正实现持久化
- 解决问题:生产者发送消息给Rabbitmq队列后,该队列只在真正实现了将消息保存到磁盘上(持久化)才通知生产者实现持久化成功,整个过程就叫发布确认
- 开启发布确认方法:默认未开启,具体实现如下:
channel.confirmSelect();
1.单个确认发布
- 概念:它是一种同步确认发布方式,即发布一个消息后,只有它被确认发布了,后续的消息才能继续发布
- 缺点:发布速度慢,因为如果没有确认发布的消息会阻塞后续消息的发布
- 简单测试(仅为代码功能重点部分)
int publicCount = 1000;//设置总的消息数
channel.confirmSelect();//开启发布确认
for(int i = 0; i < publicCount; i++){
String message = "" + i;
channel.basicPublish("", queueName, null, message.getBytes());
if(channel.waitForConfirms()){//判断单条消息是否确认发布成功
System.out.println("确认一条成功");
}
}
2.批量确认发布
- 概念:同单个确认发布一样是同步的,每发一批消息就统一确认发布一次,相对于单个确认发布提高了吞吐量,但缺点在于如果某一批中的消息出现了问题,无法准确判断是哪一个消息的问题
- 简单测试
int publicCount = 1000;//设置总的消息数
int bathSize = 10;//设置单批数量
for(int i = 0; i < publicCount; i++){
String message = "" + i;
channel.basicPublish("", queueName, null, message.getBytes());
if(i % bathSize == 0){//发送的消息数每达到一批就确认发布
channel.waitForConfirms();
System.out.println("确认一条成功");
}
}
3.异步确认发布
图解:
- 通过map表示channel给对应的消息内容排序的形式,这样做可以方便通过序号在全部信息发布完成后,分辨具体哪些消息确认发布,哪些没有
- broker用于接收消息后,最终回复给生产者,哪些消息确认发布,哪些没有。
- 对于生产者而言,只管发送消息即可,不用管哪些消息是否确认发布成功,因为发完信息后,还可以通过broker给生产者回复发送消息的情况,这便是异步的意思,即把发送消息和确认发布分离完成
- 简单测试
channel.confirmSelect();//开启确认发布
//消息确认成功,回调函数
ConfirmCallback ackCallback = (deliveryTag, multiple)->{
System.out.println("确认的消息:" + deliveryTag);
};
//消息确认失败,回调函数
/**
* 参数1,消息的标记
* 参数2,是否为批量确认
*/
ConfirmCallback nackCallback = (deliveryTag, multiple)->{
System.out.println("未确认的消息:" + deliveryTag);
};
//准备消息监听器,监听哪些消息成功确认发布,哪些没有
/**
* 参数1,监听哪些消息成功
* 参数2,监听哪些消息失败
*/
channel.addConfirmListener(ackCallback, nackCallback);
for(int i = 0; i < 1000; i++){
String message = "" + i;
channel.basicPublish("", queueName, null, message.getBytes());
}
4.处理异常未确认消息
- 方案:把未确认的消息放到一个基于内存的能被发布线程访问的队列,这个队列在confirm callbacks与发布线程之间进行消息传递,由于发布线程和回调线程是不同的线程,则要选用多线程相关队列
- 简单测试(存储信息队列选用ConcurrentSkipListMap):
第一步:队列记录下所有要发送的消息
第二步:删除已经确认的消息,剩下的就是未确认的消息
channel.confirmSelect();
/**
* 线程安全有序的哈希表,适用于高并发
* 1.将序号与信息进行关联
* 2.批量删除条目,只有给到序号
* 3.支持多线程
*/
ConcurrentSkipListMap<Long, String> map = new ConcurrentSkipListMap<>();
//消息确认成功,回调函数
ConfirmCallback ackCallback = (deliveryTag, multiple)->{
if(multiple){//批量处理
//删除已经确认的消息,剩下的就是未确认的消息
ConcurrentNavigableMap<Long, String> comfirmedMap = map.headMap(deliveryTag);
comfirmedMap.clear();
}else{//单个处理
map.remove(deliveryTag);
}
System.out.println("确认的消息:" + deliveryTag);
};
//消息确认失败,回调函数
/**
* 参数1,消息的标记
* 参数2,是否为批量确认
*/
ConfirmCallback nackCallback = (deliveryTag, multiple)->{
String message = map.get(deliveryTag);
System.out.println("未确认的消息:" + message + "tag:" + deliveryTag);
};
//准备消息监听器,监听哪些消息成功确认发布,哪些没有
/**
* 参数1,监听哪些消息成功
* 参数2,监听哪些消息失败
*/
channel.addConfirmListener(ackCallback, nackCallback);
long begin = System.currentTimeMillis();
for(int i = 0; i < 1000; i++){
String message = "" + i;
channel.basicPublish("", queueName, null, message.getBytes());
//此处记录下所有要发送的消息
map.put(channel.getNextPublishSeqNo(), message);
}
二.交换机
- 问题产生:原本每条消息只能被消费者消费一次,但如果想要实现一条消息被多个消费者消费,实现模式图如下:
图解:
1.通过RoutingKey把交换机和多个队列绑定
2.每个的队列的消息还是只能被消费一次,但由于一个交换机绑定了多个队列,故能实现消息被多个消费者消费 - 概念:生产者发送的消息从不会直接发送到队列,而是要先经过交换机才能到达队列,即使是之前没有设置交换机,也会使用默认交换机。一方面用于接收生产者的信息,另一方面将这些消息加入队列
- 处理方式:由交换机类型决定
1.把消息放到特定单个队列或多个队列
2.丢弃消息
1.类型一–扇形发布-订阅模式Fanout
- 概念:将接收到的所有消息广播到它知道的所有队列中,即交换机和消费者是一对多的关系
- 补充概念:临时队列,在以下代码中有创建代码,特点是取名随机,一旦断开了消费者的连接,队列会被自动删除
- 简单测试:由于测试的两个消费者代码一样,故只给出一个消费者的代码
public class producer1 {
public static final String EXCHANGE_NAME = "logs";
public static void main(String[] args) throws IOException, TimeoutException, InterruptedException {
Channel channel = RabbitMqUtils.getChannel();
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("成功发送");
}
}
}
public class consumer1 {
public static final String EXCHANGE_NAME = "logs";
public static void main(String[] args) throws IOException, TimeoutException, InterruptedException {
Channel channel = RabbitMqUtils.getChannel();
//参数1设置交换机名称,参数2设置交换机类型
channel.exchangeDeclare(EXCHANGE_NAME, "fanout");
//创建临时队列,并获取名称
String queue = channel.queueDeclare().getQueue();
channel.queueBind(queue, EXCHANGE_NAME, "");
System.out.println("等待接收消息");
DeliverCallback deliverCallback = (comsumerTag, message)->{
System.out.println("接收到消息:" + new String(message.getBody(), "UTF-8"));
};
channel.basicConsume(queue, true, deliverCallback,comsumerTag ->{});
}
}
2.类型二–直接交换机Direct exchange(路由交换机)
- 概念:根据起绑定作用的routingkey的不同,实现消费者的切换,即可以通过指定routingkey绑定相应的队列,使消息被特定的队列接收,效果图如下:
说明:上图的交换机绑定了q1和q2两个队列,绑定类型为direct,绑定q1的routingkey为orange,绑定q2的routingkey为black和green - 简单测试
public class producer1 {
public static final String EXCHANGE_NAME = "direct_logs";
public static void main(String[] args) throws IOException, TimeoutException, InterruptedException {
Channel channel = RabbitMqUtils.getChannel();
Scanner scanner = new Scanner(System.in);
while(scanner.hasNext()){
String message = scanner.next();
//参数1:交换机名;参数2:绑定键routingkey
channel.basicPublish(EXCHANGE_NAME, "info", null, message.getBytes("UTF-8"));
System.out.println("成功发送");
}
}
}
public class consumer1 {
public static final String EXCHANGE_NAME = "direct_logs";
public static void main(String[] args) throws IOException, TimeoutException, InterruptedException {
Channel channel = RabbitMqUtils.getChannel();
//声明交换机
//参数1:交换机名;参数2:交换机类型
channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.DIRECT);
channel.queueDeclare("console", false, false, false, null);
//绑定队列
//参数1:队列名;参数2:交换机名;参数3:routingkey
channel.queueBind("console", EXCHANGE_NAME, "info");
channel.queueBind("console", EXCHANGE_NAME, "warning");
System.out.println("consumer1等待接收消息");
DeliverCallback deliverCallback = (comsumerTag, message)->{
System.out.println("consumer1接收到消息:" + new String(message.getBody(), "UTF-8"));
};
channel.basicConsume("console", true, deliverCallback,comsumerTag ->{});
}
}
public class consumer2 {
public static final String EXCHANGE_NAME = "direct_logs";
public static void main(String[] args) throws IOException, TimeoutException {
Channel channel = RabbitMqUtils.getChannel();
channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.DIRECT);
channel.queueDeclare("disk", false, false, false, null);
channel.queueBind("disk", EXCHANGE_NAME, "error");
System.out.println("consumer2等待接收消息");
DeliverCallback deliverCallback = (consumerTag, message)->{
System.out.println("consumer2接收消息:" + new String(message.getBody(), "UTF-8"));
};
channel.basicConsume("disk", true, deliverCallback, consumerTag ->{});
}
}
3.类型三–主题交换机topics
-
问题产生:对于直接交换机来说,设置了routingkey后,只能绑定一个队列,如果想要同时绑定多个队列而且routingkey又各不相同,这是行不通的
-
解决:使用topic交换机
-
规范
1.routingkey必须是一个单词列表,以点号分隔开
2.替换符*(星号)可以代替一个单词;#(井号)可以代替0个或多个单词
3.单词列表最多不能超过255个字节 -
效果图
-
注意点
1.当一个队列绑定键为#,则这个队列将接收所有数据,像fanout
2.如果队列绑定键中没有#和*出现,则该队列绑定类型为direct -
简单测试:以下代码参照上面的效果图进行
public class producer1 {
public static final String EXCHANGE_NAME = "topic_logs";
public static void main(String[] args) throws IOException, TimeoutException, InterruptedException {
Channel channel = RabbitMqUtils.getChannel();
Scanner scanner = new Scanner(System.in);
while(scanner.hasNext()){
String message = scanner.next();
//自定义参数2,可参照效果图进行验证
channel.basicPublish(EXCHANGE_NAME, "quick.orange.fox", null, message.getBytes("UTF-8"));
System.out.println("成功发送");
}
}
}
public class consumer1 {
public static final String EXCHANGE_NAME = "topic_logs";
public static void main(String[] args) throws IOException, TimeoutException, InterruptedException {
Channel channel = RabbitMqUtils.getChannel();
channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.TOPIC);
channel.queueDeclare("console", false, false, false, null);
channel.queueBind("console", EXCHANGE_NAME, "lazy.#");
channel.queueBind("console", EXCHANGE_NAME, "*.*.rabbit");
System.out.println("consumer1等待接收消息");
DeliverCallback deliverCallback = (comsumerTag, message)->{
System.out.println("consumer1接收到消息:" + new String(message.getBody(), "UTF-8"));
};
channel.basicConsume("console", true, deliverCallback,comsumerTag ->{});
}
}
public class consumer2 {
public static final String EXCHANGE_NAME = "topic_logs";
public static void main(String[] args) throws IOException, TimeoutException {
Channel channel = RabbitMqUtils.getChannel();
channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.TOPIC);
channel.queueDeclare("disk", false, false, false, null);
channel.queueBind("disk", EXCHANGE_NAME, "*.orange.*");
System.out.println("consumer2等待接收消息");
DeliverCallback deliverCallback = (consumerTag, message)->{
System.out.println("consumer2接收消息:" + new String(message.getBody(), "UTF-8"));
};
channel.basicConsume("disk", true, deliverCallback, consumerTag ->{});
}
}
三.死信队列
-
概念:死信,即无法被消费的消息,在某些特定的原因导致queue中的某些信息无法被消费,这样的信息如果没有后续处理,就变成死信,而死信队列就是用来存储死信的
-
应用场景:为了保证消息数据不丢失,当消息消费发生异常,就将消息投入死信队列中,例如,用户下单成功,但在规定支付时间内未支付则会自动失效
-
死信来源
1.消息TTL(存活时间)过期
2.队列满了
3.消息被拒绝且不返回队列中 -
结构图:帮助理解以下代码的逻辑
说明:由于normal-queue和dead-queue,dead_exchange有关联,故而可以通过以下代码的map来建立联系,这也是与之前代码最大的不同 -
简单测试一(消息TTL过期)
注意点一:consumer1的map的key部分是固定写法,更改会报错
注意点二:先启动consumer1,为了创建好所需的交换机和队列,为了验证消息进入死信队列的效果,故可以关闭它了;再启动producer1进行消息发送,消息由于没有消费者,则会进入死信队列;最后开启consumer2消费死信队列
注意点三:对于已经声明过的队列或交换机,不用声明第二次,否则会报错。例如consumer1中声明了死信队列和死信交换机,那么在producer1和consumer2中就不用再次声明
public class producer1 {
public static final String EXCHANGE_NAME = "normal_exchange";
public static void main(String[] args) throws IOException, TimeoutException {
Channel channel = RabbitMqUtils.getChannel();
//死信消息设置TTL时间 单位ms
AMQP.BasicProperties properties =
new AMQP.BasicProperties()
.builder().expiration("10000").build();
for(int i = 1; i < 11; i++){
String message = "info" + i;
channel.basicPublish(EXCHANGE_NAME, "zhangsan", properties, message.getBytes());
}
}
}
public class consumer1 {
public static final String NORMAL_EXCHANGE = "normal_exchange";
public static final String DEAD_EXCHANGE = "dead_exchange";
public static final String NORMAL_QUEUE = "normal-queue";
public static final String DEAD_QUEUE = "dead-queue";
public static void main(String[] args) throws IOException, TimeoutException, InterruptedException {
Channel channel = RabbitMqUtils.getChannel();
channel.exchangeDeclare(NORMAL_EXCHANGE, BuiltinExchangeType.DIRECT);
channel.exchangeDeclare(DEAD_EXCHANGE, BuiltinExchangeType.DIRECT);
Map<String,Object> map = new HashMap<>();
//正常队列设置死信交换机
map.put("x-dead-letter-exchange",DEAD_EXCHANGE);
//设置死信Routingkey
map.put("x-dead-letter-routing-key", "lisi");
//声明正常队列
channel.queueDeclare(NORMAL_QUEUE, false, false, false, map);
//声明死信队列
channel.queueDeclare(DEAD_QUEUE, false, false, false, null);
//绑定正常交换机和正常队列
channel.queueBind(NORMAL_QUEUE, NORMAL_EXCHANGE, "zhangsan");
//绑定死信交换机和死信队列
channel.queueBind(DEAD_QUEUE, DEAD_EXCHANGE, "lisi");
DeliverCallback deliverCallback = (consumerTag,message)->{
System.out.println("consumer1接收消息:" + new String(message.getBody(),"UTF-8"));
};
channel.basicConsume(NORMAL_QUEUE, true, deliverCallback, (consumerTag)->{});
}
}
public class consumer2 {
public static final String DEAD_QUEUE = "dead-queue";
public static void main(String[] args) throws IOException, TimeoutException {
Channel channel = RabbitMqUtils.getChannel();
System.out.println("等待接收消息");
DeliverCallback deliverCallback = (consumerTag,message)->{
System.out.println("consumer2接收消息:" + new String(message.getBody(), "UTF-8"));
};
channel.basicConsume(DEAD_QUEUE, true, deliverCallback, (consumerTag)->{});
}
}
- 简单测试二(正常队列已满)
注意点:测试的时候,由于以下代码大部分与测试一相同,而区别仅在于对正常队列的设置,故而要删除原来的normal-queue,再重新用consumer2创建
public class producer1 {
public static final String EXCHANGE_NAME = "normal_exchange";
public static void main(String[] args) throws IOException, TimeoutException {
Channel channel = RabbitMqUtils.getChannel();
for(int i = 1; i < 11; i++){
String message = "info" + i;
channel.basicPublish(EXCHANGE_NAME, "zhangsan", properties, message.getBytes());
}
}
}
public class consumer1 {
public static final String NORMAL_EXCHANGE = "normal_exchange";
public static final String DEAD_EXCHANGE = "dead_exchange";
public static final String NORMAL_QUEUE = "normal-queue";
public static final String DEAD_QUEUE = "dead-queue";
public static void main(String[] args) throws IOException, TimeoutException, InterruptedException {
Channel channel = RabbitMqUtils.getChannel();
channel.exchangeDeclare(NORMAL_EXCHANGE, BuiltinExchangeType.DIRECT);
channel.exchangeDeclare(DEAD_EXCHANGE, BuiltinExchangeType.DIRECT);
Map<String,Object> map = new HashMap<>();
//正常队列设置死信交换机
map.put("x-dead-letter-exchange",DEAD_EXCHANGE);
//设置死信Routingkey
map.put("x-dead-letter-routing-key", "lisi");
//设置正常队列的最大长度为6
map.put("x-max-length", 6);
//声明正常队列
channel.queueDeclare(NORMAL_QUEUE, false, false, false, map);
//声明死信队列
channel.queueDeclare(DEAD_QUEUE, false, false, false, null);
//绑定正常交换机和正常队列
channel.queueBind(NORMAL_QUEUE, NORMAL_EXCHANGE, "zhangsan");
//绑定死信交换机和死信队列
channel.queueBind(DEAD_QUEUE, DEAD_EXCHANGE, "lisi");
DeliverCallback deliverCallback = (consumerTag,message)->{
System.out.println("consumer1接收消息:" + new String(message.getBody(),"UTF-8"));
};
channel.basicConsume(NORMAL_QUEUE, true, deliverCallback, (consumerTag)->{});
}
}
public class consumer2 {
public static final String DEAD_QUEUE = "dead-queue";
public static void main(String[] args) throws IOException, TimeoutException {
Channel channel = RabbitMqUtils.getChannel();
System.out.println("等待接收消息");
DeliverCallback deliverCallback = (consumerTag,message)->{
System.out.println("consumer2接收消息:" + new String(message.getBody(), "UTF-8"));
};
channel.basicConsume(DEAD_QUEUE, true, deliverCallback, (consumerTag)->{});
}
}
- 简单测试三(消息被拒绝)
注意点:测试的时候,由于以下代码大部分与测试一相同,但有也有些区别,最好删除原来的normal-queue(如果允许报错),再重新用consumer2创建
public class producer1 {
public static final String EXCHANGE_NAME = "normal_exchange";
public static void main(String[] args) throws IOException, TimeoutException {
Channel channel = RabbitMqUtils.getChannel();
for(int i = 1; i < 11; i++){
String message = "info" + i;
channel.basicPublish(EXCHANGE_NAME, "zhangsan", null, message.getBytes());
}
}
}
public class consumer1 {
public static final String NORMAL_EXCHANGE = "normal_exchange";
public static final String DEAD_EXCHANGE = "dead_exchange";
public static final String NORMAL_QUEUE = "normal-queue";
public static final String DEAD_QUEUE = "dead-queue";
public static void main(String[] args) throws IOException, TimeoutException, InterruptedException {
Channel channel = RabbitMqUtils.getChannel();
channel.exchangeDeclare(NORMAL_EXCHANGE, BuiltinExchangeType.DIRECT);
channel.exchangeDeclare(DEAD_EXCHANGE, BuiltinExchangeType.DIRECT);
Map<String,Object> map = new HashMap<>();
map.put("x-dead-letter-exchange",DEAD_EXCHANGE);
map.put("x-dead-letter-routing-key", "lisi");
channel.queueDeclare(NORMAL_QUEUE, false, false, false, map);
channel.queueDeclare(DEAD_QUEUE, false, false, false, null);
channel.queueBind(NORMAL_QUEUE, NORMAL_EXCHANGE, "zhangsan");
channel.queueBind(DEAD_QUEUE, DEAD_EXCHANGE, "lisi");
DeliverCallback deliverCallback = (consumerTag,message)->{
String msg = new String(message.getBody(), "UTF-8");
if(msg.equals("info5")){
System.out.println("consumer1接收消息" + msg + "被拒绝");
//channel.basicReject(消息标识,是否放回队列)
//根据以上注释参数可知,此处设置被拒绝的消息不会放回正常队列,而是进入死信队列
channel.basicReject(message.getEnvelope().getDeliveryTag(), false);
}else{
System.out.println("consumer1接收消息:" + new String(message.getBody(),"UTF-8"));
//channel.basicAck(消息标识,是否批量应答)
channel.basicAck(message.getEnvelope().getDeliveryTag(), false);
}
};
channel.basicConsume(NORMAL_QUEUE, false, deliverCallback, (consumerTag)->{});
}
}
public class consumer2 {
public static final String DEAD_QUEUE = "dead-queue";
public static void main(String[] args) throws IOException, TimeoutException {
Channel channel = RabbitMqUtils.getChannel();
System.out.println("等待接收消息");
DeliverCallback deliverCallback = (consumerTag,message)->{
System.out.println("consumer2接收消息:" + new String(message.getBody(), "UTF-8"));
};
channel.basicConsume(DEAD_QUEUE, true, deliverCallback, (consumerTag)->{});
}
}