RocketMQ 如何保证消息不丢失(一)

一、RocketMQ如何保证消息的不丢失
​消息的流转是通过Producer发送消息到Broker,然后Consumer再到Broker上拉取消息。

  • 生产者:Producer发送消息
  • 消息队列:RocketMQ Broker存储消息
  • 消费者:Consumer消费消息

要保证消息不丢失,Producer需要保证消息一定完整的发送到Broker。Broker一定要保证持久化存储的消息不丢失。Consumer要保证自己拉取的消息一定被消费。
哪些环节可能会丢失消息

1,2,4三个场景都是跨网络的,而跨网络就肯定会有丢消息的可能。然后在3这个环节,通常MQ存盘时都会先写入操作系统的缓存page cache中,然后再由操作系统异步的将消息写图硬盘。这个中间有个时间差,就可能会造成消息丢失。如果服务器挂了,缓存中还没有来的及写入硬盘的消息就会丢失。
Producer如何保证消息不丢失
​ 生产者发送消息之后,给生产者一个确定的通知,这个消息在Broker端是否写入完成了。
1、生产者发送消息确认机制
在RocketMQ中,提供了三种不同的发送消息的方式:

//异步发送,不需要broker确认,效率高,但是会有丢消息的可能。
producer.sendOneway(msg);
//同步发送,生产者等待Broker确认。消息最安全,但效率最低 SendResult sendResult = producer.send(msg,20*1000);
//异步发送,生产者另起一个线程等待broker确认,收到Broker确认之后直接触发回调方法。消息安全和效率之间比较均衡,但是会加大客户端的负担。 producer.send(msg,new SendCallback(){ @Override public void onSuccess(SendResult sendResult){ //do something } @Override public void onException(Throwable e){ //do something } })

与之类似的,Kafka同样也提供了这种同步和异步的发送消息机制

//直接send发送消息,返回的是一个Future。这就相当于是异步调用
Future<RecordMetadata> future = producer.send(record);
//调用future的get方法才会世纪获取到发送的结果,生产者收到这个结果后,就可以知道消息是否成功发送到broker了。这个过程就变成了一个同步的过程 RecordMetadata recordMetadata = producer.send(record).get();

而在RabbitMQ中,则是提供了一个Publisher Confirms生产者确认机制。其思路也是Publisher收到Broker的响应后再发出对应的回调方法。

//获取channel
Channel ch = ...;
//添加两个回调,一个处理ack响应,一个处理nack响应
ch.addConfirmListener(ConfirmCallback ackCallback,ConfirmCallback nackCallback)

2、RocketMQ的事务消息机制

 

3、broker写入数据如何保证不丢失
​Producer把消息发送到Broker上了之后,Broker是不是能保证消息不丢失呢?这里有一个核心问题——PageCache缓存,数据会优先写入缓存,然后过一段时间再写图到磁盘。但是缓存中的数据有个特点,就是断点即丢失,所以,如果服务器发生非正常断电,内存中的数据还没有写入磁盘,这是就会造成消息丢失。

以Linux为例,用户态就是应用程序,不管是什么应用程序,想要写入磁盘文件时,都只能调用操作系统提供的write系统调用,申请写磁盘。至于消息如何经过PageCache再写入到磁盘中,这个过程就是在内核态执行的,也就是操作系统自己执行的,应用程序无法干预。这个过程中,应用系统唯一能够干预的,就是调用操作系统提供的sync系统调用,申请一次刷盘操作,主动将PageCache中的数据写入到磁盘。
4、Broker主从同步如何保证不丢失
​对于Broker来说,通常Slaver的作用就是做一个数据备份,当Broker服务宕机时,甚至是磁盘坏了时,可以从Slaver上获取数据记录。但是,如果主从同步失败了,那么Broker的这一层保证就会失效。因此,主从同步也有可能造成消息的丢失。

在这种集群机制下,消息的安全性还是比较高的。但是有一种极端的情况需要考虑。因为消息需要从Master网slaver同步,这个过程是跨网络的,因此也是有时间延迟的。所以,如果Master出现非正常崩溃,那么就有可能有一部分数据是已经写入到了Master但是还没来得及同步slave。这一部分未来得及同步的数据,在RocketMQ的这种集群机制下,就会一致记录在Master节点上。等到重启Master后,就可以继续同步了。另外由于Slave并不会主动切换成Master,所以Master服务崩溃后,也不回有新的消息写进来,因此也不会有消息冲突的问题。所以,只要Master的磁盘没有坏,那么在这种普通集群下,主从同步通常不会造成消息丢失。

5、Consumer如何保证消费不丢失
​ 几乎所有的MQ产品都设置了消费者消费者状态确认机制。也就是消费者处理完消息之后,需要给Broker一个响应,表示消息被正常处理了。如果Broker没有拿到这个响应,不管是因为Consumer没有拿到还是Consumer处理完消息后咩有给出响应,Broker都会认为消息没有处理成功。之后,Broker就会向Consumer重复投递这些没有处理成功的消息。
6、如果MQ服务全挂了,如何保证不丢失
​针对这种情况,通常做法是设计一个降级缓存。Producer往MQ发消息失败了,就往降级缓存中写,然后,依然正常去进行后续的业务。此时,再启动一个线程,不断尝试将降级缓存中的数据往MQ中发送。这样,至少当MQ服务恢复之后,这些消息可以尽快进入到MQ中,继续往下游Consumer推送,而不至于造成消息丢失。

7、MQ消息零丢失方案总结
以下防止丢失的方案,都是以增加集群负载,降低吞吐为代价的。这必然会造成集群效率下降。

二、RocketMQ如何保证消息的顺序性
消息顺序性主要指的是确保同一组消息在消费时能够按照发送的顺序被消费。讨论顺序性通常指的是局部有序,而不是全局有序。就好比QQ和微信聊天通常只要保证同一个聊天窗口内的消息是严格有序的。至于不用窗口之间的消息,顺序出了点偏差,其实是无所谓的。
RocketMQ的顺序消费机制

这个机制需要两个方面的保障。

  • Producer将一组有序的消息写入到同一个MessageQueue中。
  • Consumer每次集中从一个Message Queue中拿取消息。
  • ​在Producer端,RocketMQ和Kafka都提供了分区计算机制,可以让应用程序自己决定消息写入到哪一个分区。所以这一块,是由业务自己决定的。只要通过定制数据分片算法,把一组局部有序的消息发到同一个队列当中,就可以通过队列的FIFO特性,保证消息的处理顺序。对于RabbitMQ,则可以通过维护Exchange与Queue之间的绑定关系,将这一组局部有序的消息转发到同一个队列中,从而保证这一组有序的消息,在RabbitMQ内部保存时,是有序的。
  • 在Consumer端,RocketMQ是通过让Consumer注入不同的消息监听器来进行区分的。而具体的实现机制核心是通过对Consumer的消费线程进行并发控制,来保证消息的消费顺序的。

三、RocketMQ如何保证消息幂等性
1、生产中发送消息到服务端如何保值幂等
Producer发送消息,如果采用大宋着确认机制,那么Producer发送消息会等待Broker的响应。如果没有收到Broker的响应,Producer就会发起重试,但是,Producer没有收到Broker响应,也有可能是Broker已经正常处理完了消息,只不过发给Producer的响应请求丢失了。这个时候Producer再次发起消息重试,就有可能造成消息重复。​RocketMQ的处理方式,是会在发送消息时,给每条消息分配一个唯一的id。

 

//org.apache.rocketmq.client.impl.producer.DefaultMQProducerImpl#sendKernelImpl
//for MessageBatch,ID has been set in the generating process
if(!(msg instanceof MessageBatch)){
    MessageClientIDsetter.setUniqID(msg);
}
public static void setUniqID(final Message msg){ if(msg.getProperty(Mdssage.PROPERTY_UNIQ_CLIENT_MESSAGE_ID_KEYIDX)==null){ msg.putProperty(Message.PROPERTY_UNIQ_CLIENT_MESSAGE_ID_KEYIDX,createUniqID()) } }

 

通过这个ID,就可以判断消息是否重复投递。而对于Kafka,则会通过他的幂等性配置,防止生产者重复投递消息造成的幂等性问题。在Kafka中,需要打开idempotence幂等性控制后(默认是打开的,但是如果配置有冲突,会影响幂等性配置)。Kafka为了保证消息发送的Exactly- once语义,增加了几个概念:

  • PID:每个新的Producer在初始化的过程中就会被分配一个唯一的PID。这个PID对用户是不可见的。
  • Sequence Number:对于每个PID,这个Producer针对Partition会维护一个Sequence NUmber,这是一个从0开始单调递增的数字。当Producer要往同一个Partition发送消息时,这个Sequence Number就会加一,然后会随着消息一起发往Broker。
  • Broker端则会针对每个<PID,Partiton>维护一个序列号SN,只有当对应的Sequence Number = SN+1时,Broker才会接收消息,同时将SN更新为SN+1,否则,Sequence Number过小就会认为消息已写入了,不需要再重复写入。而如果Sequence Number过大,就会认为中间可能有数据丢失。对生产者就会抛出一个OutOfOrderSequenceException。

 

2、消费者消费如何保持幂等
RocketMQ确保所有消息至少传递一次。在大多数情况下,消息不会重复。也就是说,在大多数情况下,不需要单独考虑消息重复消费的问题。同样也说明存在一些小概率情况,需要单独考虑消费者的幂等性问题。比如当网络出现波动的时候。RocketMQ是通过消费者的响应机制来推进offset的,如果consumer从broker上获取了消息,正常处理之后,他要往broker返回一个响应,但是如果网络出现波动,consumer从broker上拿到了消息,但是等到他向broker发响应时,发生网络波动,这个响应丢失了,那么就会造成消息的重复消费。因为broker没有收到响应,就会向这个Consumer所在的Group重复投递消息。
然后,消费者如何防止重复消费?防止重复消费,最主要是要找到一个唯一性的指标。在RocketMQ中,Producer发出一条消息后,RocketMQ内部会给每一条消息分配一个唯一的messageId。而这个messageId在Consumer中是可以获取到的。所以大多数情况下,这个messageId就是一个很好的唯一性指标。Consumer只要将处理过的messageId记录下来,就可以判断这条消息之前有没有处理过。但是同样也有一些特殊情况。如果Producer是采用批量发送,或者是事务消息机制发送,那么这个messageId就没有那么好控制了。所以,如果在真实业务中,更建议根据业务场景来确定唯一指标。例如,在电商下单的场景,订单ID就是一个很好的带有业务属性的唯一性指标。在使用RocketMQ时,可以使用message的key属性来传递订单ID。这样Consumer就能够比较好的防止重复消费。最后,对于幂等性问题,除了要防止重复消费外,还需要防止消费丢失。也就是Consumer一直没有正常消费消息的情况。在RocketMQ中,重复投递的消息,会单独放到以消费者组为维度构建的重试队列中。如果经过多次重试后还是无法被正常消费,那么最终消息会进入到消费者组对应的死信队列中。也就是说,如果RocketMQ中出现了死信队列,就意味着有一批消费者的逻辑是一直有问题的。这些消息始终无法正常消费,这时就需要针对死信队列,单独维护一个消费者,对这些错误的业务消息进行补充处理。这里需要注意一下的是,RocketMQ中的死信队列,默认权限是无法消费的,需要手动调整权限才能正常。

 

posted @ 2024-12-05 09:38  郭慕荣  阅读(15)  评论(0编辑  收藏  举报