rocketmq学习(三)进阶

rocketmq进阶

消息基础

默认情况下,producer会轮询的将消息发送到每个队列中(所有broker下的Queue合并成一个List去轮询),提高系统吞吐力。这样分布带来的问题,就是从全局上不能做到顺序性(很多时候也并不需要全局上的顺序性)。

消费完后的消息去哪里了?

消息的存储是一直存在于CommitLog中的,由于CommitLog是以文件为单位(而非消息)存在的,而且CommitLog的设计是只允许顺序写,且每个消息大小不定长,所以这决定了消息文件几乎不可能按照消息为单位删除(否则性能会极具下降,逻辑也非常复杂)。

所以消息被消费了,消息所占据的物理空间也不会立刻被回收。但消息既然一直没有删除,那RocketMQ怎么知道应该投递过的消息就不再投递?——答案是客户端自身维护——客户端拉取完消息之后,在响应体中,broker会返回下一次应该拉取的位置,PushConsumer通过这一个位置,更新自己下一次的pull请求。这样就保证了正常情况下,消息只会被投递一次。

什么时候清理物理消息文件?

那消息文件到底删不删,什么时候删?

消息存储在CommitLog之后,的确是会被清理的,但是这个清理只会在以下任一条件成立才会批量删除消息文件(CommitLog):

  • 消息文件过期(默认72小时),且到达清理时点(默认是凌晨4点),删除过期文件。
  • 消息文件过期(默认72小时),且磁盘空间达到了水位线(默认75%),删除过期文件。
  • 磁盘已经达到必须释放的上限(85%水位线)的时候,则开始批量清理文件(无论是否过期),直到空间充足。
    注:若磁盘空间达到危险水位线(默认90%),出于保护自身的目的,broker会拒绝写入服务。

这样设计带来的好处

消息的物理文件一直存在,消费逻辑只是听客户端的决定而搜索出对应消息进行,这样做,笔者认为,有以下几个好处:

一个消息很可能需要被N个消费组(设计上很可能就是系统)消费,但消息只需要存储一份,消费进度单独记录即可。这给强大的消息堆积能力提供了很好的支持——一个消息无需复制N份,就可服务N个消费组。

由于消费从哪里消费的决定权一直都是客户端决定,所以只要消息还在,就可以消费到,这使得RocketMQ可以支持其他传统消息中间件不支持的回溯消费。即我可以通过设置消费进度回溯,就可以让我的消费组重新像放快照一样消费历史消息;或者我需要另一个系统也复制历史的数据,只需要另起一个消费组从头消费即可(前提是消息文件还存在)。

消息索引服务。只要消息还存在就能被搜索出来。所以可以依靠消息的索引搜索出消息的各种原信息,方便事后排查问题。

注:在消息清理的时候,由于消息文件默认是1GB,所以在清理的时候其实是在删除一个大文件操作,这对于IO的压力是非常大的,这时候如果有消息写入,写入的耗时会明显变高。这个现象可以在凌晨4点(默认删时间时点)后的附近观察得到。

RocketMQ官方建议Linux下文件系统改为Ext4,对于文件删除操作,相比Ext3有非常明显的提升

顺序

顺序消费

简介

消息有序指的是一类消息消费时,能按照发送的顺序来消费。例如:一个订单产生了 3 条消息,分别是订单创建、订单付款、订单完成。消费时,要按照这个顺序消费才有意义。但同时订单之间又是可以并行消费的。
顺序消息被存入同一个queue,继而在消费端被顺序消费

顺序监听器

使用MessageListenerOrderly顺序监听器,进行单线程消费

顺序生产

通过 顺序生产,保证顺序消费,发送消息时,使用MessageQueueSelector:消息队列选择器,将需要顺序消费的消息存入同一queue

消息重复

简介

可能因为网络问题而产生重复消费

谁解决

业务还是MQ解决?
让MQ解决,可能会拖慢MQ;给业务解决,那就最好有统一的处理接口,避免众多业务各有一套处理逻辑,令维护疲于奔命。
因此,业务解决重复比较合适。

如何解决

记录消息ID

记录消息ID,下次消费到相同ID时,跳过消费。

消息断点续传

消费进度

commitlog:持久化消息元数据,包括消息主体等;
consumeQueue:记录数据位置;
若使用PullConsumer模式,如何ack?如何保证消费等均?自己实现。

消费异常

如何保证消费?

消费异常时,重新投递

 consumer.registerMessageListener(new MessageListenerConcurrently() {
        @Override
        public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs, ConsumeConcurrentlyContext context) {
            System.out.println(Thread.currentThread().getName() + "MESSAGES:" + msgs);// 消息
            doSomething(msgs);// 执行真正消费,return ConsumeConcurrentlyStatus
            return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
        }
    });

  若消息消费失败(数据库异常、余额不足扣款失败等业务),消息需要重试,只要返回ConsumeConcurrentlyStatus.RECONSUME_LATER,RocketMQ就会认为这批消息消费失败了。
  为了保证消息至少被成功消费一次,RocketMQ会把这批消息重发回Broker(topic不是原topic而是这个消费租的RETRY topic),在延迟的某个时间点(默认是10秒,业务可设置)后,再次投递到这个ConsumerGroup。而如果一直这样重复消费都持续失败到一定次数(默认16次),就会投递到DLQ死信队列,应用可以监控死信队列来做人工干预。

  • 如果业务的回调没有处理好而抛出异常,会认为是消费失败当ConsumeConcurrentlyStatus.RECONSUME_LATER处理;

  • 当使用顺序消费的回调MessageListenerOrderly时,由于顺序消费是要前者消费成功才能继续消费,所以没有RECONSUME_LATER的这个状态,只有SUSPEND_CURRENT_QUEUE_A_MOMENT来暂停队列的其余消费,原消息不断重试直至成功才能继续消费,也就是说有可能卡死。

启动的时候从哪里消费

  当新实例启动的时候,PushConsumer会拿到本消费组broker已经记录好的消费进度(consumer offset),按照这个进度发起自己的第一次Pull请求。

如果这个消费进度在Broker并没有存储起来,证明这个是一个全新的消费组,这时候客户端有几个策略可以选择:

CONSUME_FROM_LAST_OFFSET //默认策略,从该队列最尾开始消费,即跳过历史消息
CONSUME_FROM_FIRST_OFFSET //从队列最开始开始消费,即历史消息(还储存在broker的)全部消费一遍
CONSUME_FROM_TIMESTAMP//从某个时间点开始消费,和setConsumeTimestamp()配合使用,默认是半个小时以前
所以,社区中经常有人问:“为什么我设了CONSUME_FROM_LAST_OFFSET,历史的消息还是被消费了”? 原因就在于只有全新的消费组才会使用到这些策略,老的消费组都是按已经存储过的消费进度继续消费。

老消费组跳过历史消息

  • 在代码按日期判断,太老的消息直接return CONSUME_SUCCESS过滤
  • 在代码判断消息的offset和MAX_OFFSET相差甚远,认为是积压了很多历史消息,直接return CONSUME_SUCCESS过滤
  • 消费者启动前,先调整该消费组的消费进度,再开始消费——可以人工使用命令resetOffsetByTime,或调用内部的运维接口(linux命令、ng控制台等),祥见ResetOffsetByTimeCommand.java

回溯消费

  • 指定时间点回溯
  • 指定消息ID回溯
  • 开新消费组过滤消费
    其实就是老消费组跳过历史消息的代码实现
    利用消息出生时间点这个参数,过滤太久远的消息,如:
@Override
     public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs, ConsumeConcurrentlyContext context) {
         for(MessageExt msg: msgs){
             if(System.currentTimeMillis()-msg.getBornTimestamp()>60*1000) {//一分钟之前的认为过期
                 continue;//过期消息跳过
             }

             //do consume here

         }
         return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
     }

过滤掉堆积太多的消息,如:

@Override
     public ConsumeConcurrentlyStatus consumeMessage(//
         List<MessageExt> msgs, //
         ConsumeConcurrentlyContext context) {
         long offset = msgs.get(0).getQueueOffset();
         String maxOffset = msgs.get(0).getProperty(MessageConst.PROPERTY_MAX_OFFSET);
         long diff = Long. parseLong(maxOffset) - offset;
         if (diff > 100000) { //消息堆积了10W情况的特殊处理
             return ConsumeConcurrentlyStatus. CONSUME_SUCCESS;
         }
         //do consume here
         return ConsumeConcurrentlyStatus. CONSUME_SUCCESS;
     }

过滤消费

说明

老消费组跳过历史消息

诞生点

诞生点是个时间戳,通过比较,过滤一定时间段的消息

偏移

雷同诞生点过滤,通过偏移值和当前偏移值比较,过滤一定偏移差范围内的消息

离线消息

  • 根据消费策略,从某个起点消费

事务消息

比如,数据库事务,一套业务,必须保证完全成功,有例外就需要回滚
业务的一致性,一系列业务过程必须保证完全成功的场景就是事务
这样的消息有多个状态,并且其发送是两阶段的。第一个阶段发送PREPARED状态的消息,此时consumer是看不见这种状态的消息的,发送完毕后回调用户的TransactionExecutor接口,执行相应的事务操作(如数据库),当事务操作成功时,则对此条消息返回commit,让broker对该消息执行commit操作,成为commit状态的消息对consumer是可见的。

Created with Raphaël 2.1.2 Start Your Operation Yes or No? End yes no
发布了49 篇原创文章 · 获赞 6 · 访问量 3万+

猜你喜欢

转载自blog.csdn.net/cc007cc009/article/details/79817785