RocketMQ基本消息生产消费案例讲解

依赖
<dependency>
    <groupId>org.apache.rocketmq</groupId>
    <artifactId>rocketmq-client</artifactId>
    <version>4.7.1</version>
</dependency>

依赖的版本尽量与你RocketMQ服务的版本一样。

消息发送与消费

使用RocketMQ发送三种类型的消息:同步消息、异步消息和单向消息。其中前两种消息是可靠的,因为会有发送是否成功的应答。可以根据应答进行相应处理,比如失败重发,日志记录等等。

Producer发送同步消息

这种可靠性同步地发送方式使用的比较广泛,比如:重要的消息通知,短信通知。

public class SyncProducerDemo {


    public static void main(String[] args) throws Exception {
        //创建一个消息生产者,传入的是消息组名称
        DefaultMQProducer producer = new DefaultMQProducer("test-group");

        //输入nameserver服务的地址
        producer.setNamesrvAddr("192.168.18.142:9876");

        //启动生产者
        producer.start();

        for (int i = 0;i<50;i++){
            //创建消息
            Message message = new Message("test-topic","test-tag",("test msg" + i).getBytes());
            //发送,返回结果对象
            SendResult sendResult = producer.send(message);
            System.out.println(sendResult.getMsgId()); //消息id
            System.out.println(sendResult.getMessageQueue()); //队列信息
            System.out.println(sendResult.getSendStatus());  //发送结果
            System.out.println(sendResult.getOffsetMsgId()); //下一个要消费的消息的偏移量
            System.out.println(sendResult.getQueueOffset());  //队列消息偏移量
            System.out.println();
            System.out.println("================================================");
            TimeUnit.SECONDS.sleep(2);
        }
    }
}

结果:
在这里插入图片描述

发送异步消息

异步消息通常用在对响应时间敏感的业务场景,即发送端不能容忍长时间地等待Broker的响应。

public class AsyncProducerDemo {

    public static void main(String[] args) throws Exception{
        //创建一个消息生产者,传入的是消息组名称
        DefaultMQProducer producer = new DefaultMQProducer("test-group");

        //输入nameserver服务的地址
        producer.setNamesrvAddr("192.168.18.142:9876");

        //启动生产者
        producer.start();

        //设置异步发送失败时的重试次数
        producer.setRetryTimesWhenSendAsyncFailed(0);

        for (int i = 0;i<50;i++){
            Message message = new Message("test-topic","test-tag",("test msg for async " + i).getBytes());
            producer.send(message, new SendCallback() {
                @Override
                public void onSuccess(SendResult sendResult) {
                    //异步发送成功回调
                    System.out.println(sendResult.getMsgId());
                }

                @Override
                public void onException(Throwable e) {
                    //异步发送异常回调
                    e.printStackTrace();
                }
            });
        }
    }
}

在这里插入图片描述

单向发送消息OneWay

单向发送消息就是我发了就算了,不管是否成功,不同步,异步也没有回调。

public class SendOneWayDemo {

    public static void main(String[] args) throws Exception{
        //创建一个消息生产者,传入的是消息组名称
        DefaultMQProducer producer = new DefaultMQProducer("test-group");

        //输入nameserver服务的地址
        producer.setNamesrvAddr("192.168.18.142:9876");

        //启动生产者
        producer.start();

        for (int i = 0;i<50;i++){
            Message message = new Message("test-topic","test-tag",("test msg one way" + i).getBytes());
            producer.sendOneway(message);
        }
    }
}
消息的基本消费
public class PushConsumerDemo {
    public static void main(String[] args) throws Exception{

        DefaultMQPushConsumer defaultMQPushConsumer = new DefaultMQPushConsumer("test-consumer");
        defaultMQPushConsumer.setNamesrvAddr("192.168.18.142:9876");

        //订阅某个主题,然后使用tag过滤消息,*代表不过滤
        defaultMQPushConsumer.subscribe("test-topic","*");


        //注册监听回调实现类来处理broker推送过来的消息,MessageListenerConcurrently是并发消费
        defaultMQPushConsumer.registerMessageListener(new MessageListenerConcurrently() {
            @Override
            public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs, ConsumeConcurrentlyContext context) {
                System.out.println(msgs.size());
                for (MessageExt messageExt:msgs){
                    System.out.println(messageExt.getMsgId());
                }
                return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
            }
        });
        defaultMQPushConsumer.start();
        System.out.println("消费者启动完成");
    }
}

在这里插入图片描述

消息的有序性

消息有序指的是可以按照消息的发送顺序来消费(FIFO)。RocketMQ可以严格的保证消息有序。
如果控制发送的顺序消息只依次发送到同一个queue中,消费的时候只从这个queue上依次拉取,则就保证了顺序。

顺序生产:

//push类型消费者
public class OrderProducerDemo {

    public static void main(String[] args) throws Exception {
        //创建一个消息生产者,传入的是消息组名称
        DefaultMQProducer producer = new DefaultMQProducer("test-group");

        //输入nameserver服务的地址
        producer.setNamesrvAddr("192.168.18.142:9876");

        List<OrderStep> orderSteps = buildOrders();

        //启动生产者
        producer.start();

        //创建标签
        String[] tags = new String[]{"Tag1", "Tag2", "Tag3"};

        for (int i = 0;i<orderSteps.size();i++){

            Message message = new Message("test-order-msg",tags[i%tags.length],orderSteps.get(i).toString().getBytes());
            //MessageQueueSelector可以自定义要发送的队列
            SendResult sendResult = producer.send(message,new MessageQueueSelector() {
                @Override
                public MessageQueue select(List<MessageQueue> mqs, Message msg, Object arg) {
                    //arg 为orderSteps.get(i).orderId
                    int len = mqs.size();
                    long orderId = (long) arg;
                    //确保同一订单id的消息能够发送到同一队列中
                    long queueId = orderId%len;

                    return mqs.get((int) queueId);

                }
            },orderSteps.get(i).orderId);
            System.out.println("消息(" + orderSteps.get(i).orderId + "," + orderSteps.get(i).desc + ")发送到了队列" + sendResult.getMessageQueue().getQueueId());
        }


    }

    private static List<OrderStep> buildOrders() {
        List<OrderStep> orderList = new ArrayList<OrderStep>();

        OrderStep orderDemo = new OrderStep();
        orderDemo.setOrderId(15103111039L);
        orderDemo.setDesc("创建");
        orderList.add(orderDemo);

        orderDemo = new OrderStep();
        orderDemo.setOrderId(15103111065L);
        orderDemo.setDesc("创建");
        orderList.add(orderDemo);

        orderDemo = new OrderStep();
        orderDemo.setOrderId(15103111039L);
        orderDemo.setDesc("付款");
        orderList.add(orderDemo);

        orderDemo = new OrderStep();
        orderDemo.setOrderId(15103117235L);
        orderDemo.setDesc("创建");
        orderList.add(orderDemo);

        orderDemo = new OrderStep();
        orderDemo.setOrderId(15103111065L);
        orderDemo.setDesc("付款");
        orderList.add(orderDemo);

        orderDemo = new OrderStep();
        orderDemo.setOrderId(15103117235L);
        orderDemo.setDesc("付款");
        orderList.add(orderDemo);

        orderDemo = new OrderStep();
        orderDemo.setOrderId(15103111065L);
        orderDemo.setDesc("完成");
        orderList.add(orderDemo);

        orderDemo = new OrderStep();
        orderDemo.setOrderId(15103111039L);
        orderDemo.setDesc("推送");
        orderList.add(orderDemo);

        orderDemo = new OrderStep();
        orderDemo.setOrderId(15103117235L);
        orderDemo.setDesc("完成");
        orderList.add(orderDemo);

        orderDemo = new OrderStep();
        orderDemo.setOrderId(15103111039L);
        orderDemo.setDesc("完成");
        orderList.add(orderDemo);

        return orderList;

}


    @Data
    public static class OrderStep {

        private Long orderId;

        private String desc;


    }
}

在这里插入图片描述

消息顺序消费:

public class OrderConsumerDemo {

    public static void main(String[] args) throws Exception{
        DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("test-consumer");
        consumer.setNamesrvAddr("192.168.18.142:9876");

        //订阅某个主题,然后使用tag过滤消息,代表消费Tag1、Tag2、Tag3
        consumer.subscribe("test-order-msg", "Tag1 || Tag2 || Tag3");

        //MessageListenerOrderly顺序消费
        consumer.registerMessageListener(new MessageListenerOrderly() {

            Random random = new Random();

            @Override
            public ConsumeOrderlyStatus consumeMessage(List<MessageExt> msgs, ConsumeOrderlyContext context) {
                context.setAutoCommit(true);
                for (MessageExt msg : msgs) {
                    // 可以看到每个queue有唯一的consume线程来消费, 订单对每个queue(分区)有序
                    System.out.println("consumeThread=" + Thread.currentThread().getName() + "queueId=" + msg.getQueueId() + ", content:" + new String(msg.getBody()));
                }

                try {
                    //模拟业务逻辑处理中...
                    TimeUnit.SECONDS.sleep(random.nextInt(10));
                } catch (Exception e) {
                    e.printStackTrace();
                }
                return ConsumeOrderlyStatus.SUCCESS;
            }
        });
        consumer.start();
    }
}

在这里插入图片描述
结果咋一看有点乱,但是按照orderId分类来看,消费的顺序就是有序的 创建、付款、完成、推送。因为同一orderId会进入同一queue中,所以在同一queue中的消息是有序的,我们要想发送消息就要控制生成消息时确保应当有序的消息进入同一队列,消费消息时不要使用并发消费。

还有从上面可以看出,消费同一队列的线程的线程id是一样的,代表同一队列时单线程消费的。保证消费的有序性。

延时消息的发送

定时消息(延迟队列)是指消息发送到broker后,不会立即被消费,等待特定时间投递给真正的topic。 broker有配置项messageDelayLevel,默认值为“1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h”,18个level。

messageDelayLevel是broker的属性,不属于某个topic。发消息时,设置delayLevel等级即可:msg.setDelayLevel(level)。

有三种情况:

  • level == 0,消息为非延迟消息
  • 1<=level<=maxLevel,消息延迟特定时间,例如level==1,延迟1s
  • level > maxLevel,则level== maxLevel,例如level==20,延迟2h

定时消息会暂存在名为SCHEDULE_TOPIC_XXXX的topic中,并根据delayTimeLevel存入特定的queue,queueId = delayTimeLevel – 1,即一个queue只存相同延迟的消息,保证具有相同发送延迟的消息能够顺序消费。broker会调度地消费SCHEDULE_TOPIC_XXXX,将消息写入真实的topic。

延时生产者:

public class DelayProducerDemo {
    public static void main(String[] args) throws Exception {
        //创建一个消息生产者,传入的是消息组名称
        DefaultMQProducer producer = new DefaultMQProducer("test-group");

        //输入nameserver服务的地址
        producer.setNamesrvAddr("192.168.18.142:9876");

        //启动生产者
        producer.start();

        for (int i = 0;i<10;i++){
            Message message = new Message("test-msg-delay",("test-msg-delay " + i + "===" + (new Date())).getBytes());
            //延迟10秒,等级3
            message.setDelayTimeLevel(3);
            producer.send(message);
            TimeUnit.SECONDS.sleep(1);
        }

    }
}

消费者普通消费者就可以:

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

        DefaultMQPushConsumer defaultMQPushConsumer = new DefaultMQPushConsumer("test-consumer");
        defaultMQPushConsumer.setNamesrvAddr("192.168.18.142:9876");

        //订阅某个主题,然后使用tag过滤消息,*代表不过滤
        defaultMQPushConsumer.subscribe("test-msg-delay","*");


        //注册回调实现类来处理从broker拉取回来的消息,MessageListenerConcurrently是并发消费
        defaultMQPushConsumer.registerMessageListener(new MessageListenerConcurrently() {
            @Override
            public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs, ConsumeConcurrentlyContext context) {
                for (MessageExt messageExt:msgs){
                    System.out.println(new Date() + "---" + new String(messageExt.getBody()));
                }
                return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
            }
        });
        defaultMQPushConsumer.start();
        System.out.println("消费者启动完成");
    }
}

可以看到左边的时间是消费时间,右边的时间是发送时间,除了第一个,其他都是相差10秒,因为延时了10秒,第一个延时13秒是因为第一次发送,发送时间只是创建消息时的时间,第一次发送需要初始化一些配置,有点延时,后面就稳定了。
在这里插入图片描述

现在RocketMq并不支持任意时间的延时,需要设置几个固定的延时等级,从1s到2h分别对应着等级1到18。
由下面变量指定:

//延时等级。
private String messageDelayLevel = "1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h";

消息消费失败会进入延时队列,失败消息的再次发送时间与设置的延时等级和重试次数有关。

批量消息发送

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

public class BatchProducerDemo {

    public static void main(String[] args) throws Exception{
        //创建一个消息生产者,传入的是消息组名称
        DefaultMQProducer producer = new DefaultMQProducer("test-group");

        //输入nameserver服务的地址
        producer.setNamesrvAddr("192.168.18.142:9876");

        //启动生产者
        producer.start();

        String topic = "test-batch-msg";

        Message message1 = new Message(topic,"test-batch-msg".getBytes());
        Message message2 = new Message(topic,"test-batch-msg".getBytes());
        Message message3 = new Message(topic,"test-batch-msg".getBytes());

        List<Message> messages = new ArrayList<>();
        messages.add(message1);
        messages.add(message2);
        messages.add(message3);

        //批量发送
        producer.send(messages);
    }
}

如果不确定批量的消息是否超过4M,可以写一个消息切割的,保证批量的消息不超过4M。

过滤消息

rocketmq可以使用tag来进行消息的过滤,这个在一些场景下十分有用。
比如上面的只订阅test-order-msg下的Tag1、Tag2 、Tag3消息。

 consumer.subscribe("test-order-msg", "Tag1 || Tag2 || Tag3");

但是限制是一个消息只能有一个标签,这对于复杂的场景可能不起作用。在这种情况下,可以使用SQL表达式筛选消息。SQL特性可以通过发送消息时的属性来进行计算。在RocketMQ定义的语法下,可以实现一些简单的逻辑。

首先服务器broker要配置支持属性过滤,默认是不支持的

enablePropertyFilter=true

比如一个消息包含以下属性:
a=10 b=‘china’ c=true

消费者使用sql a > 5 AND b = ‘abc’ 能够命中该消息。
如果使用a > 15 AND b = ‘abc’ 就会miss掉该消息。

基本语法:
RocketMQ只定义了一些基本语法来支持这个特性。

  • 数值比较,比如:>,>=,<,<=,BETWEEN,=;
  • 字符比较,比如:=,<>,IN;
  • IS NULL 或者 IS NOT NULL;
  • 逻辑符号 AND,OR,NOT;

常量支持类型:

  • 数值:比如1、520
  • 字符,比如:‘abc’,必须用单引号包裹起来;
  • NULL,特殊的常量
  • 布尔值,TRUE 或 FALSE

只有使用push模式的消费者才能用使用SQL92标准的sql语句,接口如下:

public void subscribe(finalString topic, final MessageSelector messageSelector)

例子:

public class FilterProducerDemo {
    public static void main(String[] args) throws Exception{
        //创建一个消息生产者,传入的是消息组名称
        DefaultMQProducer producer = new DefaultMQProducer("test-group");

        //输入nameserver服务的地址
        producer.setNamesrvAddr("192.168.18.142:9876");

        //启动生产者
        producer.start();

        Message message = new Message("test-filter-msg","test-filter-msg name=YeHaocong age=18".getBytes());

        //添加一些属性
        message.putUserProperty("age",String.valueOf(18));
        message.putUserProperty("name","YeHaocong");

        Message message1 = new Message("test-filter-msg","test-filter-msg name=YeHaocong age=5".getBytes());

        //添加一些属性
        message1.putUserProperty("age",String.valueOf(5));
        message1.putUserProperty("name","YeHaocong");
        producer.send(message);
        producer.send(message1);
    }
}
public class FilterConsumerDemo {

    public static void main(String[] args) throws Exception{
        DefaultMQPushConsumer defaultMQPushConsumer = new DefaultMQPushConsumer("test-filter");
        defaultMQPushConsumer.setNamesrvAddr("192.168.18.142:9876");

        //只消费test-filter-msg主题下 name=YeHaocong AND age>=18的消息
        defaultMQPushConsumer.subscribe("test-filter-msg", MessageSelector.bySql("name='YeHaocong' AND age>=18"));

        defaultMQPushConsumer.registerMessageListener(new MessageListenerConcurrently() {
            @Override
            public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs, ConsumeConcurrentlyContext context) {
                for (MessageExt messageExt:msgs){
                    System.out.println(new String(messageExt.getBody()));
                }
                return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
            }
        });
        defaultMQPushConsumer.start();
    }
}

要记得配置服务器支持属性过滤,不然会报以下错误。
在这里插入图片描述

最终结果:
在这里插入图片描述
生产者逻辑发送了两个消息 ,一个消息的属性age=5,一个等于18,然后消费者消费时使用过滤条件
name=‘YeHaocong’ AND age>=18,过滤掉了age=5的消息,所以只消费age=18的消息。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值