RocketMQ如何保证消息可靠性

453 阅读5分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第10天,点击查看活动详情

简介

消息的可靠性指的是最大程度保证发送的消息不丢失。

消息发送流程

图片.png

从消息发送流程中可以看出,消息发送失败可能存在如下几个场景中。

  1. 生产者发送消息到Broker。可能由于网络或者Broker的问题导致消息发送失败。
  2. Broker接收到消息暂存到内存中,Consumer还没来得及消费,Broker异常了。
  3. 消费者消费消息失败。

Produce发送消息到Broker。可能因为网络或者Broker的问题导致消息发送失败

生产者发送消息失败,会根据设置的重试策略进行重试,RocketMq重试具体逻辑如下:

  • 如果生产者发送时本身产生异常,则消息将不会重试。
  • 如果发送为同步消息异常,则线程进行轮询到下一个Broker发送,如果是异步消息发送异常,则Broker机进行重试,重试执行方法的时间不能超过sendMsgTimeout设置的值,默认为10秒。
  • RocketMq默认重试次数为2次,客户端可以进行设置
     rocketmq:
       # 生产者配置 
       producer:
         # 发送消息失败重试次数,默认2
         retryTimesWhenSendFailed: 3
    

业务扩展保证消息可靠性

业务系统中,系统对消息的可靠性要求比较严格,虽然提供了重试机制,但业务还需要进行特殊处理保。添加业务消息日志记录表记录消息信息,消息发送成功记录日志为成功,消息发送失败记录状态为失败,对于失败的消息,系统需要通过定时任务进行重新进行发送。

图片.png

伪代码实现

 public void sendOrderMessage(String bizKey,String type,String content)
    {
        logger.info("send add order message ");
            Message message =new Message();
            //业务的主键:可以为订单id、商品id等
            message.setKeys(bizKey);
            message.setTopic("AddOrder");
            message.setTags("TAG-AddOrder");
            //消息内容,创建订单,退款订单等
            message.putUserProperty("type", type);
            try
            {
                message.setBody(content.getBytes(RemotingHelper.DEFAULT_CHARSET));
            }
            catch (Exception e)
            {
                logger.error("");
            }
  
            // 记录消息日志状态为成功
            
            rocketMQTemplate.asyncSend("testtopic", message,new SendCallback()
            {
                @Override
                public void onSuccess(SendResult result)
                {
                    logger.info("发送消息成功");
                    //业务日志记录成功
                }
                
                @Override
                public void onException(Throwable e)
                {
                    logger.error("发送消息失败",e);
                    //业务进行日志记录失败
                }
            });
    } 

说明:采用异步发送消息,如果发送成功业务记录消息已发送成功日志,如果失败业务记录消息发送失败。

Broker接收到消息暂存到内存中,Consumer还没来得及消费,Broker异常了。

当Broker接收到消息后先将消息存储到PageCache中,然后进行持久化存储操作。

消息存储结构

  • CommitLog:存储消息的元数据。所有消息都会顺序存入到CommitLog文件当中。 CommitLog由多个文件组成,每个文件固定大小1G。以第一条消息的偏移量为文件名。
  • ConsumerQueue:存储消息在CommitLog的索引
  • IndexFile:消息查询提供了一种通过key或时间区间来查询消息的方法,这种通过IndexFile来 查找消息。

图片.png

消息持久化整体流程

  1. Broker先将消息写入内存中,然后将消息按照顺序写到CommitLog文件中。
  2. 线程根据消息的队列信息,写入到相关的ConsumerQueue中(minOffset为写入的初始位置,consumerOffset为当前消费到的位置,maxOffset为ConsumerQueue最新写入的位置)和IndexFile文件中。
  3. 消费者从ConsumerQueue的consumerOffset读取到当前应该消费的消息在CommitLog中的偏移量,到CommitLog中找到对应的消息,消息消费成功后移动consumerOffset。

消息持久化刷盘机制

同步刷盘

同步刷盘指的 Broker 端收到消息发送者的消息后,先写入内存,然后将内容持久化到磁盘后才向客户端返回消息发送成功。

  • 优点:不会造成消息丢失
  • 缺点: 吞吐量低。

异步刷盘

异步刷盘指的是 Broker 将消息存储到 PageCache 后就立即返回成功,然后开启一个异步线程定时执行 FileChannel 的 forece 方法将内存中的数据定时刷写到磁盘,默认间隔为 500ms。在 RocketMQ 的异步刷盘实现类为 FlushRealTimeService。

  • 优点:吞吐量高
  • 缺点: 当磁盘损坏时,会造成丢失消息。

小结

同步刷盘的优点是能保证消息不丢失,即向客户断返回成功就代表这条消息已被持久化到磁盘,即消息非常可靠,但是以牺牲写入性能为前提条件的,但由于RocketMQ 的消息是先写入 PageCache,故消息丢失的可能性较小,如果能容忍一定几率的消息丢失,可以考虑使用异步刷盘。

消费者消费失败

消费者消费失败,会重新把消费发回给Broker进行处理。其具体流程如下:

图片.png

说明:

  1. 从流程图可以看出,消费者消息重新消息只对集群模式生效,对于消息订阅模式,消费者消费失败直接丢弃不做任何处理。
  2. 关于消费者重试的次数,MQ默认允许每条消息最多重试16次,每次消费失败发送一条延时消息到重试队列,同一条消息失败一次将延时等级提高一次,然后再放到重试队列。重试16次后如果还没有消费成功,则将消息放到死信队列中。
  3. 重试队列topic的名称被名命为%RETRY% + consumerGroup,而死信队列的名称名命为:%DLQ% + consumerGroup,可以登录RocketMQ的消息控制台进行查看。

图片.png

  1. 消费端进行重试机制,会造成重复消息的问题,关于如何处理重复消息以及消息的幂等性,将在后续的文章进行讲解。

总结

项目中一些特殊的业务场景中,如果需要保证消息的可靠性,就会大大降低RocketMQ的吞吐量,所以需要根据业务进行综合性考虑。