1. 消息如何保障百分之百的投递成功?

什么是生产端的可靠性投递?

  • 保障消息的成功发出
  • 保障MQ节点的成功接收
  • 发送端收到MQ节点(Broker)确认应答
  • 完善的进行消息补偿机制

如果想保障消息百分百投递成功,只做到前三步不一定能够保障。有些时候或者说有些极端情况,比如生产端在投递消息时可能就失败了,或者说生产端投递了消息,MQ也收到了,MQ在返回确认应答时,由于网络闪断导致生产端没有收到应答,此时这条消息就不知道投递成功了还是失败了,所以针对这些情况我们需要做一些补偿机制。

 

1.1 方案一:消息落库,对消息状态进行打标

 

  1. 进行数据的入库,比如我们要发送一条订单消息,首先得把业务数据也就是订单信息存库,然后生成一条消息,把消息也进行入库,这条消息应该包含消息状态属性 Create_Date(创建时间),并设置初始标志 比如0,表示消息创建成功,正在发送中

  2. 首先要保证第一步消息都存储成功了,没有出现任何异常情况,然后生产端再进行消息发送。如果失败了就进行快速失败机制

  3. MQ把消息收到的结果应答(confirm)给生产端

  4. 生产端有一个Confirm Listener,去异步的监听Broker回送的响应,从而判断消息是否投递成功,如果成功,去数据库查询该消息,并将消息状态更新为1,表示消息投递成功

     

    假设第二步OK了,在第三步回送响应时,网络突然出现了闪断,导致生产端的Listener就永远收不到这条消息的confirm应答了,也就是说这条消息的状态就一直为0了

  5. 此时我们需要设置一个规则,比如说消息在入库时候设置一个临界值timeout,5分钟之后如果还是0的状态那就需要把消息抽取出来。这里我们使用的是分布式定时任务,去定时抓取DB中距离消息创建时间超过5分钟的且状态为0的消息。

  6. 把抓取出来的消息进行重新投递(Retry Send),也就是从第二步开始继续往下走

  7. 当然有些消息可能就是由于一些实际的问题无法路由到Broker,比如routingKey设置不对,对应的队列被误删除了,那么这种消息即使重试多次也仍然无法投递成功,所以需要对重试次数做限制,比如限制3次,如果投递次数大于三次,那么就将消息状态更新为2,表示这个消息最终投递失败。

 

针对这种情况如何去做补偿呢,可以有一个补偿系统去查询这些最终失败的消息,然后给出失败的原因,当然这些可能都需要人工去操作。

第一种可靠性投递,在高并发的场景下是否适合?

对于第一种方案,我们需要做两次数据库的持久化操作,在高并发场景下显然数据库存在着性能瓶颈。其实在我们的核心链路中只需要对业务进行入库就可以了,消息就没必要先入库了,我们可以做消息的延迟投递,做二次确认,回调检查。

当然这种方案不一定能保障百分百投递成功,但是基本上可以保障大概99.9%的消息是OK的,有些特别极端的情况只能是人工去做补偿了,或者使用定时任务去做都可以。

 

1.2 方案二:消息的延迟投递,做二次确认,回调检查

 

Upstream Service上游服务也就是生产端,Downstream service下游服务也就是消费端,Callback service就是回调服务。

 

  1. 先将业务消息进行入库,然后生产端将消息发送出去
  2. 在发送消息之后,紧接着生产端再次发送一条消息(Second Send Delay Check),即延迟消息投递检查,这里需要设置一个延迟时间,比如5分钟之后进行投递。
  3. 消费端去监听指定队列,将收到的消息进行处理。
  4. 处理完成之后,发送一个confirm消息,也就是回送响应,但是这里响应不是正常的ACK,而是重新生成一条消息,投递到MQ中。
  5. 上面的Callback service是一个单独的服务,其实它扮演了第一种方案的存储消息的DB角色,它通过MQ去监听下游服务发送的confirm消息,如果Callback service收到confirm消息,那么就对消息做持久化存储,即将消息持久化到DB中。
  6. 5分钟之后延迟消息发送到MQ了,然后Callback service还是去监听延迟消息所对应的队列,收到Check消息后去检查DB中是否存在消息,如果存在,则不需要做任何处理,如果不存在或者消费失败了,那么Callback service就需要主动发起RPC通信给上游服务,告诉它延迟检查的这条消息我没有找到,你需要重新发送,生产端收到信息后就会重新查询业务消息然后将消息发送出去。

这么做的目的是少做了一次DB的存储,在高并发场景下,最关心的不是消息100%投递成功,而是一定要保证性能,保证能抗得住这么大的并发量。所以能节省数据库的操作就尽量节省,可以异步的进行补偿。

 

其实在主流程里面是没有这个Callback service的,它属于一个补偿的服务,整个核心链路就是生产端入库业务消息,发送消息到MQ,消费端监听队列,消费消息。其他的步骤都是一个补偿机制。

第二种方案也是互联网大厂更为经典和主流的解决方案。但是若对性能要求不是那么高,第一种方案要更简单

 

2. 幂等性

2.1 幂等性是什么?

简单来说就是用户对于同一操作发起的一次请求或者多次请求的结果是一致的。

我们可以借鉴数据库的乐观锁机制来举个例子:

  • 首先为表添加一个版本字段version

  • 在执行更新操作前呢,会先去数据库查询这个version

  • 然后执行更新语句,以version作为条件,例如:

    UPDATE T_REPS SET COUNT = COUNT -1,VERSION = VERSION + 1 WHERE VERSION = 1

  • 如果执行更新时有其他人先更新了这张表的数据,那么这个条件就不生效了,也就不会执行操作了,通过这种乐观锁的机制来保障幂等性。

 

2.2 消息端幂等性保障

重复消费问题:

当消费者消费完消息时,在给生产端返回ack时由于网络中断,导致生产端未收到确认信息,该条消息会重新发送并被消费者消费,但实际上该消费者已成功消费了该条消息,这就是重复消费问题。

 

2.2.1 唯一ID+指纹码机制

唯一ID:业务表唯一的主键,如商品ID

指纹码:为了区别每次正常操作的码,每次操作时生成指纹码;可以用时间戳+业务编号或者标志位(具体视业务场景而定)

 

  • 唯一ID+指纹码机制,利用数据库主键去重
  • SELECT COUNT(1) FROM T_ORDER WHERE ID = 唯一ID and IS_CONSUM= 指纹码
  • 好处:实现简单
  • 坏处:高并发下有数据库写入的性能瓶颈
  • 解决方案:根据ID进行分库分表算法路由

整个思路就是首先我们需要根据消息生成一个全局唯一的ID,然后还需要加上一个指纹码。这个指纹码它并不一定是系统去生成的,而是一些外部的规则或者内部的业务规则去拼接,它的目的就是为了保障这次操作是绝对唯一的。

将ID + 指纹码拼接好的值作为数据库主键,就可以进行去重了。即在消费消息前呢,先去数据库查询这条消息的指纹码标识是否存在,没有就执行insert操作,如果有就代表已经被消费了,就不需要管了。

 

2.2.2 利用Redis的原子性去实现

这里只提用Redis的原子性去解决MQ幂等性重复消费的问题

注意:MQ的幂等性问题 根本在于的是生产端未正常接收ACK,可能是网络抖动、网络中断导致

 

我的方案:

MQ消费端在消费开始时 将 ID放入到Redis的BitMap中,MQ生产端每次生产数据时,从Redis的BitMap对应位置若不能取出ID,则生产消息发送,否则不进行消息发送。

但是有人可能会说,万一消费端,生产端Redis命令执行失败了怎么办,虽然又出现重复消费又出现Redis非正常执行命令的可能性极低,但是万一呢?

OK,我们可以在Redis命令执行失败时,将消息落库,每日用定时器,对这种极特殊的消息进行处理。

 

3. Confirm机制

3.1 如何理解?

  • 消息的确认,是指生产者投递消息后,如果Broker收到消息,则会给我们生产者一个应答
  • 生产者进行接收应答,用来确定这条消息是否正常的发送到Broker,这种方式也是消息的可靠性投递

的核心保障

 

确认机制流程图

生产端发送消息到Broker,然后Broker接收到了消息后,进行回送响应,生产端有一个Confirm Listener,去监听应答,当然这个操作是异步进行的,生产端将消息发送出去就可以不用管了,让内部监听器去监听Broker给我们的响应。

 

3.2 怎么实现?

  • 第一步,在channel上开启确认模式:channel.confirmSelect()
  • 第二步,在channel上添加监听:addConfirmListener,监听成功和失败的返回结果,根据具体的结果对消息进行重新发送、或记录日志等后续处理!
public class Producer {
public static void main(String[] args) throws Exception { //创建ConnectionFactory
ConnectionFactory connectionFactory = new ConnectionFactory();
connectionFactory.setHost("192.168.244.11");
connectionFactory.setPort(5672);
connectionFactory.setVirtualHost("/");
connectionFactory.setHandshakeTimeout(20000); //获取Connection
Connection connection = connectionFactory.newConnection(); //通过connection创建一个新的Channel
Channel channel = connection.createChannel(); //指定我们的消息投递模式
channel.confirmSelect(); String exchangeName = "test_confirm_exchange";
String routingkey = "confirm.save"; //发送一条信息
String msg = "Hello RabbitMQ Send confirm message!";
channel.basicPublish(exchangeName, routingkey, null, msg.getBytes()); //添加一个确认监听
channel.addConfirmListener(new ConfirmListener() { @Override
public void handleNack(long deliveryTag, boolean multiple)
throws IOException {
System.out.println("-------no ack!---------");
} @Override
public void handleAck(long deliveryTag, boolean multiple)
throws IOException {
System.out.println("--------ack!----------");
}
});
}
}

 

public class Consumer {
public static void main(String[] args) throws Exception{
//创建ConnectionFactory
ConnectionFactory connectionFactory = new ConnectionFactory();
connectionFactory.setHost("192.168.244.11");
connectionFactory.setPort(5672);
connectionFactory.setVirtualHost("/");
connectionFactory.setHandshakeTimeout(20000); //获取Connection
Connection connection = connectionFactory.newConnection(); //通过connection创建一个新的Channel
Channel channel = connection.createChannel(); String exchangeName = "test_confirm_exchange";
String routingkey = "confirm.#";
String queueName = "test_confirm_queue"; //声明交换机和队列 然后进行绑定和 设置 最后制定路由key
channel.exchangeDeclare(exchangeName, "topic",true);
channel.queueDeclare(queueName, true, false, false, null); channel.queueBind(queueName, exchangeName, routingkey); //创建消费者
QueueingConsumer queueingConsumer = new QueueingConsumer(channel);
channel.basicConsume(queueName, true,queueingConsumer); while(true){
Delivery delivery = queueingConsumer.nextDelivery();
String msg = new String(delivery.getBody());
System.out.println("消费端:" + msg);
}
}
}

 

运行说明

先启动消费端,访问管控台:http://ip:15672,检查Exchange和Queue是否设置OK,然后启动生产端,消息被消费端消费,生产端也成功监听到了ACK响应。

 

4. Return机制

4.1 如何理解?

  • Return Listener 用于处理一些不可路由的消息!
  • 我们的消息生产者,通过指定一个Exchange 和Routingkey,把消息送达到某一个队列中去, 然后我们的消费者监听队列,进行消费处理操作!
  • 但是在某些情况下,如果我们在发送消息的时候,当前的exchange不存在或者指定的路由key路由不到,这个时候如果我们需要监听这种不可达的消息,就要使用Return Listener!

 

4.2 如何实现?

  1. 添加return监听:addReturnListener,生产端去监听这些不可达的消息,做一些后续处理,比如说,记录下消息日志,或者及时去跟踪记录,有可能重新设置一下就好了
  2. 发送消息时,设置Mandatory:如果为true,则监听器会接收到路由不可达的消息,然后进行后续处理,如果为false,那么broker端自动删除该消息!

 

public class ReturnProducer {
public static void main(String[] args) throws Exception {
//1 创建ConnectionFactory
ConnectionFactory connectionFactory = new ConnectionFactory();
connectionFactory.setHost("192.168.244.11");
connectionFactory.setPort(5672);
connectionFactory.setVirtualHost("/");
connectionFactory.setHandshakeTimeout(20000);
//2 获取Connection
Connection connection = connectionFactory.newConnection();
//3 通过Connection创建一个新的Channel
Channel channel = connection.createChannel(); String exchange = "test_return_exchange";
//String routingKey = "return.save";
String routingKeyError = "abc.save"; String msg = "Hello RabbitMQ Return Message";
//添加return监听
channel.addReturnListener(new ReturnListener() {
@Override
public void handleReturn(int replyCode, String replyText, String exchange,
String routingKey, BasicProperties properties, byte[] body) throws IOException {
//replyCode:响应码 replyText:响应信息
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));
} });
//5 发送一条消息,第三个参数mandatory:必须设置为true
channel.basicPublish(exchange, routingKeyError, true, null, msg.getBytes());
}
}

 

public class ReturnConsumer {

    public static void main(String[] args) throws Exception {
//1 创建ConnectionFactory
ConnectionFactory connectionFactory = new ConnectionFactory();
connectionFactory.setHost("192.168.244.11");
connectionFactory.setPort(5672);
connectionFactory.setVirtualHost("/");
connectionFactory.setHandshakeTimeout(20000);
//2 获取Connection
Connection connection = connectionFactory.newConnection();
//3 通过Connection创建一个新的Channel
Channel channel = connection.createChannel(); String exchangeName = "test_return_exchange";
String routingKey = "return.#";
String queueName = "test_return_queue";
//4 声明交换机和队列,然后进行绑定设置路由Key
channel.exchangeDeclare(exchangeName, "topic", true, false, null);
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){
Delivery delivery = queueingConsumer.nextDelivery();
String msg = new String(delivery.getBody());
System.err.println("消费者: " + msg);
}
}
}

 

运行说明

先启动消费端,访问管控台:http://ip:15672,检查Exchange和Queue是否设置OK,然后启动生产端。

由于生产端设置的是一个错误的路由key,所以消费端没有任何打印,而生产端打印了如下内容

如果我们将 Mandatory 属性设置为false,对于不可达的消息会被Broker直接删除,那么生产端就不会进行任何打印了。如果我们的路由key设置为正确的,那么消费端能够正确消费,生产端也不会进行任何打印。

RabbitMQ 从入门到精通(二)的更多相关文章

  1. iOS开发-UI 从入门到精通(二)

    iOS开发-UI 从入门到精通(二)是对 iOS开发-UI 从入门到精通(一)知识点的巩固,主要以习题练习为主,增强实战经验,为以后做开发打下坚实的基础! ※开发环境和注意事项: 1.前期iOS-UI ...

  2. RabbitMQ从入门到精通

    RabbitMQ从入门到精通 学习了:http://blog.csdn.net/column/details/rabbitmq.html RabbitMQ是AMQP(advanced message ...

  3. Python基本语法,python入门到精通[二]

    在上一篇博客Windows搭建python开发环境,python入门到精通[一]我们已经在自己的windows电脑上搭建好了python的开发环境,这篇博客呢我就开始学习一下Python的基本语法.现 ...

  4. PHP从入门到精通(二)

     PHP从入门到精通 之PHP中的函数 各位开发者朋友大家好,自上次更新PHP的相关知识,得到了大家的广泛支持.PHP的火爆程度不言而喻,函数作为PHP中极为重要的部分,应诸位的支持,博主继续跟进更新 ...

  5. MyBatis从入门到精通(二):MyBatis XML方式的基本用法之Select

    最近在读刘增辉老师所著的<MyBatis从入门到精通>一书,很有收获,于是将自己学习的过程以博客形式输出,如有错误,欢迎指正,如帮助到你,不胜荣幸! 1. 明确需求 书中提到的需求是一个基 ...

  6. RabbitMQ 从入门到精通 (一)

    目录 1. 初识RabbitMQ 2. AMQP 3.RabbitMQ的极速入门 4. Exchange(交换机)详解 4.1 Direct Exchange 4.2 Topic Exchange 4 ...

  7. SpringBoot从入门到精通二(SpringBoot整合myBatis的两种方式)

    前言 通过上一章的学习,我们已经对SpringBoot有简单的入门,接下来我们深入学习一下SpringBoot,我们知道任何一个网站的数据大多数都是动态的,也就是说数据是从数据库提取出来的,而非静态数 ...

  8. RabbitMQ从入门到精通(三)

    目录 1. 自定义消费者使用 自定义消费端演示 2.消费端的限流策略 2.1 限流的场景与机制 2.2 限流相关API 2.3 限流演示 3. 消费端ACK与重回队列机制 3.1 ACK与NACK 3 ...

  9. python入门到精通[二]:基础学习(1)

    摘要:Python基础学习: 注释.字符串操作.用户交互.流程控制.导入模块.文件操作.目录操作. 上一节讲了分别在windows下和linux下的环境配置,这节以linux为例学习基本语法.代码部分 ...

随机推荐

  1. 一定要在commit之前做RAR备份,这样在出问题的时候,可以排除别人代码的干扰

    否则找错实在是太痛苦了,根本不知道来自哪里...而这样上面那样做,可以节省时间.

  2. Visual studio调试Web发生未能正常启动IIS express

    今天调试web时,不知道怎么搞的,昨天还好好的,结果今天怎么也没法调试了.VS里报的错误是进程号为**的未能正常启动,看了下调试时IIS压根就没启动起来,没关系,看看事件管理器里发生了什么 找到个最关 ...

  3. Delphi事件的广播

    原文地址:Delphi事件的广播 转作者:MondaySoftware 明天就是五一节了,辛苦了好几个月,借此机会应该尽情放松一番.可是想到Blog好久没有写文章,似乎缺些什么似的.这几个月来在项目中 ...

  4. C语言的setlocale和localtime函数(C++也可用)

    Example 1234567891011121314151617181920212223242526272829303132 /* setlocale example */ #include < ...

  5. [铁人赛] ASP.NET Core 2 系列- 从头开始

    来势汹汹的.NET Core似乎要取代.NET Framework,ASP.NET也随之发布.NET Core版本.虽说名称沿用ASP.NET,但相较于ASP.NET确有许多架构上的差异,可说是除了名 ...

  6. Memory Ordering (注意Cache带来的副作用,每个CPU都有自己的Cache,内存读写不再一定需要真的作内存访问)

    Memory Ordering   Background 很久很久很久以前,CPU忠厚老实,一条一条指令的执行我们给它的程序,规规矩矩的进行计算和内存的存取. 很久很久以前, CPU学会了Out-Of ...

  7. Qt设置窗体的透明度: setWindowOpacity

    在Qt中,设置窗体透明度的函数有:void   setWindowOpacity(qreal level)   特性: 透明度的有效范围从1.0(完全不透明)到0.0(完全透明的). 默认情况下,此属 ...

  8. Qt - 设置程序界面风格(现成的QMacStyle等等)

    类的继承关系: QMotifStyle:OSF(开放基金协会)开发的一个工业标准的GUI(图形用户接口): QCDEStyle:公共桌面环境(Common Desktop Environment)的缩 ...

  9. MotionManager 陀螺仪实现方式

      题外话:   权利的游戏开播了,引用一句话:   布兰:一个人如果还把,他还能勇敢么?   奈德:人在害怕时候的勇敢,才是真的勇敢.     回归正题:   说下关于 CMMotionManage ...

  10. LVS-DR模式部署流程

    情景一 一.环境介绍 1)RIP.VIP.DIP为同一物理网络 2)LVS Hostname:lvs eth0:DIP-192.168.3.31 eth0:0:VIP-192.168.3.10 3)R ...