RocketMQ之应用

一、简介

RocketMQ是阿里巴巴开源的分布式消息中间件。支持事务消息、顺序消息、批量消息、定时消息、消息回溯等。它里面有几个区别于标准消息中件间的概念,如GroupTopicQueue等。系统组成则由ProducerConsumerBrokerNameServer等。

RocketMQ特点:

  • 是一个队列模型的消息中间件,具有高性能、高可靠、高实时、分布式等特点;
  • ProducerConsumer、队列都可以分布式;
  • Producer向一些队列轮流发送消息,队列集合称为TopicConsumer如果做广播消费,则一个Consumer实例消费这个Topic对应的所有队列,如果做集群消费,则多个Consumer实例平均消费这个Topic对应的队列集合;
  • 能够保证严格的消息顺序;
  • 支持拉(pull)和推(push)两种消息模式;
  • 高效的订阅者水平扩展能力;
  • 实时的消息订阅机制;
  • 亿级消息堆积能力;
  • 支持多种消息协议,如JMSOpenMessaging等;
  • 较少的依赖。

二、核心概念

2.1 Name Server、Broker

消息队列RocketMQ在任何一个环境都是可扩展的,生产者必须是一个集群,消息服务器必须是一个集群,消费者也同样。集群级别的高可用,是消息队列RocketMQ跟其他的消息服务器的主要区别,消息生产者发送一条消息到消息服务器,消息服务器会随机的选择一个消费者,只要这个消费者消费成功就认为是成功了。

注意:文中所提及的消息队列RocketMQ的服务端或者服务器包含Name ServerBroker等。服务端不等同于Broker

285763201908211752191961211062486.png

RocketMQ主要由ProducerBrokerConsumer三部分组成,其中Producer负责生产消息,Consumer负责消费消息,Broker负责存储消息。Broker在实际部署过程中对应一台服务器,每个Broker可以存储多个Topic的消息,每个Topic的消息也可以分片存储于不同的BrokerMessage Queue用于存储消息的物理地址,每个Topic中的消息地址存储于多个Message Queue中。ConsumerGroup由多个Consumer实例构成。

图中所涉及到的概念如下所述:

  1. Name Server:名称服务充当路由消息的提供者。是一个几乎无状态节点,可集群部署,节点之间无任何信息同步。在消息队列RocketMQ中提供命名服务,更新和发现Broker服务。

NameServer即名称服务,两个功能:

  • 接收broker的请求,注册broker的路由信息
  • 接收client(producer/consumer)的请求,根据某个topic获取其到broker的路由信息

NameServer没有状态,可以横向扩展。每个broker在启动的时候会到NameServer注册;
Producer在发送消息前会根据topicNameServer获取路由(到broker)信息;
Consumer也会定时获取topic路由信息。

  1. Broker:消息中转角色,负责存储消息,转发消息。可以理解为消息队列服务器,提供了消息的接收、存储、拉取和转发服务。brokerRocketMQ的核心,所以需要保证broker的高可用。
    broker分为Master BrokerSlave Broker,一个Master Broker可以对应多个Slave Broker,但是一个Slave Broker只能对应一个Master Broker
    MasterSlave的对应关系通过指定相同的BrokerName,不同的BrokerId来定义,BrokerId0表示Master,非0表示SlaveMaster也可以部署多个。
    每个BrokerName Server集群中的所有节点建立长连接,定时注册Topic信息到所有Name ServerBroker启动后需要完成一次将自己注册至Name Server的操作;随后每隔30s定期向Name Server上报Topic路由信息。
  2. Producer:负责生产消息,一般由业务系统负责生产消息。与Name Server集群中的其中一个节点(随机)建立长链接(Keep-alive),定期从Name Server读取Topic路由信息,并向提供Topic服务的Master Broker建立长链接,且定时向Master Broker发送心跳。
  3. Consumer:负责消费消息,一般是后台系统负责异步消费。与Name Server集群中的其中一个节点(随机)建立长连接,定期从Name Server拉取Topic路由信息,并向提供Topic服务的Master BrokerSlave Broker建立长连接,且定时向Master BrokerSlave Broker发送心跳。Consumer既可以从Master Broker订阅消息,也可以从Slave Broker订阅消息,订阅规则由Broker配置决定。
    另外,Broker中还存在一些非常重要的名词需要说明:

2.2 Topic、Queue、tags

RocketMQTopic/QueueJMS中的Topic/Queue概念有一定的差异,JMS中所有消费者都会消费一个Topic消息的副本,而Queue中消息只会被一个消费者消费;但到了RocketMQTopic只代表普通的消息队列,而Queue是组成Topic的更小单元。

Topic:表示消息的第一级类型,比如一个电商系统的消息可以分为:交易消息、物流消息等。一条消息必须有一个Topic。
Queue:主题被划分为一个或多个子主题,称为“message queues”。一个topic下,我们可以设置多个queue(消息队列)。当我们发送消息时,需要要指定该消息的topic。RocketMQ会轮询该topic下的所有队列,将消息发送出去。

定义:QueueTopic在一个Broker上的分片,在分片基础上再等分为若干份(可指定份数)后的其中一份,是负载均衡过程中资源分配的基本单元。集群消费模式下一个消费者只消费该Topic中部分Queue中的消息,当一个消费者开启广播模式时则会消费该Topic下所有Queue中的消息。

先看一张有关TopicQueue的关系图:

285763201908221201086061668559091.png

TagsTagsTopic下的次级消息类型/二级类型(注:Tags也支持TagA || TagB这样的表达式),可以在同一个Topic下基于Tags进行消息过滤。Tags的过滤需要经过两次比对,首先会在Broker端通过Tag hashcode进行一次比对过滤,匹配成功传到consumer端后再对具体Tags进行比对,以防止Tag hashcode重复的情况。比如交易消息又可以分为:交易创建消息,交易完成消息等,一条消息可以没有TagRocketMQ提供2级消息分类,方便大家灵活控制。标签,换句话说,为用户提供了额外的灵活性。有了标签,来自同一个业务模块的不同目的的消息可能具有相同的主题和不同的标签。标签将有助于保持您的代码干净和连贯,并且标签还可以为RocketMQ提供的查询系统提供帮助。

Queue中具体的存储单元结构如下图,最后面的8Byte存储Tag信息。

28576320190822135151149575354679.png

2.3 Producer与Producer Group

Producer:表示消息队列的生产者。消息队列的本质就是实现了publish-subscribe模式,生产者生产消息,消费者消费消息。所以这里的Producer就是用来生产和发送消息的,一般指业务系统。RocketMQ提供了发送:普通消息(同步、异步和单向)、定时消息、延时消息、事务消息。

Producer Group:是一类Producer的集合名称,这类Producer通常发送一类消息,且发送逻辑一致。相同角色的生产者被分组在一起。同一生产者组的另一个生产者实例可能被broker联系,以提交或回滚事务,以防原始生产者在交易后崩溃。

警告:考虑提供的生产者在发送消息时足够强大,每个生产者组只允许一个实例,以避免对生产者实例进行不必要的初始化。

2.4 Consumer与Consumer Group

Consumer:消息消费者,一般由业务后台系统异步的消费消息。

  • Push ConsumerConsumer的一种,应用通常向Consumer对象注册一个Listener接口,一旦收到消息,Consumer对象立刻回调Listener接口方法。
  • Pull ConsumerConsumer的一种,应用通常主动调用Consumer的拉消息方法从Broker拉消息,主动权由应用控制。

Consumer GroupConsumer Group是一类Consumer的集合名称,这类Consumer通常消费一类消息,且消费逻辑一致(使用相同Group ID的订阅者属于同一个集群。同一个集群下的订阅者消费逻辑必须完全一致(包括Tag的使用),这些订阅者在逻辑上可以认为是一个消费节点)。消费者群体是一个伟大的概念,它实现了负载平衡和容错的目标,在信息消费方面,是非常容易的。

警告:消费者群体的消费者实例必须订阅完全相同的主题。

三、组件的关系

3.1 Broker、Producer和Consumer

如果不考虑负载均衡和高可用,最简单的BrokerProducerConsumer之间的关系如下图所示:

3.2 Topic,Topic分片和Queue

QueueRocketMQ中的另一个重要概念。在对该概念进行分析介绍前,我们先来看上面的这张图:

从本质上来说,RocketMQ中的Queue是数据分片的产物。为了更好地理解Queue的定义,我们还需要引入一个新的概念:Topic分片。在分布式数据库和分布式缓存领域,分片概念已经有了清晰的定义。同理,对于RocketMQ,一个Topic可以分布在各个Broker上,我们可以把一个Topic分布在一个Broker上的子集定义为一个Topic分片。对应上图,TopicA3Topic分片,分布在Broker1Broker2Broker3上,TopicB2Topic分片,分布在Broker1Broker2上,TopicC2Topic分片,分布在Broker2Broker3上。

Topic分片再切分为若干等分,其中的一份就是一个Queue。每个Topic分片等分的Queue的数量可以不同,由用户在创建Topic时指定。

queue数量指定方式:

  1. 代码指定:producer.setDefaultTopicQueueNums(8);
  2. 配置文件指定,同时设置broker服务器的配置文件broker.properties:defaultTopicQueueNums=16
  3. rocket-console控制台指定

28576320190823163246804938503784.png

我们知道,数据分片的主要目的是突破单点的资源(网络带宽,CPU,内存或文件存储)限制从而实现水平扩展。RocketMQ在进行Topic分片以后,已经达到水平扩展的目的了,为什么还需要进一步切分为Queue呢?

解答这个问题还需要从负载均衡说起。以消息消费为例,借用Rocket MQ官方文档中的Consumer负载均衡示意图来说明:

2857632019082316325928239536543.png

如图所示,TOPIC_A在一个Broker上的Topic分片有5Queue,一个Consumer Group内有2Consumer按照集群消费的方式消费消息,按照平均分配策略进行负载均衡得到的结果是:第一个Consumer消费3Queue,第二个Consumer消费2Queue。如果增加Consumer,每个Consumer分配到的Queue会相应减少。Rocket MQ的负载均衡策略规定:Consumer数量应该小于等于Queue数量,如果Consumer超过Queue数量,那么多余的Consumer将不能消费消息。

在一个Consumer Group内,QueueConsumer之间的对应关系是一对多的关系:一个Queue最多只能分配给一个Consumer,一个Cosumer可以分配得到多个Queue。这样的分配规则,每个Queue只有一个消费者,可以避免消费过程中的多线程处理和资源锁定,有效提高各Consumer消费的并行度和处理效率。

由此,我们可以给出Queue的定义:QueueTopic在一个Broker上的分片等分为指定份数后的其中一份,是负载均衡过程中资源分配的基本单元。

四、RocketMQ生产模式

  1. 消息发送方式:RocketMQ提供三种方式可以发送普通消息:同步、异步、和单向发送。
  2. 消息类型:消息客户端提供多种SDK:普通、顺序、事务、延时消息。

4.1 发送方式

RocketMQ发送普通消息的三种方式:同步消息(默认)、异步消息和单向消息。其中前两种消息是可靠的,因为会有发送是否成功的应答。

4.1.1 同步发送(Sync)

原理:同步发送是指消息发送方发出一条消息后,会在收到服务端返回响应之后才发下一条消息的通讯方式。

sync

应用场景:可靠的同步传输应用于广泛的场景,例如重要通知邮件、报名短信通知、营销短信系统等。

实例代码

SendResult sendResult = producer.send(msg);

4.1.2 异步发送(Async)

原理:异步发送是指发送方发出一条消息后,不等服务端返回响应,接着发送下一条消息的通讯方式。消息队列RocketMQ版的异步发送,需要您实现异步发送回调接口(SendCallback)。消息发送方在发送了一条消息后,不需要等待服务端响应即可发送第二条消息。发送方通过回调接口接收服务端响应,并处理响应结果。

async

应用场景:异步发送一般用于链路耗时较长,对响应时间较为敏感的业务场景,例如,您视频上传后通知启动转码服务,转码完成后通知推送转码结果等。

实例代码

// 异步发送消息, 发送结果通过callback返回给客户端。
producer.sendAsync(msg, new SendCallback() {
    @Override
    public void onSuccess(final SendResult sendResult) {
        // 消息发送成功。
        System.out.println("send message success. topic=" + sendResult.getTopic()
                   + ", msgId=" + sendResult.getMessageId());
    }
    
    @Override
    public void onException(OnExceptionContext context) {
        // 消息发送失败,需要进行重试处理,可重新发送这条消息或持久化这条数据进行补偿处理。
        System.out.println("send message failed. topic=" + context.getTopic()
                   + ", msgId=" + context.getMessageId());
    }
});

4.1.3 单向发送(Oneway)

由于在oneway方式发送消息时没有请求应答处理,一旦出现消息发送失败,则会因为没有重试而导致数据丢失。若数据不可丢,建议选用可靠同步或可靠异步发送方式。

原理:发送方只负责发送消息,不等待服务端返回响应且没有回调函数触发,即只发送请求不等待应答。此方式发送消息的过程耗时非常短,一般在微秒级别。

oneway

应用场景:适用于某些耗时非常短,但对可靠性要求并不高的场景,例如日志收集。

实例代码

//发送单向消息,没有任何返回结果
producer.sendOneway(msg);

4.2 消息类型

消息客户端提供多种SDK:普通、顺序、事务、延时消息

4.2.1 收发顺序消息

顺序消息(FIFO消息)是消息队列RocketMQ版提供的一种严格按照顺序来发布和消费的消息类型。

顺序消息分为两类:

  • 全局顺序:对于指定的一个Topic,所有消息按照严格的先入先出FIFO(First In First Out)的顺序进行发布和消费。
  • 分区顺序:对于指定的一个Topic,所有消息根据Sharding Key进行区块分区。同一个分区内的消息按照严格的FIFO顺序进行发布和消费。Sharding Key是顺序消息中用来区分不同分区的关键字段,和普通消息的Key是完全不同的概念。

实例代码

发送消息

SendResult sendResult = producer.send(msg, new MessageQueueSelector() {
    @Override
    public MessageQueue select(List<MessageQueue> mqs, Message msg, Object arg) {
        Integer id = (Integer) arg;
        int index = id % mqs.size();
        return mqs.get(index);
     }
}, orderId);

订阅消息

consumer.registerMessageListener(new MessageListenerOrderly() {
    @Override
    public ConsumeOrderlyStatus consumeMessage(List<MessageExt> msgs, ConsumeOrderlyContext context) {
        context.setAutoCommit(true);
        System.out.printf("%s Receive New Messages: %s %n", Thread.currentThread().getName(), msgs);
        return ConsumeOrderlyStatus.SUCCESS;
   }
});

4.2.2 发送延迟消息

延时消息用于指定消息发送到消息队列RocketMQ版的服务端后,延时一段时间才被投递到客户端进行消费(例如3秒后才被消费),适用于解决一些消息生产和消费有时间窗口要求的场景,或者通过消息触发延迟任务的场景,类似于延迟队列。

实例代码

// 延时消息,单位毫秒(ms),在指定延迟时间(当前时间之后)进行投递,例如消息在3秒后投递。
long delayTime = System.currentTimeMillis() + 3000;
// 设置消息需要被投递的时间。 
msg.setStartDeliverTime(delayTime);

4.2.3 发送定时消息

定时消息可以做到在指定时间戳之后才可被消费者消费,适用于对消息生产和消费有时间窗口要求,或者利用消息触发定时任务的场景。

实例代码

// 定时消息,单位毫秒(ms),在指定时间戳(当前时间之后)进行投递,例如2021-06-26 16:20:00投递。
//如果被设置成当前时间戳之前的某个时刻,消息将立即被投递给消费者。
long timeStamp = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").parse("2021-06-26 16:20:00").getTime();
msg.setStartDeliverTime(timeStamp);

4.2.4 发送延时消息

使用场景

比如电商里,提交了一个订单就可以发送一个延时消息,1h后去检查这个订单的状态,如果还是未付款就取消订单释放库存。

延时消息的使用限制

现在RocketMQ并不支持任意时间的延时,需要设置几个固定的延时等级,从1s2h分别对应着等级118
消息消费失败会进入延时消息队列,消息发送时间与设置的延时等级和重试次数有关

private String messageDelayLevel = "1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h";
Message message = new Message("delay", msg.getBytes());
//在创建好message对象之后
message.setDelayTimeLevel(3); //这里是对应延时等级3对应是10s

4.2.5 发送事务消息

消息队列RocketMQ版提供类似XAOpen XA的分布式事务功能,通过消息队列RocketMQ版事务消息,能达到分布式事务的最终一致。
事务消息交互流程如下图所示。

1070060475.jpg

实例代码

发送事务消息包含以下两个步骤:

  1. 发送半事务消息(Half Message)及执行本地事务,
public static void main(String[] args) throws MQClientException {
    /**
     * 创建事务消息Producer
     */
    TransactionMQProducer transactionMQProducer = new TransactionMQProducer(MqConfig.GROUP_ID, getAclRPCHook());
    transactionMQProducer.setNamesrvAddr(MqConfig.NAMESRV_ADDR);
    transactionMQProducer.setTransactionCheckListener(new LocalTransactionCheckerImpl());
    transactionMQProducer.start();

    for (int i = 0; i < 10; i++) {
        try {
            Message message = new Message(MqConfig.TOPIC,
                MqConfig.TAG,
                "Hello world".getBytes(RemotingHelper.DEFAULT_CHARSET));

            SendResult sendResult = transactionMQProducer.sendMessageInTransaction(message, 
                    new LocalTransactionExecuter() {
                @Override
                public LocalTransactionState executeLocalTransactionBranch(Message msg, Object arg) {
                    System.out.println("开始执行本地事务: " + msg);
                    return LocalTransactionState.UNKNOW;
                }
            }, null);
            assert sendResult != null;
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
  1. 提交事务消息状态。

当本地事务执行完成(执行成功或执行失败),需要通知服务器当前消息的事务状态。通知方式有以下两种:

  • 执行本地事务完成后提交。
  • 执行本地事务一直没提交状态,等待服务器回查消息的事务状态。

事务状态有以下三种:

  • TransactionStatus.CommitTransaction:提交事务,允许订阅方消费该消息。
  • TransactionStatus.RollbackTransaction:回滚事务,消息将被丢弃不允许消费。
  • TransactionStatus.Unknow:无法判断状态,期待消息队列RocketMQ版的Broker向发送方再次询问该消息对应的本地事务的状态。
/**
 * MQ发送事务消息本地Check接口实现类
 */
public class LocalTransactionCheckerImpl implements TransactionCheckListener {

    @Override
    public LocalTransactionState checkLocalTransactionState(MessageExt msg) {
        System.out.println("收到事务消息的回查请求, MsgId: " + msg.getMsgId());
        return LocalTransactionState.COMMIT_MESSAGE;
    }

}

事务回查机制说明

  • 发送事务消息为什么必须要实现回查Check机制?当步骤1中半事务消息发送完成,但本地事务返回状态为TransactionStatus.Unknow,或者应用退出导致本地事务未提交任何状态时,从Broker的角度看,这条Half状态的消息的状态是未知的。因此Broker会定期要求发送方CheckHalf状态消息,并上报其最终状态。

  • Check被回调时,业务逻辑都需要做些什么?事务消息的Check方法里面,应该写一些检查事务一致性的逻辑。消息队列RocketMQ版发送事务消息时需要实现LocalTransactionChecker接口,用来处理Broker主动发起的本地事务状态回查请求,因此在事务消息的Check方法中,需要完成两件事情:

    1. 检查该半事务消息对应的本地事务的状态(committed or rollback)。
    2. Broker提交该半事务消息本地事务的状态。

4.2.6 发送批量消息

批量发送消息能显著提高传递小消息的性能。限制是这些批量消息应该有相同的topic,相同的waitStoreMsgOK,而且不能是延时消息。此外,这一批消息的总大小不应超过4MB

String topic = "BatchTest";
List<Message> messages = new ArrayList<>();
messages.add(new Message(topic, "TagA", "OrderID001", "Hello world 0".getBytes()));
messages.add(new Message(topic, "TagA", "OrderID002", "Hello world 1".getBytes()));
messages.add(new Message(topic, "TagA", "OrderID003", "Hello world 2".getBytes()));
try {
    producer.send(messages);
} catch (Exception e) {
    e.printStackTrace();
    //处理error
}

批量发送消息

//large batch
String topic = "BatchTest";
List<Message> messages = new ArrayList<>(100 * 1000);
for (int i = 0; i < 100 * 1000; i++) {
    messages.add(new Message(topic, "Tag", "OrderID" + i, ("Hello world " + i).getBytes()));
}
//split the large batch into small ones:
ListSplitter splitter = new ListSplitter(messages);
while (splitter.hasNext()) {
    List<Message> listItem = splitter.next();
    producer.send(listItem);
}
class ListSplitter implements Iterator<List<Message>> {
    private int sizeLimit = 1000 * 1000;
    private final List<Message> messages;
    private int currIndex;

    public ListSplitter(List<Message> messages) {
        this.messages = messages;
    }

    @Override
    public boolean hasNext() {
        return currIndex < messages.size();
    }

    @Override
    public List<Message> next() {
        int nextIndex = currIndex;
        int totalSize = 0;
        for (; nextIndex < messages.size(); nextIndex++) {
            Message message = messages.get(nextIndex);
            int tmpSize = message.getTopic().length() + message.getBody().length;
            Map<String, String> properties = message.getProperties();
            for (Map.Entry<String, String> entry : properties.entrySet()) {
                tmpSize += entry.getKey().length() + entry.getValue().length();
            }
            tmpSize = tmpSize + 20; //for log overhead
            if (tmpSize > sizeLimit) {
                //it is unexpected that single message exceeds the sizeLimit
                //here just let it go, otherwise it will block the splitting process
                if (nextIndex - currIndex == 0) {
                    //if the next sublist has no element, add this one and then break, otherwise just break
                    nextIndex++;
                }
                break;
            }
            if (tmpSize + totalSize > sizeLimit) {
                break;
            } else {
                totalSize += tmpSize;
            }
        }
        List<Message> subList = messages.subList(currIndex, nextIndex);
        currIndex = nextIndex;
        return subList;
    }

    @Override
    public void remove() {
        throw new UnsupportedOperationException("Not allowed to remove");
    }
}

4.3 分布式消息OpenMessaging

除了以上发送方式外,RocketMQ还支持面向云的分布式消息,请参考OpenMessaging 示例

五、RocketMQ消费模式

5.1 订阅方式

消息队列RocketMQ版支持以下两种订阅方式:

1.集群订阅:同一个Group ID所标识的所有Consumer平均分摊消费消息。例如某个Topic9条消息,一个Group ID3Consumer实例,那么在集群消费模式下每个实例平均分摊,只消费其中的3条消息。设置方式如下所示。

// 集群订阅方式设置(不设置的情况下,默认为集群订阅方式)。
properties.put(PropertyKeyConst.MessageModel, PropertyValueConst.CLUSTERING);

2.广播订阅:同一个Group ID所标识的所有Consumer都会各自消费某条消息一次。例如某个Topic9条消息,一个Group ID3Consumer实例,那么在广播消费模式下每个实例都会各自消费9条消息。设置方式如下所示。

// 广播订阅方式设置。
properties.put(PropertyKeyConst.MessageModel, PropertyValueConst.BROADCASTING);

说明:请确保同一个Group ID下所有Consumer实例的订阅关系保持一致。两种不同的订阅方式有着不同的功能限制,例如,广播模式不支持顺序消息、不维护消费进度、不支持重置消费位点等。

5.2 消费方式

消息队列RocketMQ版支持以下两种消息获取方式:

  • Push(默认):消息由消息队列RocketMQ版推送至ConsumerPush方式下,消息队列RocketMQ版还支持批量消费功能,可以将批量消息统一推送至Consumer进行消费。
  • Pull:消息由Consumer主动从消息队列RocketMQ版拉取。

5.2.1 push方式实现

// 程序第一次启动从消息队列头获取数据
consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_FIRST_OFFSET);
// 广播消费
consumer.setMessageModel(MessageModel.BROADCASTING);
consumer.subscribe("TopicTest", "TagA || TagC || TagD");
// 注册回调实现类来处理从broker拉取回来的消息
consumer.registerMessageListener(new MessageListenerConcurrently() {
    @Override
    public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs, ConsumeConcurrentlyContext context) {
        System.out.printf("%s Receive New Messages: %s %n", Thread.currentThread().getName(), msgs);
        // 标记该消息已经被成功消费
        return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
    }
});

5.2.2 pull方式实现

//消费者监听处理消息的方法,这里是抽取的方式
PullResult pullResult = consumer.pull(new MessageQueue("pull-push", "*");
for (MessageExt messageExt : pullResult.getMsgFoundList()) {
    System.out.println("消费线程:" + Thread.currentThread().getName() +
        ", 消息ID:" + messageExt.getMsgId() +
        ", 消息内容:" + new String(messageExt.getBody()));
}

消费端的Push模式是通过长轮询的模式来实现的,就如同下图:

285763201908221607127731182877836.png

Consumer端每隔一段时间主动向broker发送拉消息请求,broker在收到Pull请求后,如果有消息就立即返回数据,Consumer端收到返回的消息后,再回调消费者设置的Listener方法。如果broker在收到Pull请求时,消息队列里没有数据,broker端会阻塞请求直到有数据传递或超时才返回。

当然,Consumer端是通过一个线程将阻塞队列LinkedBlockingQueue<PullRequest>中的PullRequest发送到broker拉取消息,以防止Consumer一致被阻塞。而Broker端,在接收到ConsumerPullRequest时,如果发现没有消息,就会把PullRequest扔到ConcurrentHashMap中缓存起来。

broker在启动时,会启动一个线程不停的从ConcurrentHashMap取出PullRequest检查,直到有数据返回。

区别

推送方式:必须消费者在线,当生产者生成了信息之后,消费者自动获取
抽取方式:当生成者生成了消息之后,这时刚刚启动消费者,自动抽取生成者没有消费的信息

注意:如果是springboot继承的话,消费者不会停止启动,都是使用推送方式

5.3 消息过滤

生产者创建消息

// 创建消息,并指定Topic,Tag和消息体生成者
Message msg = new Message("Topic",// Topic
    "Tag", // Tag
    ("hello world").getBytes("utf-8") //Message body
);

5.3.1 Tag标签过滤

发送消息时我们会为每一条消息设置Tag标签,同一大类中的消息放在一个主题Topic下,但是如果进行分类我们则可以根据Tag进行分类,每一类消费者可能不是关系某个主题下的所有消息,我们就可以通过Tag进行过滤,订阅关注的某一类数据。

// 设置标签 消费者
// 订阅主题Topic和Tag
consumer.subscribe("Topic", "TagA || TagB");

消费者组订阅相同的主题不同的Tag时,如果订阅是多个Tag则通过“||”分割
同一个消费者组订阅的主题,Tag必须相同

5.3.2 SQL过滤

SQL92表达式消息过滤,是通过消息的属性运行SQL过滤表达式进行条件匹配,消息发送时需要设置用户的属性putUserProperty方法设置属性。

支持的语法:

  1. 数值比较, 如>,>=,<,<=,BETWEEN,=
  2. 字符比较, 如=,<>,IN;
  3. IS NULL或者IS NOT NULL;
  4. 逻辑连接符,AND,OR,NOT;

支持的常量类型:

  1. 数值型,如1233.1415
  2. 字符型,如‘abc’(必须用单引号);
  3. NULL,特殊常数;
  4. 布尔值,TRUEFALSE
//订阅主题Topic和Tag
consumer.subscribe("filterSqlTopic", MessageSelector.bySql("i>5"));

注意

  1. 只有使用push模式的消费者才能用使用SQL92标准的sql语句
  2. SQL92过滤默认关闭,需要在代理开始前设置conf文件夹下broker.conf的参数:
#开启对filter的支持
enablePropertyFilter=true

优化

拉取消息时进行过滤的性能很差:

  1. 堆外到堆,一旦每个消费者订阅相同的主题拉消息。
  2. 解码消息属性,一旦每个消费者订阅相同的主题拉消息。

可使用BloomFilter预计算优化情况,具体请参考:RocketMQ通过SQL92过滤消息

注意:此优化默认关闭,需要在代理开始开启时设置一些配置:

  1. enableCalcFilterBitMap = true,表示在构建消费队列时计算位图。
  2. expectConsumerNumUseFilter = XX(Integer, default is 32), 表示估计订阅相同主题的消费者数量。
  3. maxErrorRateOfBloomFilter = XX(1~100,默认为20),表示布隆过滤器的错误率。
  4. enableConsumeQueueExt = true,表示构造消费队列扩展文件。

5.3.3 类过滤

通过定义消息过滤类的接口实现消息过滤

//使用Java代码,在服务器做消息过滤
String filterCode = MixAll.file2String("opt\\classfilter\\MessageFilterImpl.java");
consumer.subscribe("TopicFilter1", "cn.gumx.rocketmq.filter.MessageFilterImpl",filterCode);

自定义消息的过滤类

public class MessageFilterImpl implements MessageFilter {
     
    public boolean match(MessageExt msg, FilterContext arg1) {
        String property = msg.getUserProperty("age");
        if (property != null) {
            int age = Integer.parseInt(property);
            if ((age % 3) == 0 && (age > 10)) {
                return true;
            }
        }
        return false;
    }
}

使用类消息过滤模式,需要额外需要启动filter组件mqfiltersrv服务,否则消费不了,每个broker都需要启动一个,相当于加了一层过滤层。启动命令如下:

./mqfiltersrv -n 10.10.12.203:9876;10.10.12.204:9876 &

filtersrv出现了。减少了Broker的负担,又减少了Consumer接收无用的消息。当然缺点也是有的,多了一层filtersrv网络开销
MessageFilterImpl消息过滤实现类中的代码最好不要带有中文防止错误

注意RocketMQ4.3.1开始删除与mqfilter服务器相关的脚本,4.3.2删除客户端关于mqfilter客户端代码,后面版本不支持该功能。

参考文章

posted @ 2022-04-22 18:27  夏尔_717  阅读(247)  评论(0编辑  收藏  举报