RocketMQ实践简述

RocketMQ作为阿里开源的一款高性能、高吞吐量的消息中间件,下面就简要阐述一下其核心概念

Producer

消息发送方,将业务系统中产生的消息发送到brokers(brokers可以理解为消息代理),rocketmq提供了以下消息发送方式:同步、异步、单向(适用于可靠性要求不高的场景,如日志收集)。(A producer sends messages generated by the business application systems to brokers. RocketMQ provides multiple paradigms of sending: synchronous, asynchronous and one-way.)

Producer Group

相同角色的生产者被归为同一组,譬如一个应用web应用可能会部署多个实例,那么这些实例就是一个生产组,生产者分组的作用只体现在消息回查的时候(即如果一个生产者组中的一个生产者实例发送一个事务消息到broker后挂掉了,那么broker会回查此实例所在组的其他实例,从而进行消息的提交或回滚操作)。(Producers of the same role are grouped together. A different producer instance of the same producer group may be contacted by a broker to commit or roll back a transaction in case the original producer crashed after the transaction.)

Consumer

消息的消费方,主要又一下2种方式(A Consumer pulls messages from brokers and feeds them into application. In perspective of user application, two types of consumers are provided:)

PullConsumer

从brokers中主动获取消息并消费(Pull consumer actively pulls messages from brokers. Once batches of messages are pulled, user application initiates consuming process.)

PushConsumer

其内部也是通过pull的方式获取消息,只是将消息获取、消息消费以及其他的维护功能进行了封装和扩展,在消息回掉时调用用户自定义i的回掉接口(Push consumer, on the other hand, encapsulates message pulling, consuming progress and maintaining other work inside, leaving a callback interface to end user to implement which will be executed on message arrival.)

Consumer Group

和生产者组相似。主要作用体现在对消费者的负载均衡和容错方面。需要注意:相同消费者组的消费者订阅的消息主题必须相同(Similar to previously mentioned producer group, consumers of the exactly same role are grouped together and named Consumer Group.

Consumer Group is a great concept with which achieving goals of load-balance and fault-tolerance, in terms of message consuming, is super easy.

Warning: consumer instances of a consumer group must have exactly the same topic subscription(s).)

Topic

主题就是消息传递的类型。一个生产者实例可以发送消息到多个主题,多个生产者实例也可以发送消息到同一个主题。同样的,对于消费者端来说,一个消费者组可以订阅多个主题的消息,一个主题的消息也可以被多个消费者组订阅

(Topic is a category in which producers deliver messages and consumers pull messages. Topics have very loose relationship with producers and consumers. Specifically, a topic may have zero, one or multiple producers that sends messages to it; conversely, a producer can send messages of different topics. In consumer’s perspective, a topic may be subscribed by zero, one or multiple consumer groups. And a consumer group, similarly, may subscribe to one or more topics as long as instances of this group keep their subscription consistent.)

Message

消息就像是你发送邮件信息。每个消息必须指定一个主题,就好比每个信封上都必须写明收件人。

(Message is the information to be delivered. A message must have a topic, which can be interpreted as address of your letter to mail to. A message may also have an optional tag and extra key-value pairs. For example, you may set a business key to your message and look up the message on a broker server to diagnose issues during development.)

Tag

标签,可以被当作是子主题。主要用于区分同一个主题下的不同作用或者说不同业务的消息。同时也是避免主题定义过多引起性能问题,通常情况下一个生产者组只向一个主题发送消息,其中不同业务的消息通过标签或者说子主题来区分。

(Tag, in other words sub-topic, provides extra flexibility to users. With tag, messages with different purposes from the same business module may have the same topic and different tag. Tags would be helpful to keep your code clean and coherent, and tags also can facilitate the query system RocketMQ provides.)

Message Queue

消息主题被划分为一个或者多个子主题,每个子主题被称为消息队列。主要作用是提高并发以及故障切换。

(Topic is partitioned into one or more sub-topics, “message queues”.)

Message Model

  • Clustering:缺省的,Consumer的MessageModel就是CLUSTERING模式,也就是同1个Consumer Group内部,多个Consumer分摊同1个topic的多个queue,也就是负载均衡。

  • Broadcasting:同1个Consumer Group内部也变成了广播,此时ConsumerGroup其实就没有区分的意义了。此时,不管是1个Consumer Group,还是多个Consumer Group,对同1个topic的消息,都变成了广播。


代码实例

Producer消息发送方式:同步、异步、单向

Producer默认的消息发送方式是采用同步发送如下实例:

package org.apache.rocketmq.example.simple;

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.common.RemotingHelper;

public class Producer {

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

        DefaultMQProducer producer = new DefaultMQProducer("ProducerGroupName");

        producer.start();

        for (int i = 0; i < 10000000; i++)

            try {

                {

                    Message msg = new Message("TopicTest",

                        "TagA",

                        "OrderID188",

                        "Hello world".getBytes(RemotingHelper.DEFAULT_CHARSET));

                    SendResult sendResult = producer.send(msg);

                    System.out.printf("%s%n", sendResult);

                }

            catch (Exception e) {

                e.printStackTrace();

            }

        producer.shutdown();

    }

}

Producer异步发送消息实例代码如下:

package org.apache.rocketmq.example.simple;

import java.io.UnsupportedEncodingException;

import org.apache.rocketmq.client.exception.MQClientException;

import org.apache.rocketmq.client.producer.DefaultMQProducer;

import org.apache.rocketmq.client.producer.SendCallback;

import org.apache.rocketmq.client.producer.SendResult;

import org.apache.rocketmq.common.message.Message;

import org.apache.rocketmq.remoting.common.RemotingHelper;

public class AsyncProducer {

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

        DefaultMQProducer producer = new DefaultMQProducer("Jodie_Daily_test");

        producer.start();

        producer.setRetryTimesWhenSendAsyncFailed(0);

        for (int i = 0; i < 10000000; i++) {

            try {

                final int index = i;

                Message msg = new Message("Jodie_topic_1023",

                    "TagA",

                    "OrderID188",

                    "Hello world".getBytes(RemotingHelper.DEFAULT_CHARSET));

                producer.send(msg, new SendCallback() {

                    @Override

                    public void onSuccess(SendResult sendResult) {

                        System.out.printf("%-10d OK %s %n", index, sendResult.getMsgId());

                    }

                    @Override

                    public void onException(Throwable e) {

                        System.out.printf("%-10d Exception %s %n", index, e);

                        e.printStackTrace();

                    }

                });

            catch (Exception e) {

                e.printStackTrace();

            }

        }

        producer.shutdown();

    }

}

producer单向发送消息实例

public class OnewayProducer {

    public static void main(String[] args) throws Exception{

        //Instantiate with a producer group name.

        DefaultMQProducer producer = new DefaultMQProducer("ExampleProducerGroup");

        //Launch the instance.

        producer.start();

        for (int i = 0; i < 100; i++) {

            //Create a message instance, specifying topic, tag and message body.

            Message msg = new Message("TopicTest" /* Topic */,

                "TagA" /* Tag */,

                ("Hello RocketMQ " +

                    i).getBytes(RemotingHelper.DEFAULT_CHARSET) /* Message body */

            );

            //Call send message to deliver message to one of brokers.

            producer.sendOneway(msg);

        }

        //Shut down once the producer instance is not longer in use.

        producer.shutdown();

    }

}

发送顺序消息,消息的顺序性是通过业务调用方和消息中间件共同完成,主要思想是:将消息,添加到按某种算法(随机/Hash)获取的发送队列,这样 就保证在发送端,在某个特定的队列里面消息是有顺序的。

至于消费者,首先通过topic获取消费队列,发送锁住队列的消息至broker,broker返回锁住成功的队列结合,消费者在设置是否对队列枷锁的标志代码实例如下:

生产者:

public class OrderedProducer {

    public static void main(String[] args) throws Exception {

        //Instantiate with a producer group name.

        MQProducer producer = new DefaultMQProducer("example_group_name");

        //Launch the instance.

        producer.start();

        String[] tags = new String[] {"TagA""TagB""TagC""TagD""TagE"};

        for (int i = 0; i < 100; i++) {

            int orderId = i % 10;

            //Create a message instance, specifying topic, tag and message body.

            Message msg = new Message("TopicTestjjj", tags[i % tags.length], "KEY" + i,

                    ("Hello RocketMQ " + i).getBytes(RemotingHelper.DEFAULT_CHARSET));

            // RocketMQ通过MessageQueueSelector中实现的算法来确定消息发送到哪一个队列上

            // RocketMQ默认提供了两种MessageQueueSelector实现:随机/Hash

            // 当然你可以根据业务实现自己的MessageQueueSelector来决定消息按照何种策略发送到消息队列中

            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.printf("%s%n", sendResult);

        }

        //server shutdown

        producer.shutdown();

    }

}

消费者:

public class OrderedConsumer {

    public static void main(String[] args) throws Exception {

        DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("example_group_name");

        consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_FIRST_OFFSET);

        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(false);

                System.out.printf(Thread.currentThread().getName() + " Receive New Messages: " + msgs + "%n");

                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;

            }

        });

        consumer.start();

        System.out.printf("Consumer Started.%n");

    }

}

事务消息,Rocket MQ的事务消息,并不是针对整个体系而言(生产者,消息中间件,消费者),而只是针对生产者和消息中间件(rocket MQ的 broker)。事务消息的时序图如下所示

示例代码如下:

public class TransactionProducer {

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

        //如果broker未获取到本地消息的事务状态(提交或者回滚),broker会定时访问此接口的checkLocalTransactionState方法来判断,本地事务的状态

        TransactionCheckListener transactionCheckListener = new TransactionCheckListenerImpl();

        // 构造事务消息的生产者

        TransactionMQProducer producer = new TransactionMQProducer("please_rename_unique_group_name");

        // 事务回查最小并发数

        producer.setCheckThreadPoolMinSize(2);

        // 事务回查最大并发数

        producer.setCheckThreadPoolMaxSize(2);

        // 队列数

        producer.setCheckRequestHoldMax(2000);

        producer.setTransactionCheckListener(transactionCheckListener);

        producer.start();

        String[] tags = new String[] {"TagA""TagB""TagC""TagD""TagE"};

        //也就是上文途中的第4步

        TransactionExecuterImpl tranExecuter = new TransactionExecuterImpl();

        for (int i = 0; i < 100; i++) {

            try {

                Message msg =

                    new Message("TopicTest", tags[i % tags.length], "KEY" + i,

                        ("Hello RocketMQ " + i).getBytes(RemotingHelper.DEFAULT_CHARSET));

                SendResult sendResult = producer.sendMessageInTransaction(msg, tranExecuter, null);

                System.out.printf("%s%n", sendResult);

                Thread.sleep(10);

            catch (MQClientException | UnsupportedEncodingException e) {

                e.printStackTrace();

            }

        }

        for (int i = 0; i < 100000; i++) {

            Thread.sleep(1000);

        }

        producer.shutdown();

    }

}

并发消费消息

consumer被分为2类:MQPullConsumer和MQPushConsumer,其实本质都是拉模式(pull),即consumer轮询从broker拉取消息。

区别是:

push方式里,consumer把轮询过程封装了,并注册MessageListener监听器,取到消息后,唤醒MessageListener的consumeMessage()来消费,对用户而言,感觉消息是被推送过来的。

pull方式里,取消息的过程需要用户自己写,首先通过消费的Topic拿到MessageQueue的集合,遍历MessageQueue集合,然后针对每个MessageQueue批量取消息,一次取完后,记录该队列下一次要取的开始offset,直到取完了,再换另一个MessageQueue。

PushConsumer方式消费消息实例代码如下:

public class PushConsumerTest { 

   

    public static void main(String[] args) { 

        DefaultMQPushConsumer consumer=new DefaultMQPushConsumer("test-topic-1"); 

        consumer.setNamesrvAddr("192.168.250.5:9876"); 

        try 

               

            // 订阅PushTopic下Tag为push的消息,都订阅消息 

            consumer.subscribe("PushTopic""push"); 

               

            // 程序第一次启动从消息队列头获取数据 

            consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_FIRST_OFFSET); 

            //可以修改每次消费消息的数量,默认设置是每次消费一条 

            // consumer.setConsumeMessageBatchMaxSize(10); 

   

            //注册消费的监听 

            consumer.registerMessageListener(new MessageListenerConcurrently() { 

               //在此监听中消费信息,并返回消费的状态信息 

                public ConsumeConcurrentlyStatus consumeMessage( 

                        List<MessageExt> msgs, 

                        ConsumeConcurrentlyContext context) { 

                       

                    // msgs中只收集同一个topic,同一个tag,并且key相同的message 

                    // 会把不同的消息分别放置到不同的队列中 

                    for(Message msg:msgs){ 

               

                        System.out.println(new String(msg.getBody())); 

                    }    

                    return ConsumeConcurrentlyStatus.CONSUME_SUCCESS; 

                

            }); 

   

            consumer.start(); 

            Thread.sleep(5000); 

            //5秒后挂载消费端消费 

            consumer.suspend(); 

               

        catch (Exception e) { 

            e.printStackTrace(); 

        

    

PullConsumer消费方式消费消息示例代码:

public class PullConsumer {

    private static final Map<MessageQueue, Long> OFFSE_TABLE = new HashMap<MessageQueue, Long>();

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

        DefaultMQPullConsumer consumer = new DefaultMQPullConsumer("please_rename_unique_group_name_5");

        consumer.setNamesrvAddr("192.168.250.5:9876");

        consumer.start();

        Set<MessageQueue> mqs = consumer.fetchSubscribeMessageQueues("TopicTest1");

        for (MessageQueue mq : mqs) {

            System.out.printf("Consume from the queue: " + mq + "%n");

            SINGLE_MQ:

            while (true) {

                try {

                    PullResult pullResult =

                        consumer.pullBlockIfNotFound(mq, null, getMessageQueueOffset(mq), 32);

                    System.out.printf("%s%n", pullResult);

                    putMessageQueueOffset(mq, pullResult.getNextBeginOffset());

                    switch (pullResult.getPullStatus()) {

                        case FOUND:

                            break;

                        case NO_MATCHED_MSG:

                            break;

                        case NO_NEW_MSG:

                            break SINGLE_MQ;

                        case OFFSET_ILLEGAL:

                            break;

                        default:

                            break;

                    }

                catch (Exception e) {

                    e.printStackTrace();

                }

            }

        }

        consumer.shutdown();

    }

    private static long getMessageQueueOffset(MessageQueue mq) {

        Long offset = OFFSE_TABLE.get(mq);

        if (offset != null)

            return offset;

        return 0;

    }

    private static void putMessageQueueOffset(MessageQueue mq, long offset) {

        OFFSE_TABLE.put(mq, offset);

    }

}

MQPullConsumer定时消费

public class PullScheduleService {

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

        final MQPullConsumerScheduleService scheduleService = new MQPullConsumerScheduleService("GroupName1");

        scheduleService.getDefaultMQPullConsumer().setNamesrvAddr("192.168.250.5:9876");

        scheduleService.setMessageModel(MessageModel.CLUSTERING);

        scheduleService.registerPullTaskCallback("TopicTest1"new PullTaskCallback() {

            @Override

            public void doPullTask(MessageQueue mq, PullTaskContext context) {

                MQPullConsumer consumer = context.getPullConsumer();

                try {

                    long offset = consumer.fetchConsumeOffset(mq, false);

                    if (offset < 0)

                        offset = 0;

                    PullResult pullResult = consumer.pull(mq, "*", offset, 32);

                    System.out.printf("%s%n", offset + "\t" + mq + "\t" + pullResult);

                    switch (pullResult.getPullStatus()) {

                        case FOUND:

                            break;

                        case NO_MATCHED_MSG:

                            break;

                        case NO_NEW_MSG:

                        case OFFSET_ILLEGAL:

                            break;

                        default:

                            break;

                    }

                    // 存储Offset,客户端每隔5s会定时刷新到Broker

                    consumer.updateConsumeOffset(mq, pullResult.getNextBeginOffset());

                    // 设置间隔100ms后重新拉取

                    context.setPullNextDelayTimeMillis(100);

                catch (Exception e) {

                    e.printStackTrace();

                }

            }

        });

        scheduleService.start();

    }

}

猜你喜欢

转载自blog.csdn.net/jiahao1186/article/details/85236203