RocketMQ超时用的消息发送、消费实践总结


原文链接

RocketMQ思维导图,不看会后悔哟
Mysql思维导图分享

上面思维导图可去gongzhonghao回复:扣扣号,获取联系方式后找我免费获得可编辑版本。 后面会继续分享其他思维导图,包括Redis、JVM、并发编程、RocketMQ、RabbtiMQ、Kafka、spring、Zookeeper、Dubbo等等

一、生产者消息发送方式

 消息的发送方式有三种:单向发送(oneway)、同步发送、异步发送。
 消息的发送过程如下:
  生产者发送请求到MQ服务器
  MQ服务器处理请求
  MQ服务器向生产者返回应答

单向发送(oneway)

 oneway发送了不等待应答,即在实现层面仅仅将数据写入 socket缓冲区,此过程耗时通常在微秒级。
 所以对可靠性要求并不高,例如日志收集类应用可以采用oneway形式调用。

    public void sendOneway(Message msg) throws MQClientException, RemotingException, InterruptedException {
        this.defaultMQProducerImpl.sendOneway(msg);
    }

 单向发送没有重试机制哟!

同步发送

 同步发送是发送后会在等到响应之后才算是发送完。

    public SendResult send(Message msg) throws MQClientException, RemotingException, MQBrokerException, InterruptedException {
        return this.defaultMQProducerImpl.send(msg);
    }

 同步发送可查看返回结果。send消息方法只要不抛异常,就代表发送成功。发送成功会有多种状态,在SendResult里定义。各种状态说明如下:

 SEND_OK

 消息发送成功。要注意的是消息发送成功也不意味着它是可靠的。要确保不会丢失任何消息,还应启用同步Master服务器或同步刷盘,即SYNC_MASTER或SYNC_FLUSH。

 FLUSH_DISK_TIMEOUT

 此状态说明消息虽已经进入MQ服务器队列(内存),但是服务器刷盘超时。
 因为消息存储配置参数中可以设置刷盘方式和同步刷盘时间长度,如果Broker服务器设置了刷盘方式为同步刷盘,即FlushDiskType=SYNC_FLUSH(默认为异步刷盘方式),当Broker服务器 未在同步刷盘时间内(默认为5s)完成刷盘,则将返回该状态——刷盘超时。
 因为还在内存中,所以此时如果MQ服务器宕机,消息会丢失。

 FLUSH_SLAVE_TIMEOUT

 消息发送成功,但是MQ服务器同步到Slave时超时。此时消息已经进入服务器队列,只有服务器宕机,消息才会丢失。如果Broker服务器的角色是同步Master,即SYNC_MASTER(默认是异步Master即ASYNC_MASTER),并且从Broker服务器未在同步刷盘时间(默认为5秒)内完成与主服务器的同步,则将返回该状态——数据同步到Slave服务器超时。

 SLAVE_NOT_AVAILABLE

 消息发送成功,但是此时Slave不可用。如果Broker服务器的角色是同步Master,即SYNC_MASTER(默认是异步Master服务器即ASYNC_MASTER),但没有配置slave Broker服务器,则将返回该状态——无Slave服务器可用。

异步发送

 异步发送是指发送后不会等等MQ服务返回本次消息发送的结果,只需提供一个回调函数供MQ服务器异步回调生产者。

    public void send(Message msg, SendCallback sendCallback) throws MQClientException, RemotingException, InterruptedException {
        this.defaultMQProducerImpl.send(msg, sendCallback);
    }

 异步发送提高了发送效率,但可能会给MQ服务器带来压力,所以异步发送时RocketMQ对消息发送进行了并发控制(默认65535),通过参数clientAsyncSemaphoreValue配置。

 异步发送虽然也可以通过DefaultMQProducer#retryTimesWhenSendAsyncFailed属性来控制发消息的发生重试次数,但是重试的调用入口是在收到MQ服务器的响应包之后才进行的,如果出现网络异常、网络超时等未收到响应包的情况将不会重试。

实际生产中的选择

 像日志这种不是太重要的,首选当然是“单向发送”。当然,如果该日志较为重要还是选择别的方式。

 一致性要求高的(多数),最好是在发消息前将消息存在本地库,然后专门有一个字段存储当前消息的最新状态。如果有失败,可通过定时任务去兜底重新发送。此时无论选择哪种发送都不是太重要了。因为会有定时任务重新发消息,所以消费者记得去重判断;其实生产者不重发,RocketMQ的设计里也会有可能导致同一条消息被多次消费,所以去重判断是必须的,除非系统没有这个要求。

 其实具体怎么选还是要看自己项目的整体设计的,我举一个我在项目中的设计。“消费者端”只允许重新消费两次,两次之后就记录下来,并告诉MQ服务器消费成功了;在“生产者端”,每条消息发送之前都落库了,消费者消费成功了会将库里该条数据的状态进行变更。还加了一个定时,十分钟前状态还未消费成功的,就会重发一次消息。

 这种设计下,即使选了异步发送,我们收到了异步回调是成功的消息,但是消费者可能会因为超过重试次数而让此条消息被丢掉了。所以此时选异步发送的SendCallback 对系统来讲也没什么太大的意义。所以此时即使选择“单向发送”也是可以的。

消息发送重试

 单向发送无重试,其它两重试逻辑如下:

 1.默认最多重试2次。
 2.同步模式发送失败,则轮询到下一个Broker;异步模式发送失败,则只会在当前Broker进行重试。这个方法的总耗时时间不超过sendMsgTimeout设置的值,默认10s。
 如果向broker发送消息产生超时异常,则不会再重试。


二、消费者的各种使用姿势

消费幂等

 生产者端可能会误以为发送失败而将同一条消息发两遍,这就会导致同一条消息可能会有两个不同的msgId(消息ID),所以同一条消息可能会存在两个不同的msgId。

 所以我们的消息一定要有一个我们自己能区别的唯一标识,比如任务ID、订单ID。每次发送消息时都带着订单ID,那么即使同一条消息多次发送,在消息者端也能通过订单ID去判断是否已经处理过了。

消息堆积、处理不过来肿么办?

1.增加消费者
  同一 ConsumerGroup 下,通过增加消费者数量来提高消费速度(注:超过订阅队列数的消费者无效)

2.批量消费
  通过设置 consumer的 consumeMessageBatchMaxSize 这个参数,默认是 1,即一次只消费一条消息

3.扔掉消息
  当消息因处理不过来堆积到一定阈值时,可以扔掉后面的消息,即直接返回成功。直接返回成功,MQ服务就认为处理成功了。示例如下:

    public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> messages, ConsumeConcurrentlyContext context) {
        long offset = messages.get(0).getQueueOffset();
        String maxOffset = messages.get(0).getProperty(MessageConst.PROPERTY_MAX_OFFSET);
        long diff = Long.parseLong(maxOffset) - offset;
        if (diff > 10000) {
            // 消息堆积后直接返回成功,即不处理该消息了
            return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
        }
        // bug师姐 假装业务逻辑.....        

        return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
    }

4.优化耗时代码
  消费速度变慢,除了量本身很大以为,可以检查下消费者代码是不是有可以优化的地方。比如常见单条操作数据库改成批量、单条远程调用改成批量操作等等。

5.提高消费者的并发线程数
  配置消费者的消费并行线程,通过修改参数 consumeThreadMin、consumeThreadMax实现,默认最大是64,最小是20:

public DefaultMQPushConsumer(String consumerGroup, RPCHook rpcHook, AllocateMessageQueueStrategy allocateMessageQueueStrategy) {
     this.messageModel = MessageModel.CLUSTERING;
     this.consumeFromWhere = ConsumeFromWhere.CONSUME_FROM_LAST_OFFSET;
     this.consumeTimestamp = UtilAll.timeMillisToHumanString3(System.currentTimeMillis() - 1800000L);
     this.subscription = new HashMap();
     this.consumeThreadMin = 20;
     this.consumeThreadMax = 64;
     this.adjustThreadPoolNumsThreshold = 100000L;
     this.consumeConcurrentlyMaxSpan = 2000;
     this.pullThresholdForQueue = 1000;
     this.pullInterval = 0L;
     this.consumeMessageBatchMaxSize = 1;
     this.pullBatchSize = 32;
     this.postSubscriptionWhenPull = false;
     this.unitMode = false;
     this.consumerGroup = consumerGroup;
     this.allocateMessageQueueStrategy = allocateMessageQueueStrategy;
     this.defaultMQPushConsumerImpl = new DefaultMQPushConsumerImpl(this, rpcHook);
 }

 从DefaultMQPushConsumerImpl里的checkConfig源码可看出,最大并行线程数上限是1000.

private void checkConfig() throws MQClientException {
 if (!orderly && !concurrently) {
  throw new MQClientException("messageListener must be instanceof MessageListenerOrderly or MessageListenerConcurrently" + FAQUrl.suggestTodo("https://github.com/alibaba/RocketMQ/issues/41"), (Throwable)null);
 } else if (this.defaultMQPushConsumer.getConsumeThreadMin() >= 1 && this.defaultMQPushConsumer.getConsumeThreadMin() <= 1000 && this.defaultMQPushConsumer.getConsumeThreadMin() <= this.defaultMQPushConsumer.getConsumeThreadMax()) {
     if (this.defaultMQPushConsumer.getConsumeThreadMax() >= 1 && this.defaultMQPushConsumer.getConsumeThreadMax() <= 1000) {
         if (this.defaultMQPushConsumer.getConsumeConcurrentlyMaxSpan() >= 1 && this.defaultMQPushConsumer.getConsumeConcurrentlyMaxSpan() <= 65535) {
             if (this.defaultMQPushConsumer.getPullThresholdForQueue() >= 1 && this.defaultMQPushConsumer.getPullThresholdForQueue() <= 65535) {
                 if (this.defaultMQPushConsumer.getPullInterval() >= 0L && this.defaultMQPushConsumer.getPullInterval() <= 65535L) {
                     if (this.defaultMQPushConsumer.getConsumeMessageBatchMaxSize() >= 1 && this.defaultMQPushConsumer.getConsumeMessageBatchMaxSize() <= 1024) {
                         if (this.defaultMQPushConsumer.getPullBatchSize() < 1 || this.defaultMQPushConsumer.getPullBatchSize() > 1024) {
                             throw new MQClientException("pullBatchSize Out of range [1, 1024]" + FAQUrl.suggestTodo("https://github.com/alibaba/RocketMQ/issues/41"), (Throwable)null);
                         }
                     } else {
                         throw new MQClientException("consumeMessageBatchMaxSize Out of range [1, 1024]" + FAQUrl.suggestTodo("https://github.com/alibaba/RocketMQ/issues/41"), (Throwable)null);
                     }
                 } else {
                     throw new MQClientException("pullInterval Out of range [0, 65535]" + FAQUrl.suggestTodo("https://github.com/alibaba/RocketMQ/issues/41"), (Throwable)null);
                 }
             } else {
                 throw new MQClientException("pullThresholdForQueue Out of range [1, 65535]" + FAQUrl.suggestTodo("https://github.com/alibaba/RocketMQ/issues/41"), (Throwable)null);
             }
         } else {
             throw new MQClientException("consumeConcurrentlyMaxSpan Out of range [1, 65535]" + FAQUrl.suggestTodo("https://github.com/alibaba/RocketMQ/issues/41"), (Throwable)null);
         }
     } else {
         throw new MQClientException("consumeThreadMax Out of range [1, 1000]" + FAQUrl.suggestTodo("https://github.com/alibaba/RocketMQ/issues/41"), (Throwable)null);
     }
 } else {
     throw new MQClientException("consumeThreadMin Out of range [1, 1000]" + FAQUrl.suggestTodo("https://github.com/alibaba/RocketMQ/issues/41"), (Throwable)null);
 }
}

指定消费位置

 当建立新的消费者组时,可以指定消费已经存在于 Broker 中的历史消息
  CONSUME_FROM_LAST_OFFSET :将会忽略历史消息,并消费之后生成的任何消息。
  CONSUME_FROM_FIRST_OFFSET :将会消费每个存在于 Broker 中的信息。
  CONSUME_FROM_TIMESTAMP :消费在指定时间戳后产生的消息。

消费重试处理

 几年前用rabbitmq的时候遇到一个问题:消费端有个bug导致消费不成,结果一直重试,当每条消息都要重试,而且还不停的有新的消息过来。消费不成功倒影响不大,但是产生了大量的无用日志。

 大量重试的原因就是当时没有设置重试次数导致的。试想一想,如果消费者是因为超时导致的,结果大量重试就会形成洪泛攻击,最终把服务打垮。

 所以最好的做法是,每条消息设置一个重试上限,达到重试上限之后,将数据记录下来做特殊处理。代码思路如下:

public ConsumeConcurrentlyStatus consumeMessage(MessageExt message, ConsumeConcurrentlyContext context) {
    try {
        // bug师姐 假装这里是很复杂的消费业务逻辑.....
        System.out.println(context.getAckIndex());

    } catch (Exception e) {
        if (message.getReconsumeTimes() < this.getRetryLimitTimes()) {
            message.setReconsumeTimes(message.getReconsumeTimes() + 1);
            return ConsumeConcurrentlyStatus.RECONSUME_LATER;
        }
        this.savleMysql(message);// 超过重试次数处理的伪代码
        return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;// 失败也返回成功
    }
    return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
}

public int getRetryLimitTimes() {
    return 2;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值