互联网面试必杀:如何保证消息中间件全链路数据100%不丢失:第二篇

前情提示

上一篇文章《互联网面试必杀:如何保证消息中间件全链路数据100%不丢失:第一篇》,咱们初步介绍了以前制定的那些消息中间件数据不丢失的技术方案遗留的问题。html

一个最大的问题,就是生产者投递出去的消息,可能会丢失。java

丢失的缘由有不少,好比消息在网络传输到一半的时候由于网络故障就丢了,或者是消息投递到MQ的内存时,MQ突发故障宕机致使消息就丢失了。面试

针对这种生产者投递数据丢失的问题,RabbitMQ其实是提供了一些机制的。服务器

好比,有一种重量级的机制,就是事务消息机制。采用类事务的机制把消息投递到MQ,能够保证消息不丢失,可是性能极差,通过测试性能会呈现几百倍的降低。微信

因此说如今通常是不会用这种过于重量级的机制,而是会用轻量级的confirm机制网络

可是咱们这篇文章还不能直接讲解生产者保证消息不丢失的confirm机制,由于这种confirm机制其实是采用了相似消费者的ack机制来实现的。架构

因此,要深刻理解confirm机制,咱们得先从这篇文章开始,深刻的分析一下消费者手动ack机制保证消息不丢失的底层原理。性能

ack机制回顾

其实手动ack机制很是的简单,必需要消费者确保本身处理完毕了一个消息,才能手动发送ack给MQ,MQ收到ack以后才会删除这个消息。测试

若是消费者还没发送ack,本身就宕机了,此时MQ感知到他的宕机,就会从新投递这条消息给其余的消费者实例。3d

经过这种机制保证消费者实例宕机的时候,数据是不会丢失的。

再次提醒一下你们,若是还对手动ack机制不太熟悉的同窗,能够回头看一下以前的一篇文章:《扎心!线上服务宕机时,如何保证数据100%不丢失?》。而后这篇文章,咱们将继续深刻探讨一下ack机制的实现原理。

ack机制实现原理:delivery tag

若是你写好了一个消费者服务的代码,让他开始从RabbitMQ消费数据,这时这个消费者服务实例就会本身注册到RabbitMQ。

因此,RabbitMQ实际上是知道有哪些消费者服务实例存在的。

你们看看下面的图,直观的感觉一下:

接着,RabbitMQ就会经过本身内部的一个“basic.delivery”方法来投递消息到仓储服务里去,让他消费消息。

投递的时候,会给此次消息的投递带上一个重要的东西,就是“delivery tag”,你能够认为是本次消息投递的一个惟一标识。

这个所谓的惟一标识,有点相似于一个ID,好比说消息本次投递到一个仓储服务实例的惟一ID。经过这个惟一ID,咱们就能够定位一次消息投递。

因此这个delivery tag机制不要看很简单,实际上他是后面要说的不少机制的核心基础。

并且这里要给你们强调另一个概念,就是每一个消费者从RabbitMQ获取消息的时候,都是经过一个channel的概念来进行的。

你们回看一下下面的消费者代码片断,咱们必须是先对指定机器上部署的RabbitMQ创建链接,而后经过这个链接获取一个channel。

ConnectionFactory factory = new ConnectionFactory();
factory.setHost("localhost");
Connection connection = factory.newConnection();
Channel channel = connection.createChannel();

并且若是你们还有点印象的话,咱们在仓储服务里对消息的消费、ack等操做,所有都是基于这个channel来进行的,channel又有点相似因而咱们跟RabbitMQ进行通讯的这么一个句柄,好比看看下面的代码:

另外这里提一句:以前写那篇文章讲解手动ack保证数据不丢失的时候,有不少人提出疑问:为何上面代码里直接是try finally,若是代码有异常,那仍是会直接执行finally里的手动ack?其实很简单,本身加上catch就能够了。

好的,我们继续。你大概能够认为这个channel就是进行数据传输的一个管道吧。对于每一个channel而言,一个“delivery tag”就能够惟一的标识一次消息投递,这个delivery tag大体而言就是一个不断增加的数字。

你们来看看下面的图,相信会很好理解的:

若是采用手动ack机制,实际上仓储服务每次消费了一条消息,处理完毕完成调度发货以后,就会发送一个ack消息给RabbitMQ服务器,这个ack消息是会带上本身本次消息的delivery tag的。

我们看看下面的ack代码,是否是带上了一个delivery tag?

channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);

而后,RabbitMQ根据哪一个channel的哪一个delivery tag,不就能够惟必定位一次消息投递了?

接下来就能够对那条消息删除,标识为已经处理完毕。

这里你们必须注意的一点,就是delivery tag仅仅在一个channel内部是惟一标识消息投递的。

因此说,你ack一条消息的时候,必须是经过接受这条消息的同一个channel来进行。

你们看看下面的图,直观的感觉一下。

其实这里还有一个很重要的点,就是咱们能够设置一个参数,而后就批量的发送ack消息给RabbitMQ,这样能够提高总体的性能和吞吐量。

好比下面那行代码,把第二个参数设置为true就能够了。

channel.basicAck(delivery.getEnvelope().getDeliveryTag(), true);

看到这里,你们应该对这个ack机制的底层原理有了稍微进一步的认识了。起码是知道delivery tag是啥东西了,他是实现ack的一个底层机制。

而后,咱们再来简单回顾一下自动ack、手动ack的区别。

实际上默认用自动ack,是很是简单的。RabbitMQ只要投递一个消息出去给仓储服务,那么他立马就把这个消息给标记为删除,由于他是无论仓储服务到底接收到没有,处理完没有的。

因此这种状况下,性能很好,可是数据容易丢失。

若是手动ack,那么就是必须等仓储服务完成商品调度发货之后,才会手动发送ack给RabbitMQ,此时RabbitMQ才会认为消息处理完毕,而后才会标记消息为删除。

这样在发送ack以前,仓储服务宕机,RabbitMQ会重发消息给另一个仓储服务实例,保证数据不丢。

RabbitMQ如何感知到仓储服务实例宕机

以前就有同窗提出过这个问题,可是其实要搞清楚这个问题,其实不须要深刻的探索底层,只要本身大体的思考和推测一下就能够了。

若是你的仓储服务实例接收到了消息,可是没有来得及调度发货,没有发送ack,此时他宕机了。

咱们想想就知道,RabbitMQ以前既然收到了仓储服务实例的注册,所以他们之间必然是创建有某种联系的。

一旦某个仓储服务实例宕机,那么RabbitMQ就必然会感知到他的宕机,并且对发送给他的还没ack的消息,都发送给其余仓储服务实例。

因此这个问题之后有机会咱们能够深刻聊一聊,在这里,你们其实先创建起来这种认识便可。

咱们再回头看看下面的架构图:

仓储服务处理失败时的消息重发

首先,咱们来看看下面一段代码:

假如说某个仓储服务实例处理某个消息失败了,此时会进入catch代码块,那么此时咱们怎么办呢?难道仍是直接ack消息吗?

固然不是了,你要是仍是ack,那会致使消息被删除,可是实际没有完成调度发货。

这样的话,数据不是仍是丢失了吗?所以,合理的方式是使用nack操做。

就是通知RabbitMQ本身没处理成功消息,而后让RabbitMQ将这个消息再次投递给其余的仓储服务实例尝试去完成调度发货的任务。

咱们只要在catch代码块里加入下面的代码便可:

channel.basicNack(delivery.getEnvelope().getDeliveryTag(),  true);

注意上面第二个参数是true,意思就是让RabbitMQ把这条消息从新投递给其余的仓储服务实例,由于本身没处理成功。

你要是设置为false的话,就会致使RabbitMQ知道你处理失败,可是仍是删除这条消息,这是不对的。

一样,咱们仍是来一张图,你们一块儿来感觉一下:

阶段总结

这篇文章对以前的ack机制作了进一步的分析,包括底层的delivery tag机制,以及消息处理失败时的消息重发。

经过ack机制、消息重发等这套机制的落地实现,就能够保证一个消费者服务自身忽然宕机、消息处理失败等场景下,都不会丢失数据。

互联网面试必杀:如何保证消息中间件全链路数据100%不丢失:第一篇
互联网面试必杀:如何保证消息中间件全链路数据100%不丢失:第三篇
互联网面试必杀:如何保证消息中间件全链路数据100%不丢失:第四篇

来源:【微信公众号 - 石杉的架构笔记】

相关文章
相关标签/搜索