Spring Boot 通过监听器方式整合 RocketMq(基于模板方法的并发消费、局部顺序消息消费)

目录

生产者发送消息

消费者消费消息

常见问题

 1、消息发送失败处理方式

2、消费过程幂等

3、消费速度慢的处理方式

 Broker 角色


项目中有关rocketMq及相关类的Maven依赖

        <dependency>
            <groupId>org.apache.rocketmq</groupId>
            <artifactId>rocketmq-spring-boot-starter</artifactId>
            <version>2.1.1</version>
        </dependency>
        <dependency>
            <groupId>org.apache.rocketmq</groupId>
            <artifactId>rocketmq-client</artifactId>
            <version>4.3.0</version>
        </dependency>
        <dependency>
            <groupId>com.google.guava</groupId>
                <artifactId>guava</artifactId>
            <version>30.1.1-jre</version>
        </dependency>

生产者发送消息

并发消费场景下的生产者代码,并发消息无法保证消息一定是按照顺序消费,在绝大多数场景下不需要过问消息的消费顺序,可通过此方式进行mq消息的发送:

package com.fss.project.mq.producer;

import com.alibaba.fastjson.JSON;
import com.fss.project.mq.enums.DelayLevelEnum;
import lombok.extern.slf4j.Slf4j;
import org.apache.rocketmq.client.exception.MQBrokerException;
import org.apache.rocketmq.client.exception.MQClientException;
import org.apache.rocketmq.client.producer.DefaultMQProducer;
import org.apache.rocketmq.client.producer.SendResult;
import org.apache.rocketmq.client.producer.SendStatus;
import org.apache.rocketmq.common.message.Message;
import org.apache.rocketmq.remoting.exception.RemotingException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;
import java.nio.charset.StandardCharsets;
import java.util.Objects;

@Slf4j
@Component
public class RocketMqProductComponent {

    private Logger logger = LoggerFactory.getLogger(RocketMqProductComponent.class);

    @Resource
    private DefaultMQProducer defaultMQProducer;

    /**
     * 发送消息
     *
     * @param dto       具体数据
     * @param topicName
     * @param tagName
     * @param key
     * @return 执行状态
     */
    public boolean sendMessage(Object dto, String topicName, String tagName, String key) {
        if (Objects.isNull(dto) || Objects.isNull(topicName) || Objects.isNull(tagName)) {
            return false;
        }

        boolean result = false;
        // 构造消息body
        String body = builderBody(dto);

        try {
            Message message = new Message(topicName, tagName, key, body.getBytes(StandardCharsets.UTF_8));
            SendResult send = defaultMQProducer.send(message);

            logger.info("发送者,发送消息:" + JSON.toJSONString(send));
            if (Objects.nonNull(send) && SendStatus.SEND_OK.equals(send.getSendStatus())) {
                result = true;
            } else {
                logger.warn("消息发送失败,send={},body={}", JSON.toJSONString(send), body);
            }
        } catch (MQClientException | RemotingException | MQBrokerException e) {
            logger.warn("发送消息失败:{}", e);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
        return result;
    }

    /**
     * 发送延时消息
     *
     * @param dto
     * @param topicName
     * @param tagName
     * @param delayLevelEnum 延时等级
     * @return
     */
    public boolean sendDelayMessage(Object dto, String topicName, String tagName, String key, DelayLevelEnum delayLevelEnum) {
        if (Objects.isNull(dto) || Objects.isNull(topicName) || Objects.isNull(tagName) || Objects.isNull(delayLevelEnum)) {
            return false;
        }

        boolean result = false;
        // 构造消息body
        String body = builderBody(dto);

        try {
            Message message = new Message(topicName, tagName, key, body.getBytes(StandardCharsets.UTF_8));
            message.setDelayTimeLevel(delayLevelEnum.getDelayLevel());
            logger.warn("发送延时消息 message:{}", JSON.toJSONString(message));
            SendResult send = defaultMQProducer.send(message);
            if (Objects.nonNull(send) && SendStatus.SEND_OK.equals(send.getSendStatus())) {
                result = true;
            } else {
                logger.warn("延时消息发送失败,send={},body={}", JSON.toJSONString(send), body);
            }
        } catch (MQClientException | RemotingException | MQBrokerException e) {
            logger.warn("发送延时消息失败:{}", e);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
        return result;
    }

    /**
     * 构造消息body
     *
     * @param dto
     * @return
     */
    public String builderBody(Object dto) {
        // 构造消息body
        String body = null;
        if (dto instanceof String) {
            body = (String) dto;
        } else {
            body = JSON.toJSONString(dto);
        }
        return body;
    }
}

顺序消费场景下的生产者代码(局部顺序),在特定场景下需要消息按照顺序消费时,可通过此方式进行发送

package com.fss.project.mq.producer;

import com.alibaba.fastjson.JSON;
import com.fss.project.mq.enums.DelayLevelEnum;
import lombok.extern.slf4j.Slf4j;
import org.apache.rocketmq.client.exception.MQBrokerException;
import org.apache.rocketmq.client.exception.MQClientException;
import org.apache.rocketmq.client.producer.DefaultMQProducer;
import org.apache.rocketmq.client.producer.MessageQueueSelector;
import org.apache.rocketmq.client.producer.SendResult;
import org.apache.rocketmq.client.producer.SendStatus;
import org.apache.rocketmq.common.message.Message;
import org.apache.rocketmq.common.message.MessageQueue;
import org.apache.rocketmq.remoting.exception.RemotingException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;
import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.Objects;

@Slf4j
@Component
public class RocketMqProductOrderLyComponent {

    private Logger logger = LoggerFactory.getLogger(RocketMqProductOrderLyComponent.class);

    @Resource
    private DefaultMQProducer defaultMQProducer;

    /**
     * 发送消息
     *
     * @param dto       具体数据
     * @param topicName
     * @param tagName
     * @return 执行状态
     */
    public boolean sendMessage(Object dto, String topicName, String tagName, String key) {
        if (Objects.isNull(dto) || Objects.isNull(topicName) || Objects.isNull(tagName) || Objects.isNull(key)) {
            return false;
        }

        boolean result = false;

        // 构造消息body
        String body = builderBody(dto);

        try {
            Message message = new Message(topicName, tagName, key, body.getBytes(StandardCharsets.UTF_8));
            /**
             * 局部的顺序消息
             * message:消息信息
             * arg:选择队列的业务标识
             */
            SendResult send = defaultMQProducer.send(message, new MessageQueueSelector() {
                @Override
                public MessageQueue select(List<MessageQueue> list, Message message, Object o) {
                    Long key = Long.parseLong((String) o);
                    int index = (int) (key % list.size());
                    return list.get(index);
                }
            }, key);

            System.err.println("发送者,发送消息:" + JSON.toJSONString(send));
            if (Objects.nonNull(send) && SendStatus.SEND_OK.equals(send.getSendStatus())) {
                result = true;
            } else {
                logger.warn("消息发送失败,send={},body={}", JSON.toJSONString(send), body);
            }
        } catch (MQClientException | RemotingException | MQBrokerException e) {
            logger.warn("发送消息失败:{}", e);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
        return result;
    }

    /**
     * 发送延时消息
     *
     * @param dto
     * @param topicName
     * @param tagName
     * @param delayLevelEnum 延时等级
     * @return
     */
    public boolean sendDelayMessage(Object dto, String topicName, String tagName, String key, DelayLevelEnum delayLevelEnum) {
        if (Objects.isNull(dto) || Objects.isNull(topicName) || Objects.isNull(tagName) || Objects.isNull(key) || Objects.isNull(delayLevelEnum)) {
            return false;
        }

        boolean result = false;

        // 构造消息body
        String body = builderBody(dto);
        try {
            Message message = new Message(topicName, tagName, key, body.getBytes(StandardCharsets.UTF_8));
            message.setDelayTimeLevel(delayLevelEnum.getDelayLevel());

            SendResult send = defaultMQProducer.send(message, new MessageQueueSelector() {
                @Override
                public MessageQueue select(List<MessageQueue> list, Message message, Object o) {
                    Long key = (Long) o;
                    int index = (int) (key % list.size());
                    return list.get(index);
                }
            }, key);
            if (Objects.nonNull(send) && SendStatus.SEND_OK.equals(send.getSendStatus())) {
                result = true;
            } else {
                logger.warn("延时消息发送失败,send={},body={}", JSON.toJSONString(send), body);
            }
        } catch (MQClientException | RemotingException | MQBrokerException e) {
            logger.warn("发送延时消息失败:{}", e);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
        return result;
    }

    /**
     * 构造消息body
     *
     * @param dto
     * @return
     */
    public String builderBody(Object dto) {
        // 构造消息body
        String body = null;
        if (dto instanceof String) {
            body = (String) dto;
        } else {
            body = JSON.toJSONString(dto);
        }
        return body;
    }
}

消息有序指的是可以按照消息的发送顺序来消费(FIFO)。RocketMQ可以严格的保证消息有序,可以分为分区有序或者全局有序。

顺序消费的原理解析,在默认的情况下消息发送会采取Round Robin轮询方式把消息发送到不同的queue(分区队列);而消费消息的时候从多个queue上拉取消息,这种情况发送和消费是不能保证顺序。但是如果控制发送的顺序消息只依次发送到同一个queue中,消费的时候只从这个queue上依次拉取,则就保证了顺序。当发送和消费参与的queue只有一个,则是全局有序;如果多个queue参与,则为分区有序,即相对每个queue,消息都是有序的。

通常局部有序已经我完全可以满足需求,并且效率上会更高,故通常是使用局部消费

所以在代码中,要发送顺序消息时,须指定key,即作为message的key,也作为key对队列长度取余来选择某个queue,从而实现局部顺序,key可以传入业务的唯一ID,例:订单ID、退款ID等。

消费者消费消息

消费者是通过模板方法的方式,来让开发者更多的关注业务逻辑,在监听器中对消息已经有了统一的处理。

消费者Bean配置

package com.fss.project.mq.consume;

import org.apache.rocketmq.client.consumer.DefaultMQPushConsumer;
import org.apache.rocketmq.client.exception.MQClientException;
import org.apache.rocketmq.common.consumer.ConsumeFromWhere;
import org.apache.rocketmq.common.protocol.heartbeat.MessageModel;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.SpringBootConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.util.StringUtils;
import java.util.List;

/**
 * 消费者配置
 */
@SpringBootConfiguration
public class MQConsumerConfiguration {
    public static final Logger LOGGER = LoggerFactory.getLogger(MQConsumerConfiguration.class);


    @Value("${rocketmq.consumer.namesrvAddr:127.0.0.1:9876}")
    private String namesrvAddr;

    @Value("${rocketmq.consumer.groupName:producer-group}")
    private String groupName;

    /**
     * 并发消费topic
     */
    @Value("#{'${rocketmq.consumer.topics:DEMO_TEST_TOPIC}'.split(',')}")
    private List<String> topicList;

    /**
     * 顺序消费topic
     */
    @Value("#{'${rocketmq.consumer.topics:DEMO_ORDERLY_TOPIC}'.split(',')}")
    private List<String> orderLyTopicList;

    @Value("${rocketmq.consumer.consumeMessageBatchMaxSize:1}")
    private int consumeMessageBatchMaxSize;

    /**
     * 并发消费监听器
     */
    @Autowired
    private RocketMqMessageListener registerMessageListener;

    /**
     * 顺序消费监听器
     */
    @Autowired
    private RocketMqMessageOrderLyListener rocketMqMessageOrderLyListener;

    @Bean
    public DefaultMQPushConsumer getRocketMQConsumer() throws RuntimeException {
        if (StringUtils.isEmpty(groupName)){
            throw new RuntimeException("groupName is null !!!");
        }
        if (StringUtils.isEmpty(namesrvAddr)){
            throw new RuntimeException("namesrvAddr is null !!!");
        }
        if(StringUtils.isEmpty(topicList)){
            throw new RuntimeException("topics is null !!!");
        }
        DefaultMQPushConsumer consumer = new DefaultMQPushConsumer(groupName);
        consumer.setNamesrvAddr(namesrvAddr);
        consumer.registerMessageListener(registerMessageListener);
//        consumer.registerMessageListener(rocketMqMessageOrderLyListener);
        /**
         * 设置Consumer第一次启动是从队列头部开始消费还是队列尾部开始消费
         * 如果非第一次启动,那么按照上次消费的位置继续消费
         */
        consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_LAST_OFFSET);
        /**
         * 设置消费模型,集群还是广播,默认为集群
         */
        consumer.setMessageModel(MessageModel.CLUSTERING);
        /**
         * 设置一次消费消息的条数,默认为1条
         */
        consumer.setConsumeMessageBatchMaxSize(consumeMessageBatchMaxSize);
        try {
            /**
             * 设置该消费者订阅的主题和tag,如果是订阅该主题下的所有tag,则tag使用*;如果需要指定订阅该主题下的某些tag,则使用||分割,例如tag1||tag2||tag3
             */
            topicList.forEach(topic->{
                try {
                    consumer.subscribe(topic,"*");
                } catch (MQClientException e) {
                    e.printStackTrace();
                }
            });
//            orderLyTopicList.forEach(topic->{
//                try {
//                    consumer.subscribe(topic,"*");
//                } catch (MQClientException e) {
//                    e.printStackTrace();
//                }
//            });
            consumer.start();
            LOGGER.info("consumer is start !!! groupName:{},topics:{},namesrvAddr:{}",groupName,topicList,namesrvAddr);
        }catch (MQClientException e){
            LOGGER.error("consumer is start !!! groupName:{},topics:{},namesrvAddr:{}",groupName,topicList,namesrvAddr,e);
            throw new RuntimeException(e);
        }
        return consumer;
    }

}

通过DefaultMQPushConsumer设置监听器的实现类,来将消费逻辑转移给监听器RocketMqMessageListener 。

MessageHandler:定义处理消息的接口handle及处理的消息topic、tag的类型。

public interface MessageHandler {

    void handle(String body);

    List<String> tags();

    String topic();
}
AbstractMessageHandler:MessageHandler的子类,主要是自动将body的数据转成对应的dto
package com.fss.project.mq.consume;

import com.alibaba.fastjson.JSON;

import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;

public abstract class AbstractMessageHandler<T> implements MessageHandler{

    private Class<T> paramClass;

    public AbstractMessageHandler() {
        Type genericSuperclass = this.getClass().getGenericSuperclass();
        if(genericSuperclass instanceof ParameterizedType){
            ParameterizedType parameterizedType = (ParameterizedType) genericSuperclass;
            Type[] actualTypeArguments = parameterizedType.getActualTypeArguments();
            paramClass = (Class<T>) actualTypeArguments[0];
        }
    }

    @Override
    public void handle(String body) {
        T param = JSON.parseObject(body, paramClass);
        if (checkDo(param)) {
            handler(param);
        }
    }

    /**
     * 执行消费的具体业务逻辑
     * @param param
     */
    public abstract void handler(T param);

    /**
     * 检查是否需要执行
     */
    boolean checkDo(T param){
        return true;
    }
}

RocketMqMessageListener :通过topic、tag可以确定AbstractMessageHandler的子类(某个topic、tag消费者),若传入的有key,则可以根据key来防止重复消费消息(key通常作为唯一业务),如没有传入,在具体的消费业务代码中根据业务ID来特定处理也可以。

package com.fss.project.mq.consume;

import com.alibaba.fastjson.JSON;
import com.google.common.collect.HashBasedTable;
import com.google.common.collect.Table;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyContext;
import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyStatus;
import org.apache.rocketmq.client.consumer.listener.MessageListenerConcurrently;
import org.apache.rocketmq.common.message.MessageExt;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;

import javax.annotation.PostConstruct;
import java.nio.charset.StandardCharsets;
import java.util.*;

@Component
public class RocketMqMessageListener implements MessageListenerConcurrently, ApplicationContextAware {

    private Logger logger = LoggerFactory.getLogger(RocketMqMessageListener.class);

    private ApplicationContext context;

    @Value("#{'${rocketmq.consumer.topics:DEMO_TEST_TOPIC}'.split(',')}")
    private List<String> topicList;

    private Table<String, String, List<MessageHandler>> messageHandlerTable = HashBasedTable.create();

    @PostConstruct
    public void init() {
        Map<String, MessageHandler> consumers = context.getBeansOfType(MessageHandler.class);
        consumers.values().forEach(
                messageHandler -> {
                    String topic = messageHandler.topic();
                    for (String tagName : messageHandler.tags()) {
                        List<MessageHandler> messageHandlers = messageHandlerTable.get(topic, tagName);
                        if (messageHandlers == null) {
                            messageHandlers = new ArrayList<>(3);
                        }
                        messageHandlers.add(messageHandler);
                        messageHandlerTable.put(topic, tagName, messageHandlers);
                    }
                }
        );
    }

    private List<MessageHandler> getHandler(String topic, String tag) {
        if (StringUtils.isBlank(topic) || StringUtils.isBlank(tag)) {
            return Collections.emptyList();
        }

        return messageHandlerTable.get(topic, tag);
    }

    @Override
    public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> list, ConsumeConcurrentlyContext consumeConcurrentlyContext) {
        try {
            for (MessageExt messageExt : list) {
                handlerMessageExt(messageExt);
                logger.info("ThreadName:{},messageExt:{},消费成功", Thread.currentThread().getName(), messageExt);
            }
            return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
        } catch (Exception e) {
            logger.warn("消息消费异常:e:{}", e);
            return ConsumeConcurrentlyStatus.RECONSUME_LATER;
        }
    }

    private void handlerMessageExt(MessageExt messageExt) {
        String topic = messageExt.getTopic();
        String tag = messageExt.getTags();
        String key = messageExt.getKeys();

        // 若传入key,则做唯一性校验
        if (Objects.nonNull(key) && checkMessageKey(key)) {

        }

        String body = new String(messageExt.getBody(), StandardCharsets.UTF_8);
        logger.info("handlerMessageExt,topic:{},tag:{},body:{}", topic, tag, body);
        if (!topicList.contains(topic)) {
            return;
        }

        List<MessageHandler> messageHandlerList = getHandler(topic, tag);
        if (CollectionUtils.isNotEmpty(messageHandlerList)) {
            messageHandlerList.forEach(
                    messageHandler -> {
                        messageHandler.handle(body);
                    }
            );
        }

    }

    /**
     * 判断是否重复消费
     */
    private boolean checkMessageKey(String key) {
        // TODO: 2021/9/4 可根据redis、数据库保证消息不重复消费
        return false;
    }

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        context = applicationContext;
    }
}

消费业务demo

package com.fss.project.mq.consume.component;

import com.fss.project.mq.consume.AbstractMessageHandler;
import com.fss.project.mq.consume.RocketMqTagEnum;
import com.fss.project.mq.consume.TopicEnum;
import com.fss.project.mq.dto.OrderDto;
import org.springframework.stereotype.Component;
import java.util.Collections;
import java.util.List;

@Component
public class OrderConsumer extends AbstractMessageHandler<OrderDto> {

    @Override
    public void handler(OrderDto param) {
        if(checkConsumer()){
            return;
        }
        System.err.println("成功消费:"+param);
    }

    /**
     * 若message未设置key,则可以根据body的数据(例orderId、refundId),根据redis、数据库中当前数据的状态来判断是否要消费
     * 若数据没有唯一ID来区分,则可以认为该消息不重要不作为唯一校验
     * @return
     */
    private boolean checkConsumer() {
        return false;
    }

    @Override
    public List<String> tags() {
        return Collections.singletonList(RocketMqTagEnum.TEST_TAG.getTagName());
    }

    @Override
    public String topic() {
        return TopicEnum.TEST_TOPIC.getTopicName();
    }
}

若要顺序消费,消费者监听类需要实现MessageListenerOrderly来实现,例:RocketMqMessageOrderLyListener

package com.fss.project.mq.consume;

import com.google.common.collect.HashBasedTable;
import com.google.common.collect.Table;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.rocketmq.client.consumer.listener.*;
import org.apache.rocketmq.common.message.MessageExt;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;

import javax.annotation.PostConstruct;
import java.nio.charset.StandardCharsets;
import java.util.*;

@Component
public class RocketMqMessageOrderLyListener implements MessageListenerOrderly, ApplicationContextAware {

    private Logger logger = LoggerFactory.getLogger(RocketMqMessageOrderLyListener.class);

    private ApplicationContext context;

    @Value("#{'${rocketmq.consumer.orderly.topics:DEMO_ORDERLY_TOPIC}'.split(',')}")
    private List<String> topicList;

    private Table<String, String, List<MessageHandler>> messageHandlerTable = HashBasedTable.create();

    @PostConstruct
    public void init() {
        Map<String, MessageHandler> consumers = context.getBeansOfType(MessageHandler.class);
        consumers.values().forEach(
                messageHandler -> {
                    String topic = messageHandler.topic();
                    for (String tagName : messageHandler.tags()) {
                        List<MessageHandler> messageHandlers = messageHandlerTable.get(topic, tagName);
                        if (messageHandlers == null) {
                            messageHandlers = new ArrayList<>(3);
                        }
                        messageHandlers.add(messageHandler);
                        messageHandlerTable.put(topic, tagName, messageHandlers);
                    }
                }
        );
    }

    private List<MessageHandler> getHandler(String topic, String tag) {
        if (StringUtils.isBlank(topic) || StringUtils.isBlank(tag)) {
            return Collections.emptyList();
        }

        return messageHandlerTable.get(topic, tag);
    }

    private void handlerMessageExt(MessageExt messageExt) {
        String topic = messageExt.getTopic();
        String tag = messageExt.getTags();
        String key = messageExt.getKeys();

        // 若传入key,则做唯一性校验
        if (Objects.nonNull(key) && checkMessageKey(key)) {

        }

        String body = new String(messageExt.getBody(), StandardCharsets.UTF_8);
        logger.info("handlerMessageExt,topic:{},tag:{},body:{}", topic, tag, body);
        if (!topicList.contains(topic)) {
            return;
        }

        List<MessageHandler> messageHandlerList = getHandler(topic, tag);
        if (CollectionUtils.isNotEmpty(messageHandlerList)) {
            messageHandlerList.forEach(
                    messageHandler -> {
                        messageHandler.handle(body);
                    }
            );
        }

    }

    /**
     * 判断是否重复消费
     */
    private boolean checkMessageKey(String key) {
        // TODO: 2021/9/4 可根据redis、数据库保证消息不重复消费
        return false;
    }

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        context = applicationContext;
    }

    @Override
    public ConsumeOrderlyStatus consumeMessage(List<MessageExt> list, ConsumeOrderlyContext consumeOrderlyContext) {
        try {
            for (MessageExt messageExt : list) {
                handlerMessageExt(messageExt);
                logger.info("ThreadName:{},messageExt:{},消费成功", Thread.currentThread().getName(), messageExt);
            }
            return ConsumeOrderlyStatus.SUCCESS;
        } catch (Exception e) {
            logger.warn("消息消费异常:e:{}", e);
            return ConsumeOrderlyStatus.SUSPEND_CURRENT_QUEUE_A_MOMENT;
        }
    }
}

常见问题

 1、消息发送失败处理方式

Producer的send方法本身支持内部重试,重试逻辑如下:

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

以上策略也是在一定程度上保证了消息可以发送成功。如果业务对消息可靠性要求比较高,建议应用增加相应的重试逻辑:比如调用send同步方法发送失败时,则尝试将消息存储到db,然后由后台线程定时重试,确保消息一定到达Broker。

2、消费过程幂等

RocketMQ无法避免消息重复(Exactly-Once),所以如果业务对消费重复非常敏感,务必要在业务层面进行去重处理。可以借助关系数据库进行去重。首先需要确定消息的唯一键,可以是msgId,也可以是消息内容中的唯一标识字段,例如订单Id等。在消费之前判断唯一键是否在关系数据库中存在。如果不存在则插入,并消费,否则跳过。(实际过程要考虑原子性问题,判断是否存在可以尝试插入,如果报主键冲突,则插入失败,直接跳过)

msgId一定是全局唯一标识符,但是实际使用中,可能会存在相同的消息有两个不同msgId的情况(消费者主动重发、因客户端重投机制导致的重复等),这种情况就需要使业务字段进行重复消费。

3、消费速度慢的处理方式

1 提高消费并行度

绝大部分消息消费行为都属于 IO 密集型,即可能是操作数据库,或者调用 RPC,这类消费行为的消费速度在于后端数据库或者外系统的吞吐量,通过增加消费并行度,可以提高总的消费吞吐量,但是并行度增加到一定程度,反而会下降。所以,应用必须要设置合理的并行度。 如下有几种修改消费并行度的方法:

  • 同一个 ConsumerGroup 下,通过增加 Consumer 实例数量来提高并行度(需要注意的是超过订阅队列数的 Consumer 实例无效)。可以通过加机器,或者在已有机器启动多个进程的方式。
  • 提高单个 Consumer 的消费并行线程,通过修改参数 consumeThreadMin、consumeThreadMax实现。

2 批量方式消费

某些业务流程如果支持批量方式消费,则可以很大程度上提高消费吞吐量,例如订单扣款类应用,一次处理一个订单耗时 1 s,一次处理 10 个订单可能也只耗时 2 s,这样即可大幅度提高消费的吞吐量,通过设置 consumer的 consumeMessageBatchMaxSize 返个参数,默认是 1,即一次只消费一条消息,例如设置为 N,那么每次消费的消息数小于等于 N。

3 优化每条消息消费过程

举例如下,某条消息的消费过程如下:

  • 根据消息从 DB 查询【数据 1】
  • 根据消息从 DB 查询【数据 2】
  • 复杂的业务计算
  • 向 DB 插入【数据 3】
  • 向 DB 插入【数据 4】

这条消息的消费过程中有4次与 DB的 交互,如果按照每次 5ms 计算,那么总共耗时 20ms,假设业务计算耗时 5ms,那么总过耗时 25ms,所以如果能把 4 次 DB 交互优化为 2 次,那么总耗时就可以优化到 15ms,即总体性能提高了 40%。所以应用如果对时延敏感的话,可以把DB部署在SSD硬盘,相比于SCSI磁盘,前者的RT会小很多。

消费慢的两种类型:CPU内部计算代码、外部I/O操作代码

通常是外部IO操作导致的:

1、数据库操作慢

2、缓存数据库IO操作慢

3、下游系统PRC请求,响应慢

消息堆积:下游服务异常、达到DBMS的容量限制

 Broker 角色

​ Broker 角色分为 ASYNC_MASTER(异步主机)、SYNC_MASTER(同步主机)以及SLAVE(从机)。如果对消息的可靠性要求比较严格,可以采用 SYNC_MASTER加SLAVE的部署方式。如果对消息可靠性要求不高,可以采用ASYNC_MASTER加SLAVE的部署方式。如果只是测试方便,则可以选择仅ASYNC_MASTER或仅SYNC_MASTER的部署方式。

Guess you like

Origin blog.csdn.net/kolbjbe/article/details/120098263