消息模式 之 消息100%投递、幂等性、Confirm确认消息、Return返回消息、自定义消费者
本章知识:
1、消息如何保证100%的投递?
2、幂等性概念
3、Confirm 确认消息
4、Return 返回消息
5、自定义消费者
消息100%的投递
可参考 -------- RabbitMQ 快速入门:https://blog.csdn.net/qq_44641053/article/details/103401964
如何保障消息100%的投递成功?
什么意思呢?
就比如说我们购物下单,用户在下单后,有订单服务要发送消息到MQ系统,并不是消息发送过去了就是投递成功了,还要在发送端收到 MQ 节点后,由Confirm 确认消息;
当然,不可能是每条消息都会投递成功;那么,那些没有投递成功的消息该怎么办呢?
这就需要用到补偿机制(说白了有点像回调机制,消息回滚),由补偿机制进行处理,确保消息能够投递成功。
( .o ~ ′ ︶ ` ~ o. )
什么是生产端的可靠性投递?
-
保障消息的成功发出
-
保障MQ节点的成功接收
-
发送端收到MQ节点(Broker)确认应答
-
完善的消息进行补偿机制
BAT/TMD 互联网大厂的两种解决方案:
-
解决方案一:消息落库,对消息状态进行打标。
-
解决方案二:消息的延迟投递,做二次确认,回调检查。
注意:BAT/TMD 指的是 百度,阿里,腾讯 / 头条,美团,滴滴
消息落库,对消息状态进行打标:
消息的延迟投递,做二次确认,回调检查:
我们一般用这种方案。ヽ(○ ′ ▽ `○)~
幂等性概念
幂等性是什么?
我们可以借鉴 数据库的乐观锁机制。
比如我们执行一条更新库存的SQL语句:
Update t_repository set count = count -1,version = version + 1 where version = 1
Elasticsearch 也是严格遵循幂等性概念,每次数据更新,version+1
消费端 ------ 幂等性保障:
在海量订单产生的业务高峰期,如何 避免消息的重复消费 问题?
消费实现幂等性,就意味着,我们的 消息永远不会消费多次 ,即使我们收到了多条一样的消息,消息也没有多次消费。
业界主流的幂等性操作:
-
唯一ID+指纹码机制,利用数据库主键去重
利用数据库主键去重
Select count(1) from T_order where ID=唯一ID+指纹码
好处:实现简单
坏处:高并发下有数据库写入的性能瓶颈
解决方案:根据ID进行分库分表进行算法路由 -
利用Redis的原子性去实现
使用Redis进行幂等,需要考虑的问题:
-
是否要 进行数据落库,如果落库的话,关键解决的问题是 数据库和缓存如何做到原子性?
-
如果 不进行落库,那么 都存储到缓存中,如何设置定时同步策略?
-
Confirm确认消息
理解 Confirm 消息确认机制:
消息的确认,是指生产者投递消息后,如果 Broker 收到消息,则会给生产者一个应答。
生产者进行接收应答,用来确定这条消息是否正常的发送到 Broker,这种方式 是消息的可靠性投递的核心保障。
如何实现 Confirm 确认消息?
步骤:
-
第一步:在 Channel 上开启确认模式:
channel.confirmSelect()
-
第二步:在 Channel 上添加监听:
addConfirmListener,监听成功和失败的返回结果,根据具体的结果对消息进行重新发送、或记录日志等后续处理!
消费端代码:
package com.dj.rabbitmqapi.confirm;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import com.rabbitmq.client.QueueingConsumer;
/**
* 消费端
*
* 注意:生产者发消息给交换机,消费者从队列消费消息
*/
public class Consumer {
public static void main(String[] args) throws Exception {
//1 创建ConnectionFactory
ConnectionFactory connectionFactory = new ConnectionFactory();
connectionFactory.setHost("192.168.153.130");
connectionFactory.setPort(5672);
connectionFactory.setVirtualHost("/");
//2 获取C onnection
Connection connection = connectionFactory.newConnection();
//3 通过Connection创建一个新的Channel
Channel channel = connection.createChannel();
String exchangeName = "test_confirm_exchange"; //交换机
String routingKey = "confirm.#";//制定规则
String queueName = "test_confirm_queue";//队列
//4 声明交换机和队列 然后进行绑定设置, 最后制定路由Key
channel.exchangeDeclare(exchangeName, "topic", true);
channel.queueDeclare(queueName, true, false, false, null);
channel.queueBind(queueName, exchangeName, routingKey);
//5 创建消费者
QueueingConsumer queueingConsumer = new QueueingConsumer(channel);
channel.basicConsume(queueName, true, queueingConsumer);
while(true){
QueueingConsumer.Delivery delivery = queueingConsumer.nextDelivery();
String msg = new String(delivery.getBody());
System.err.println("消费端: " + msg);
}
}
}
服务端代码:
package com.dj.rabbitmqapi.confirm;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.ConfirmListener;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import java.io.IOException;
/**
* 生产端
*
* 注意:生产者发消息给交换机,消费者从队列消费消息
*/
public class Producer {
public static void main(String[] args) throws Exception {
//1 创建ConnectionFactory
ConnectionFactory connectionFactory = new ConnectionFactory();
connectionFactory.setHost("192.168.153.130");
connectionFactory.setPort(5672);
connectionFactory.setVirtualHost("/");
//2 获取C onnection
Connection connection = connectionFactory.newConnection();
//3 通过Connection创建一个新的Channel
Channel channel = connection.createChannel();
//4 指定我们的消息投递模式: 消息的确认模式
channel.confirmSelect();
String exchangeName = "test_confirm_exchange";
String routingKey = "confirm.save";
//5 发送一条消息
String msg = "Hello RabbitMQ Send confirm message!";
channel.basicPublish(exchangeName, routingKey, null, msg.getBytes());//发送消息的过程
//6 添加一个确认监听,监听消息是否到达
channel.addConfirmListener(new ConfirmListener() {
@Override
public void handleNack(long deliveryTag, boolean multiple) throws IOException {
System.err.println("------- 消息未到达: no ack! -----------");
}
@Override
public void handleAck(long deliveryTag, boolean multiple) throws IOException {
System.err.println("------- 消息已到达: ack! -----------");
//发送短信的业务逻辑代码写在这里面
}
});
}
}
测试:
这里要先用 Docker 启动容器:
🆗 ,我们来测验一下Comfirm机制:
注意:要先启动Consumer,再启动Producer!!!
如果是先启动Producer的话,会报错,因为没有队列。
按顺序启动:
Return返回消息
Return Listener 用于处理一些不可路由的消息!
正常情况:
我们的消息生产者,通过指定一个 Exchange(交换机) 和 RoutingKey ,把消息送达到某一个队列中去,然后我们的消费者监听队列,进行消费处理操作!
异常情况:
在某些情况下,如果我们在发送消息的时候,当前的 Exchange 不存在或者指定的路由key路由不到,这个时候如果我们需要监听这种不可达的消息,就需要使用 Return Listener!
在基础 API 中有一个关键的配置项
Mandatory:
如果为true,则监听器会接收到路由不可达的消息,然后进行后续处理;(我们开发一般是用这个)
如果为false,那么Broker端自动删除该消息!
消费端代码:
package com.dj.rabbitmqapi.returnLinstener;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import com.rabbitmq.client.QueueingConsumer;
/**
* 消费端
*/
public class Consumer {
public static void main(String[] args) throws Exception {
ConnectionFactory connectionFactory = new ConnectionFactory();
connectionFactory.setHost("192.168.153.130");
connectionFactory.setPort(5672);
connectionFactory.setVirtualHost("/");
Connection connection = connectionFactory.newConnection();
Channel channel = connection.createChannel();
String exchangeName = "test_return_exchange";//交换器
String routingKey = "return.#";//规则
String queueName = "test_return_queue";//队列
channel.exchangeDeclare(exchangeName, "topic", true, false, null);
channel.queueDeclare(queueName, true, false, false, null);
channel.queueBind(queueName, exchangeName, routingKey);
QueueingConsumer queueingConsumer = new QueueingConsumer(channel);
channel.basicConsume(queueName, true, queueingConsumer);
while(true){
QueueingConsumer.Delivery delivery = queueingConsumer.nextDelivery();
String msg = new String(delivery.getBody());
System.err.println("消费者: " + msg);
}
}
}
生产端代码:
package com.dj.rabbitmqapi.returnLinstener;
import com.rabbitmq.client.*;
import java.io.IOException;
/**
* 生产端
*/
public class Producer {
public static void main(String[] args) throws Exception {
ConnectionFactory connectionFactory = new ConnectionFactory();
connectionFactory.setHost("192.168.153.130");
connectionFactory.setPort(5672);
connectionFactory.setVirtualHost("/");
Connection connection = connectionFactory.newConnection();
Channel channel = connection.createChannel();
String exchange = "test_return_exchange";
String routingKey = "return.save";//正确的routingKey
String routingKeyError = "abc.save";//错误的routingKey
String msg = "Hello RabbitMQ Return Message";
channel.addReturnListener(new ReturnListener() {
@Override
public void handleReturn(int replyCode, String replyText, String exchange,
String routingKey, AMQP.BasicProperties properties, byte[] body) throws IOException {
System.err.println("---------handle return----------");
System.err.println("replyCode: " + replyCode);
System.err.println("replyText: " + replyText);
System.err.println("exchange: " + exchange);
System.err.println("routingKey: " + routingKey);
System.err.println("properties: " + properties);
System.err.println("body: " + new String(body));
}
});
//消息投递成功,会被消费者所消费
// channel.basicPublish(exchange, routingKey, true, null, msg.getBytes());
//消息不可达,将触发ReturnListener
channel.basicPublish(exchange, routingKeyError, true, null, msg.getBytes());
}
}
测试:
启动消费端:
启动生产端:
消息不可达:
消息可达:
由此可见,Return Listener 会保证信息一定能发送成功(重新投递)!
自定义消费者
我们一般就是在代码中编写 while 循环,进行 consumer.nextDelivery 方法进行获取下一条消息,然后进行消费处理!
但是我们使用自定义的 Consumer 更加的方便,解耦性更加的强,也是实际工作中最常用的使用方式!
自定义消费端代码:
package com.dj.rabbitmqapi.consumer;
import com.rabbitmq.client.AMQP;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.DefaultConsumer;
import com.rabbitmq.client.Envelope;
import java.io.IOException;
/**
* 自定义消费端
*/
public class MyConsumer extends DefaultConsumer {
public MyConsumer(Channel channel) {
super(channel);
}
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
//写的是处理消息的方案
System.err.println("----------- consume message ----------");
System.err.println("consumerTag: " + consumerTag);
System.err.println("envelope: " + envelope);
System.err.println("properties: " + properties);
System.err.println("body: " + new String(body));
}
}
消费端调用:
package com.dj.rabbitmqapi.consumer;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
/**
* 消费端调用
*/
public class Consumer {
public static void main(String[] args) throws Exception {
ConnectionFactory connectionFactory = new ConnectionFactory();
connectionFactory.setHost("192.168.153.130");
connectionFactory.setPort(5672);
connectionFactory.setVirtualHost("/");
Connection connection = connectionFactory.newConnection();
Channel channel = connection.createChannel();
String exchangeName = "test_consumer_exchange";
String routingKey = "consumer.#";
String queueName = "test_consumer_queue";
channel.exchangeDeclare(exchangeName, "topic", true, false, null);
channel.queueDeclare(queueName, true, false, false, null);
channel.queueBind(queueName, exchangeName, routingKey);
//之前是通过是 while 死循环处理
//现在是通过自己定义的消费者去处理
channel.basicConsume(queueName, true, new MyConsumer(channel));
}
}
生产端调用:
package com.dj.rabbitmqapi.consumer;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
/**
* 生产端调用
*/
public class Producer {
public static void main(String[] args) throws Exception {
ConnectionFactory connectionFactory = new ConnectionFactory();
connectionFactory.setHost("192.168.153.130");
connectionFactory.setPort(5672);
connectionFactory.setVirtualHost("/");
Connection connection = connectionFactory.newConnection();
Channel channel = connection.createChannel();
String exchange = "test_consumer_exchange";//指定consumer交换机
String routingKey = "consumer.save";
String msg = "Hello RabbitMQ Consumer Message";
for(int i =0; i<5; i ++){
channel.basicPublish(exchange, routingKey, true, null, msg.getBytes());
}
}
}
测试:
启动消费端:
启动生产端:
~o(°~ω~°)0~\ノ完结撒花!
本章小结:
-
消息的100%投递
消息中间件:生产者、Broker、xiaofeizhe
确保消息到达:Confirm机制、Return机制
Confirm机制
生产端添加 ConfirmListener nack ack
Return机制
生产端添加ReturnListener nack ack
补偿机制:…
-
幂等性的概念
乐观锁 version
-
自定义消费者
消费者 extends DefaultConsumer
在重写的方法中加处理消息的逻辑