MetaQ是一款分布式、队列模型的消息中间件。基于发布订阅模式,有Push和Pull两种消费方式,支持严格的消息顺序,亿级别的堆积能力,支持消息回溯和多个维度的消息查询。metaq是rocketmq的开源版本,
rocketmq的一些文档:https://help.aliyun.com/document_detail/44397.html?spm=a2c4g.95837.0.0.3db95ac4zlV500

架构:

RocketMQ的架构包括四个主要组件:Name Server、Broker、Producer和Consumer。

  1. Name Server:Name Server是RocketMQ的核心组件之一,用于维护Broker的地址信息和路由信息。在RocketMQ中,Producer和Consumer通过Name Server来发现Broker,并且获取指定Topic的Broker列表和路由信息。Broker启动的时候,会往每台NameServer(因为NameServer之间不通信,所以每台都得注册)注册自己的信息,这些信息包括自己的ip和端口号,自己这台Broker有哪些topic等信息。
  2. Broker:Broker是RocketMQ的另一个核心组件,用于存储和传输消息。一个Broker可以管理多个Topic,每个Topic可以有多个队列。Broker分为Master和Slave两种模式,Master是主节点,负责消息的写入和读取,而Slave是备份节点,负责消息的备份和恢复。Master和Slave之间通过异步复制来保证数据的一致性。
  3. Producer:Producer是用于生产消息的组件。Producer将消息发送到指定的Topic,在发送的过程中,Producer需要通过Name Server获取指定Topic的Broker列表和路由信息,并将消息发送到对应的Broker上。
  4. Consumer:Consumer是用于消费消息的组件。Consumer从指定的Topic订阅消息,在订阅的过程中,Consumer需要通过Name Server获取指定Topic的Broker列表和路由信息,并从对应的Broker上消费消息。
  5. message queue:消息物理管理单位,一个topic将有若干个q,若topic创建在多个topic,则不同的topic都有若干个q,消息将物理地存储落在不同Broker节点上,具有水平拓展的能力。

tag和topic

Topic表示一类消息的集合,每个主题包含若干条消息,每条消息只能属于一个主题,是RocketMQ进行消息订阅的基本单位。
RocketMQ的架构支持水平扩展和高可用性,可以通过添加Broker节点来扩展集群的处理能力,并且通过Master-Slave模式来保证消息的可靠性和高可用性。RocketMQ通过Topic完成消息的发布和订阅。消息生产者将消息发送到Topic中,而消息消费者则通过订阅该Topic来消费消息

到底什么时候该用Topic,什么时候该用Tag?
RocketMQ的tag是用于消息标记的一种属性,可以更精细地控制消息的消费。在发送消息时,可以为每条消息设置tag,消费者在订阅消息时可以指定tag来只消费特定标记的消息,从而避免消费无用消息,提高系统性能。例如,对于一个订单系统,可以设置不同的tag来表示不同的订单状态,消费者只订阅感兴趣的订单状态的消息。
从以下几个方面进行判断:
● 消息类型是否一致:如普通消息、事务消息、定时(延时)消息、顺序消息,不同的消息类型使用不同的Topic,无法通过Tag进行区分。
● 业务是否相关联:没有直接关联的消息,如淘宝交易消息,京东物流消息使用不同的Topic进行区分;而同样是天猫交易消息,电器类订单、女装类订单、化妆品类订单的消息可以用Tag进行区分。
● 消息优先级是否一致:如同样是物流消息,盒马必须小时内送达,天猫超市24小时内送达,淘宝物流则相对会慢一些,不同优先级的消息用不同的Topic进行区分。
● 消息量级是否相当:有些业务消息虽然量小但是实时性要求高,如果跟某些万亿量级的消息使用同一个Topic,则有可能会因为过长的等待时间而“饿死”,此时需要将不同量级的消息进行拆分,使用不同的Topic。
总的来说,针对消息分类,您可以选择创建多个Topic,或者在同一个Topic下创建多个Tag。但通常情况下,不同的Topic之间的消息没有必然的联系,而Tag则用来区分同一个Topic下相互关联的消息,例如全集和子集的关系、流程先后的关系。

场景示例:
以天猫交易平台为例,订单消息和支付消息属于不同业务类型的消息,分别创建Topic_Order和Topic_Pay,其中订单消息根据商品品类以不同的Tag再进行细分,例如电器类、男装类、女装类、化妆品类等被各个不同的系统所接收。

nameServer

在RocketMQ中,NameServer充当了注册中心的角色,它维护了所有可用的RocketMQ broker节点的信息,并为producer和consumer提供服务发现。当producer和consumer启动时,只需要指定NameServer的地址,NameServer就会将可用的broker节点的信息告诉它们,以便它们可以与相应的broker建立连接并进行消息传递。因此,用户只需要关注NameServer的地址,而不需要手动指定broker的地址。在某些情况下,RocketMQ 的注册中心可以出现数据不一致性,但必须保证高可用,就算是返回了包含不实的信息的结果也比什么都不返回要好,如果采用zookeeper来提供服务注册发现,如果某次选举时间过长(30 ~ 120s),那么将导致长时间获取不到任务服务的信息,造成严重的后果。因此,RocketMQ自制的NameServer实现的是AP(可用性 分区容错性)。[附:分区容错性”指分布式系统在遇到某节点或网络分区故障的时候,仍然能够对外提供满足一致性和可用性的服务]

消息幂等

当出现消费者对某条消息重复消费的情况时,重复消费的结果与消费一次的结果是相同的,并且多次消费并未对业务系统产生任何负面影响,那么这个消费者的处理过程就是幂等的。
例如,在支付场景下,消费者消费扣款消息,对一笔订单执行扣款操作,扣款金额为100元。如果因网络不稳定等原因导致扣款消息重复投递,消费者重复消费了该扣款消息,但最终的业务结果是只扣款一次,扣费100元,且用户的扣款记录中对应的订单只有一条扣款流水,不会多次扣除费用。那么这次扣款操作是符合要求的,整个消费过程实现了消费幂等。
在互联网应用中,尤其在网络不稳定的情况下,消息队列RocketMQ版的消息有可能会出现重复。如果消息重复会影响您的业务处理,请对消息做幂等处理。
消息重复的场景如下:
● 发送时消息重复当一条消息已被成功发送到服务端并完成持久化,此时出现了网络闪断或者客户端宕机,导致服务端对客户端应答失败。 如果此时生产者意识到消息发送失败并尝试再次发送消息,消费者后续会收到两条内容相同但Message ID不同的消息。
● 投递时消息重复消息消费的场景下,消息已 投递到消费者并完成业务处理,当客户端给服务端反馈应答的时候网络闪断。为了保证消息至少被消费一次,消息队列RocketMQ版的服务端将在网络恢复后再次尝试投递之前已被处理过的消息,消费者后续会收到两条内容相同并且Message ID也相同的消息。
● 负载均衡时消息重复(包括但不限于网络抖动、Broker重启以及消费者应用重启) 当消息队列RocketMQ版的Broker或客户端重启、扩容或缩容时,会触发Rebalance,此时消费者可能会收到少量重复消息。
因为不同的Message ID对应的消息内容可能相同,有可能出现冲突(重复)的情况,所以真正安全的幂等处理,不建议以Message ID作为处理依据。最好的方式是以业务唯一标识作为幂等处理的关键依据,而业务的唯一标识可以通过消息Key设置。
以支付场景为例,可以将消息的Key设置为订单号,作为幂等处理的依据。具体代码示例如下:

Message message = new Message();
message.setKey("ORDERID_100");
SendResult sendResult = producer.send(message);

接入:
metaqJava客户端接入

<dependency>
    <groupId>com.taobao.metaq.final</groupId>
    <artifactId>metaq-client</artifactId>
    <version>4.3.2.Final</version>
</dependency>

rocketmq客户端接入:

<dependency>
<groupId>org.apache.rocketmq</groupId>
 <artifactId>rocketmq-client</artifactId>
 <version>4.5.2</version>
</dependency>

生产者消费者代码:

生产者:

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.common.message.Message;
import org.apache.rocketmq.remoting.exception.RemotingException;

public class MQproducer {
    private DefaultMQProducer producer;

    public MQproducer() throws MQClientException {
        producer = new DefaultMQProducer("producer_group");
        producer.setNamesrvAddr("localhost:9876");
        producer.start();
    }

    public void send(String topic, String message) throws MQClientException, InterruptedException, RemotingException, MQBrokerException {
        Message msg = new Message(topic, "tag", message.getBytes());
        SendResult sendResult = producer.send(msg);
        System.out.println("Send message success." + sendResult);
    }

    public void shutdown() {
        producer.shutdown();
    }
}

消费者:

public class MQconsumer {
    private DefaultMQPushConsumer consumer;

    public MQconsumer() throws MQClientException {
        consumer = new DefaultMQPushConsumer("consumer_group");
        consumer.setNamesrvAddr("localhost:9876");
        consumer.subscribe("topic", "*");
        consumer.registerMessageListener((MessageListenerConcurrently) (msgs, context) -> {
            System.out.println(Thread.currentThread().getName() + " Receive New Messages: " + msgs);
            return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
        });
        consumer.start();
    }

    public void shutdown() {
        consumer.shutdown();
    }
}

main:

public class Main {
    public static void main(String[] args) throws MQBrokerException, RemotingException, InterruptedException, MQClientException {
        MQproducer producer = new MQproducer();
        producer.send("topic", "hello world");
        producer.shutdown();

        MQconsumer consumer = new MQconsumer();
        TimeUnit.MINUTES.sleep(1);
        consumer.shutdown();
    }
}

如果订阅多个tag,写法:

consumer.subscribe("TopicB", "Tag1||Tag2", new MessageListener() {
        public Action consume(Message message, ConsumeContext context) {
            System.out.println(message.getMsgID());
            return Action.CommitMessage;
        }
    });

使用metaq接入的代码示例:

public class Producer {

    public static void main(String[] args) throws MQClientException, InterruptedException {

        /**
         * 一个应用创建一个Producer,由应用来维护此对象,可以设置为全局对象或者单例<br>
         * 注意:ProducerGroupName需要由应用来保证唯一<br>
         * ProducerGroup这个概念发送普通的消息时,作用不大,但是发送分布式事务消息时,比较关键,
         * 因为服务器会回查这个Group下的任意一个Producer
         */

        MetaProducer producer = new MetaProducer("manhongTestPubGroup");

        /**
         * Producer对象在使用之前必须要调用start初始化,初始化一次即可<br>
         * 注意:切记不可以在每次发送消息时,都调用start方法
         */
        producer.start();

        /**
         * 下面这段代码表明一个Producer对象可以发送多个topic,多个tag的消息。
         * 注意:send方法是同步调用,只要不抛异常就标识成功。但是发送成功也可会有多种状态,<br>
         * 例如消息写入Master成功,但是Slave不成功,这种情况消息属于成功,但是对于个别应用如果对消息可靠性要求极高,<br>
         * 需要对这种情况做处理。另外,消息可能会存在发送失败的情况,失败重试由应用来处理。
         */
        try {

            for (int i = 0; i < 20; i++) {
                {
                    Message msg = new Message("Jodie_topic_1023",// topic
                            "TagA",// tag
                            "OrderID001",// key,消息的Key字段是为了唯一标识消息的,方便运维排查问题。如果不设置Key,则无法定位消息丢失原因。
                            ("Hello MetaQ").getBytes());// body
                    SendResult sendResult = producer.send(msg);
                    System.out.println(sendResult);
                }

                {
                    Message msg = new Message("TopicTest2",// topic
                            "TagB",// tag
                            "OrderID0034",// key
                            ("Hello MetaQ").getBytes());// body
                    SendResult sendResult = producer.send(msg);
                    System.out.println(sendResult);
                }

                {
                    Message msg = new Message("TopicTest3",// topic
                            "TagC",// tag
                            "OrderID061",// key
                            ("Hello MetaQ").getBytes());// body
                    SendResult sendResult = producer.send(msg);
                    System.out.println(sendResult);
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }

        /**
         * 应用退出时,要调用shutdown来清理资源,关闭网络连接,从MetaQ服务器上注销自己
         * 注意:我们建议应用在JBOSS、Tomcat等容器的退出钩子里调用shutdown方法
         */
        producer.shutdown();
    }
}

顺序消费

消息顺序消费,要保证消息顺序消费,同一个queue就只能被一个消费者所消费,因此对broker中消费队列加锁是无法避免的。同一时刻,一个消费队列只能被一个消费者消费,消费者内部,也只能有一个消费线程来消费该队列。即,同一时刻,一个消费队列只能被一个消费者中的一个线程消费
RocketMQ支持发送顺序消息,保证同一队列内消息的顺序消费。所谓顺序消息,就是指消息在生产者端按照一定顺序发送到MQ中,而消费者端接收到的消息也是按照相同顺序进行消费。
在RocketMQ中,顺序消息的实现需要保证以下两个条件:

  1. 消息生产者将同一个业务的消息发送到相同的队列,消费者按相同的顺序从队列中消费消息。
  2. 当前队列中只有一个消费者在消费,保证消息消费的顺序。
    为了支持顺序消息,RocketMQ提供了一种特殊的消息发送方式:MessageQueueSelector。这种方式可以让生产者将特定业务的消息发送到指定的队列中,从而保证消费者按照特定顺序消费消息。
    使用MessageQueueSelector发送顺序消息的步骤如下:
  3. 首先创建MessageQueueSelector对象。这个对象需要实现select方法,该方法的作用是根据业务规则选择目标队列。RocketMQ默认提供了一些实现方法,比如HashMessageQueueSelector、RandomMessageQueueSelector等,可以根据实际情况选择。
  4. 生产者使用select方法将消息发送到指定队列。
  5. 消费者按照相同顺序从指定队列中消费消息。
    通过以上三个步骤,即可实现RocketMQ的顺序消息功能。但需要注意的是,如果消息生产速度过快,有可能会导致消息在队列中乱序。因此,在使用顺序消息时,需要注意控制消息生产的速度,保证消息有序到达队列中。

顺序生产消息代码:

public void sendBySequence() throws MQBrokerException, RemotingException, InterruptedException, MQClientException {
        String[] tags = new String[]{"TagA", "TagB", "TagC", "TagD", "TagE"};
        for (int i = 0; i < 100; i++) {
            // 订单ID相同的消息要有序
            int orderId = i % 10;
            Message msg =
                    new Message("TopicTest", tags[i % tags.length], "KEY" + i,
                            ("Hello MetaQ " + i).getBytes());
//selector – 消息队列选择器,通过它我们可以获得目标消息队列以将消息传递到目标队列。
            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);

            System.out.println(sendResult);
        }
    }

顺序消费消息代码:

consumer.subscribe("TopicTest", "TagA || TagC || TagD");

        consumer.registerMessageListener(new MessageListenerOrderly() {
            AtomicLong consumeTimes = new AtomicLong(0);


            @Override
            public ConsumeOrderlyStatus consumeMessage(List<MessageExt> msgs, ConsumeOrderlyContext context) {
                context.setAutoCommit(true);
                System.out.println(Thread.currentThread().getName() + " Receive New Messages: " + msgs);
                this.consumeTimes.incrementAndGet();
                if ((this.consumeTimes.get() % 2) == 0) {
                    return ConsumeOrderlyStatus.SUCCESS;
                } else if ((this.consumeTimes.get() % 3) == 0) {
                    return ConsumeOrderlyStatus.ROLLBACK;
                } else if ((this.consumeTimes.get() % 4) == 0) {
                    return ConsumeOrderlyStatus.COMMIT;
                } else if ((this.consumeTimes.get() % 5) == 0) {
                    context.setSuspendCurrentQueueTimeMillis(3000);
                    return ConsumeOrderlyStatus.SUSPEND_CURRENT_QUEUE_A_MOMENT;
                }

                return ConsumeOrderlyStatus.SUCCESS;
            }