RocketMQ消息消费

本文只提供生产者和消费者部分的示例代码,其它配置部分见RocketMQ消息客户端生产与消费的基本实现

技术框架

JDK: java version "1.8.0_391"
RocketMQ SDK: rocketmq-spring-boot-starter:2.2.3

消息消费原理

消费者必须关联一个指定的消费者分组,以获取分组内统一定义的行为配置和消费状态;RocketMQ根据订阅关系关联消息消息主题与消费者分组,并通过订阅关系从队列中获取到关系。模型关系如图:

消费者分类

根据消费行为分类

分为: PushConsumer 、 SimpleConsumer 以及 PullConsumer

PushConsumer

全托管消费,消息的获取、消费状态提交以及消费重试都通过 Apache RocketMQ 的客户端SDK完成,Rocketmq-client:5.x中默认的消费方式。

工作原理

在PushConsumer类型中,消息的实时处理能力是基于SDK内部的典型Reactor线程模型实现的。如下图所示,SDK内置了一个长轮询线程,先将消息异步拉取到SDK内置的缓存队列中,再分别提交到消费线程中,触发监听器执行本地消费逻辑。

使用方式

PushConsumer的使用方式比较固定,在消费者初始化时注册一个消费监听器,并在消费监听器内部实现消息处理逻辑。由 Apache RocketMQ 的SDK在后台完成消息获取、触发监听器调用以及进行消息重试处理。
示例代码:

@Component
@RocketMQMessageListener(
    topic = "smsTopic",
    consumerGroup = "smsConsumerGroup"
)
public class SMSTopicListener implements RocketMQListener<MessageExt> {

    private static final Logger logger = LoggerFactory.getLogger(SMSTopicListener.class);

    @Override
    public void onMessage(MessageExt msgExt) {
        // 执行本地事务逻辑,返回事务状态
        try {
            String bodyStr = new String(msgExt.getBody());
            logger.info("消息消费开始:{}", bodyStr);
            logger.info("消息消费结束");
        } catch (Exception e) {
            logger.error("交易消息消费失败!", e);
            throw e;
        }
    }
}

PullConsumer

被动消费,自动拉取模式,业务自定拉取消息、维护OffsetStore、根据不同的消息状态做不同的处理。
rocketmq-spring-boot-starter:2.2.3中封装了PullConsumer消费者的创建过程,使用时可通过配置调整相关参数,不需要自己单独实现消费管理的过程。

使用方式

使用默认的配置

配置内容
application.yml

rocketmq:
  name-server: 127.0.0.1:9876
  pull-consumer:
    group: smsPullConsumerGroup
    topic: pull-topic
    pullBatchSize: 20

添加消息监听器

@Component
@RocketMQMessageListener(
        topic = "pull-topic",
        consumerGroup = "smsPullConsumerGroup"
)
public class SMSPullTopicListener implements RocketMQListener<MessageExt> {

    private static final Logger logger = LoggerFactory.getLogger(SMSTopicListener.class);

    @Override
    public void onMessage(MessageExt msgExt) {
        // 执行本地事务逻辑,返回事务状态
        try {
            String bodyStr = new String(msgExt.getBody());
            logger.info("消息消费开始:{}", bodyStr);
            logger.info("消息消费结束");
        } catch (Exception e) {
            logger.error("交易消息消费失败!", e);
            throw e;
        }
    }

}

框架实现源码

org.apache.rocketmq.spring.autoconfigure.RocketMQAutoConfiguration#defaultLitePullConsumer
自定义配置

配置内容

@ExtRocketMQConsumerConfiguration(
        topic = "ex-pull-topic",
        group = "ex-smsPullConsumerGroup",
        pullBatchSize = 20
)
public class ExtPullConsumerRocketConfig extends RocketMQTemplate{

}

添加消息监听器

@Component
@RocketMQMessageListener(
        topic = "ex-pull-topic",
        consumerGroup = "ex-smsPullConsumerGroup"
)
public class SMSExPullTopicListener implements RocketMQListener<MessageExt> {

    private static final Logger logger = LoggerFactory.getLogger(SMSTopicListener.class);

    @Override
    public void onMessage(MessageExt msgExt) {
        // 执行本地事务逻辑,返回事务状态
        try {
            String bodyStr = new String(msgExt.getBody());
            logger.info("消息消费开始:{}", bodyStr);
            logger.info("消息消费结束");
        } catch (Exception e) {
            logger.error("交易消息消费失败!", e);
            throw e;
        }
    }

}

框架实现源码

org.apache.rocketmq.spring.autoconfigure.ExtConsumerResetConfiguration#afterSingletonsInstantiated

SimpleConsumer

半托管消费模式,RocketMQ主动推送消息;用户需要自行进行消息的拉取,然后再根据自己业务逻辑处理结果的不同再对拉取到的消息进行不同的处理,在rocketmq-client:5.x中没有提供SimpleConsumer的消费实例,需要自己根据PushConsumer自定义消息的拉取和消息结果返回的过程。

三种消费模式比较

image

根据消息类型分类

普通消息

参考文章RocketMQ消息客户端生产与消费的基本实现

顺序消息

监听器为单线程,单消费队列,在@RocketMQMessageListener注解中指定consumeMode为ConsumeMode.ORDERLY
示例代码:

@Component
@RocketMQMessageListener(
        topic = "smsOrderlyTopic",
        consumerGroup = "smsOrderlyConsumerGroup",
        consumeMode = ConsumeMode.ORDERLY
)
public class SMSOrderlyTopicListener implements RocketMQListener<MessageExt> {

    private static final Logger logger = LoggerFactory.getLogger(SMSTopicListener.class);

    @Override
    public void onMessage(MessageExt msgExt) {
        // 执行本地事务逻辑,返回事务状态
        try {
            String bodyStr = new String(msgExt.getBody());
            logger.info("消息消费开始:{}", bodyStr);
        } catch (Exception e) {
            logger.error("交易消息消费失败!", e);
            throw e;
        }
    }
}

消费业务执行结果返回

生产者发送消息时接收消息的消费结果

@Component
@RocketMQMessageListener(
        topic = "smsReplyTopic",
        consumerGroup = "smsReplyConsumerGroup"
)
public class SMSRocketMQReplyListener implements RocketMQReplyListener<MessageExt, R<String>> {

    private static final Logger logger = LoggerFactory.getLogger(SMSRocketMQReplyListener.class);

    @Override
    public R<String> onMessage(MessageExt message) {
        // 执行本地事务逻辑,返回事务状态
        try {
            String bodyStr = new String(message.getBody());
            logger.info("消息消费开始:{}", bodyStr);
            return R.ok("消息消费成功");
        } catch (Exception e) {
            logger.error("交易消息消费失败!", e);
            throw e;
        }
    }
}

消费特性

消费者过滤

定义

过滤的含义指的是将符合条件的消息投递给消费者,而不是将匹配到的消息过滤掉。

原理

image

分类

Apache RocketMQ 支持Tag标签过滤和SQL属性过滤,这两种过滤方式对比如下:
image

约束

过滤表达式属于订阅关系的一部分,Apache RocketMQ 的领域模型规定,同一消费者分组内的多个消费者的订阅关系包括过滤表达式,必须保持一致,否则可能会导致部分消息消费不到。

Tag标签

规则

Tag标签过滤为精准字符串匹配,过滤规则设置格式如下:

  • 单Tag匹配:过滤表达式为目标Tag。表示只有消息标签为指定目标Tag的消息符合匹配条件,会被发送给消费者。

  • 多Tag匹配:多个Tag之间为或的关系,不同Tag间使用两个竖线(||)隔开。例如,Tag1||Tag2||Tag3,表示标签为Tag1或Tag2或Tag3的消息都满足匹配条件,都会被发送给消费者进行消费。

  • 全部匹配:使用星号(*)作为全匹配表达式。表示主题下的所有消息都将被发送给消费者进行消费。

代码示例
@RocketMQMessageListener(
    topic = "smsTagTopic",
    consumerGroup = "smsTagConsumerGroup",
    selectorExpression = "CC||ZZ"
)
public class SMSTagTopicListener implements RocketMQListener<MessageExt> {

    private static final Logger logger = LoggerFactory.getLogger(SMSTagTopicListener.class);

    @Override
    public void onMessage(MessageExt msgExt) {
        // 执行本地事务逻辑,返回事务状态
        try {
            String bodyStr = new String(msgExt.getBody());
            logger.info("消息消费开始:{}", bodyStr);
            logger.info("消息消费结束");
        } catch (Exception e) {
            logger.error("交易消息消费失败!", e);
            throw e;
        }
    }
}

消费者tag匹配失败时,Broker默认会消费匹配失败的消息,如果需要将未匹配的消息存放到队列等待消费者消费,需要修改broker配置文件的brokerClusterName = default

SQL属性过滤

规则

SQL属性过滤是 Apache RocketMQ 提供的高级消息过滤方式,通过生产者为消息设置的属性(Key)及属性值(Value)进行匹配。
image

代码示例

RocketMQ:5.x中默认没开启SQL过滤,需要调整broker配置文件中的enablePropertyFilter= true
生产者,过滤条件条件在head中

String reqNo = TransactionIdGenerator.generateReqTransactionId();

        // 创建 RocketMQ 的 短信Message 实例
        Message<String> smsMessage = MessageBuilder
                .withPayload("发送自定义Sql消息。")
                .setHeader(RocketMQHeaders.KEYS, "SMS_" + reqNo)
//                sql过滤属性添加
                .setHeader("USER", condition)
                .build();

        //发送短信通知消息
        SendResult smsSendResult = rocketMQTemplate.syncSend("smsSqlTopic:sqlTag", smsMessage);
        if (smsSendResult.getSendStatus().equals(SendStatus.SEND_OK)) {
            logger.info("自定义Sql提醒消息已经发送");
            return r;
        }

消费者:@RocketMQMessageListener注解的selectorType指定为SelectorType.SQL92,并添加过滤表达式

@RocketMQMessageListener(
    topic = "smsSqlTopic",
    consumerGroup = "smsSqlConsumerGroup",
    selectorType = SelectorType.SQL92,
    selectorExpression = "USER IN ('ZZ','CC')"
)
public class SMSSQLTopicListener implements RocketMQListener<MessageExt> {

    private static final Logger logger = LoggerFactory.getLogger(SMSSQLTopicListener.class);

    @Override
    public void onMessage(MessageExt msgExt) {
        // 执行本地事务逻辑,返回事务状态
        try {
            String bodyStr = new String(msgExt.getBody());
            logger.info("消息消费开始:{}", bodyStr);
            logger.info("消息消费结束");
        } catch (Exception e) {
            logger.error("交易消息消费失败!", e);
            throw e;
        }
    }
}

消费者负载均衡

广播消费与共享消费

广播消费: 消息队列中的消息同时广播给所有订阅过的消费者;不同消费组订阅同一个主题时,不同消费组之间属于广播消费。
共享消费: 同一个消费组中的消费者设置为“负载均衡”时,消费者间使用负载均衡的策略依次消费队列中的消息。
如图展示组间广播、组内共享:image

负载均衡策略

队列粒度负载均衡

将不同的队列固定分配给一个消费者,要求队列数量大于消费者数量,否则会出现某些消费者消费不到消息的现象。在RocketMQ 5.X版本之前,负载均衡策略为队列粒度负载均衡。

消息粒度负载均衡

队列中的消息按照轮询方式依次发送给每一个消费者,保证所有消费者都能相等分的消费消息。在RocketMQ 5.X版本中,对于PushConsumer和SimpleConsumer类型的消费者,默认且仅使用消息粒度负载均衡策略。
使用示例:
rocketmq-spring-boot-starter:2.2.3中提供了如下负载均衡策略:

  • AllocateMessageQueueAveragely:平均负载策略,将消息队列平均分配给每一个消费者。
  • AllocateMessageQueueConsistentHash:一致性哈希负载策略,为每一个消费者创建多个虚拟的节点,将虚拟节点连成一个环,然后将消息队列进行哈希计算得到哈希值,通过哈希值找到距离一致性哈希环顺时针方向最近的那个虚拟节点,此时就可以通过虚拟节点获取到真实的消费者了,就将这个消息队列分配给这个消息者。
  • AllocateMessageQueueRandom:随机负载策略,将消息队列随机分配给每一个消息者。
  • AllocateMessageQueueRoundRobin:轮询负载策略,将消息队列按顺序分配给每一个消息者。
    使用AllocateMessageQueueConsistentHash可以实现消息粒度负载均衡,在实现消息监听时,同时实现RocketMQPushConsumerLifecycleListener接口的prepareStart
@Component
@RocketMQMessageListener(
    topic = "smsLoadTopic",
    consumerGroup = "smsLoadConsumerGroup"
)
public class SMSLoadTopicListener1 implements RocketMQListener<MessageExt>, RocketMQPushConsumerLifecycleListener {

    private static final Logger logger = LoggerFactory.getLogger(SMSLoadTopicListener1.class);

    @Override
    public void onMessage(MessageExt msgExt) {
        // 执行本地事务逻辑,返回事务状态
        try {
            String bodyStr = new String(msgExt.getBody());
            logger.info("负载均衡1消息消费:{}", bodyStr);
        } catch (Exception e) {
            logger.error("交易消息消费失败!", e);
            throw e;
        }
    }

    @Override
    public void prepareStart(DefaultMQPushConsumer consumer) {
        //修改消费者负载均衡策略
        consumer.setAllocateMessageQueueStrategy(new AllocateMessageQueueConsistentHash());
    }
}

消费进度管理

消息位点

Apache RocketMQ 定义队列中最早一条消息的位点为最小消息位点(MinOffset);最新一条消息的位点为最大消息位点(MaxOffset)。虽然消息队列逻辑上是无限存储,但由于服务端物理节点的存储空间有限, Apache RocketMQ 会滚动删除队列中存储最早的消息。因此,消息的最小消费位点和最大消费位点会一直递增变化。image

消费位点

Apache RocketMQ 领域模型为发布订阅模式,每个主题的队列都可以被多个消费者分组订阅。若某条消息被某个消费者消费后直接被删除,则其他订阅了该主题的消费者将无法消费该消息。

因此,Apache RocketMQ 通过消费位点管理消息的消费进度。每条消息被某个消费者消费完成后不会立即在队列中删除,Apache RocketMQ 会基于每个消费者分组维护一份消费记录,该记录指定消费者分组消费某一个队列时,消费过的最新一条消息的位点,即消费位点。image

消息轨迹

消息轨迹用于记录消息从生产到消费的全过程,收集信息并形成图形记录,开始方式:

  1. Brocker配置
traceOn=true
traceTopicEnable=true
  1. 生产者
    application.yml
rocketmq:
  name-server: 127.0.0.1:9876
  producer:
    group-name: tradeTxProducer
#开启消息轨迹
    enableMsgTrace: true

业务代码

@Configuration
public class RocketMQConfig {

    @Value("${rocketmq.name-server}")
    private String nameServer;
    @Value("${rocketmq.producer.group-name}")
    private String producerGroupName;
    @Value("${rocketmq.producer.enableMsgTrace}")
    private boolean enableMsgTrace;

    /**
     * 普通消息生产者
     * @return
     */
    @Bean
    public DefaultMQProducer mqProducer() {
// 创建生产者时,开启消息轨迹
        DefaultMQProducer producer = new DefaultMQProducer(producerGroupName,enableMsgTrace);
        producer.setNamesrvAddr(nameServer);
        return producer;
    }

    /**
     * RocketMQ普通消息连接组件
     * @param mqProducer
     * @return
     */
    @Bean
    public RocketMQTemplate rocketMQTemplate(DefaultMQProducer mqProducer) {
        RocketMQTemplate rocketMQTemplate = new RocketMQTemplate();
        rocketMQTemplate.setProducer(mqProducer);
        return rocketMQTemplate;
    }
}

参考文献

RocketMQ官网文档

posted @   周仙僧  阅读(671)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 【自荐】一款简洁、开源的在线白板工具 Drawnix
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY
点击右上角即可分享
微信分享提示