前言

上次分享介绍了Java整合RocketMQ-client的方式,并且简单的了解了生产者生产数据,消费者消费数据的整个流程,这次,我们详细的了解一下生产者和几种消息类别

一、生产者概述

发送消息的一方被称为生产者,我们先了解下几个基础概念

生产者组:一个逻辑概念,在使用生产者实例的时候需要指定一个组名。一个生产者组可以生产多个Topic的消息。

生产者实例:一个生产者组部署了多个进程,每个进程都可以称为一个生产者实例。

Topic:主题名字,一个Topic由若干Queue组成。

RocketMQ 客户端中的生产者有两个独立实现类:

  • org.apache.rocketmq.client.producer.DefaultMQProducer
  • org.apache.rocketmq.client.producer.TransactionMQProducer

前者用于生产普通消息、顺序消息、单向消息、批量消息、延迟消息,后者主要

用于生产事务消息

二、RocketMQ五种消息类型

1.普通消息

普通消息也称为并发消息,和传统的队列相比,并发消息没有顺序, 但是生产消费都是并行进行的,单机性能可达十万级别的TPS。

再上一章中介绍的方式就是普通消息,这里就不再重复示例,忘记的小伙伴可以移步复习一下

2.有序消息

与Kafka中的分区类似,把一个Topic消息分为多个分区“保存”和消费,在一个分区内的消息就是传统的队列,遵循FIFO(先进先出)原则。 简单的理解就是把需要按序发送的信息放在同一个队列中。

//创建DefaultMQProducer消息生产者对象
        DefaultMQProducer producer = new DefaultMQProducer("TestProducerGroup");
        //设置NameServer
        producer.setNamesrvAddr("192.168.2.5:9876");
        //设置NameServer节点地址,多个节点间用分号分割
        try {
            //与NameServer建立长连接
            producer.start();
            //设置个集合,集合信息中是要发送订单消息、支付消息、物流消息。要保证相同订单号的在同一个队列
            List<JSONObject> orderInfos = new ArrayList<>();
            JSONObject orderInfo1 = new JSONObject();
            orderInfo1.put("orderId",3347);
            orderInfo1.put("info","我是订单消息");
            orderInfos.add(orderInfo1);
            JSONObject orderInfo2 = new JSONObject();
            orderInfo2.put("orderId",3348);
            orderInfo2.put("info","我是订单消息");
            orderInfos.add(orderInfo2);
            JSONObject orderInfo3 = new JSONObject();
            orderInfo3.put("orderId",3347);
            orderInfo3.put("info","我是订单支付消息");
            orderInfos.add(orderInfo3);
            JSONObject orderInfo4 = new JSONObject();
            orderInfo4.put("orderId",3348);
            orderInfo4.put("info","我是订单支付消息");
            orderInfos.add(orderInfo4);
            JSONObject orderInfo5 = new JSONObject();
            orderInfo5.put("orderId",3347);
            orderInfo5.put("info","我是订单物流消息");
            orderInfos.add(orderInfo5);
            for (JSONObject orderInfo : orderInfos) {
                int orderId = orderInfo.getInteger("orderId");
                Message msg = new Message("TopicOrder", "orderinfo", orderId+"",
                        orderInfo.toJSONString().getBytes());
                SendResult sendResult = producer.send(msg, new MessageQueueSelector() {
                    @Override
                    public MessageQueue select(List<MessageQueue> mqs, Message msg, Object arg) {
                        //附加参数 producer.send方法中的第三个参数
                        int id = (Integer) arg;
                        System.out.println("*******************orderId:"+id+"***********************");

                        //和队列总数取余,来决定选择哪个队列,因为相同的订单ID取余的结果是一样的,这样就能保证相同的订单,我们选择的是同一个队列
                        int index = id % mqs.size();
                        MessageQueue messageQueue = mqs.get(index);
                        System.out.println("队列:"+messageQueue);
                        return messageQueue;
                    }
                }, orderId);
            }
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            try {
                producer.shutdown();
            } catch (Exception e) {
            }
        }

执行之后,我们可以看到以下打印消息:

rocketMQTemplate 消息监听_中间件

从信息中,我们不难看出,相同ID的信息,我们是通过同一个队列发送的。

3.延迟消息

消息发送后,消费者要在一定时间后,或者指定某个时间点才可以消费。在没有延迟消息时,基本的做法是基于定时计划任务调度,定时发送消息。在RocketMQ中只需要在发送消息时设置延迟级别即可实现。

Apache RocketMQ 一共支持18个等级的延迟投递,具体时间如下:

rocketMQTemplate 消息监听_中间件_02

由于只有固定的18个延迟级别,所以通常我们很少使用这种方式,大部分会通过定时任务去执行,而不是通过延迟消息,所以我们只需要了解一下,写法和普通消息基本一样

//创建DefaultMQProducer消息生产者对象
        DefaultMQProducer producer = new DefaultMQProducer("TestProducerGroup");
        //设置NameServer
        producer.setNamesrvAddr("192.168.2.5:9876");
        //设置NameServer节点地址,多个节点间用分号分割
        try {
            //与NameServer建立长连接
            producer.start();
            JSONObject json = new JSONObject();
            json.put("orderId", "8888");
            json.put("desc", "这是延迟队列测试");
            //数据正文
            String data = json.toJSONString();
            Message message = new Message("TopicOrder", "PAY_TAG", data.getBytes());
            //1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h
            message.setDelayTimeLevel(3);//代表延迟10S发送
            //发送消息,获取发送结果
            SendResult result = producer.send(message);
            //将发送结果对象打印在控制台
            System.out.println("发送时间:"+ DateFormatUtils.format(new Date(), "yyyy-MM-dd HH:mm:ss"));
            System.out.println("消息已发送:MsgId:" + result.getMsgId() + ",发送状态:"
                    + result.getSendStatus());
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            try {
                producer.shutdown();
            } catch (Exception e) {
            }
        }

通过上一节写的示例接受消息并打印时间,可以看到下面的信息

发送方:

rocketMQTemplate 消息监听_System_03

接收方:

rocketMQTemplate 消息监听_中间件_04

4.批量消息

在对吞吐率有一定要求的情况下,Apache RocketMQ可以将一些消息聚成一批以后进行发送,可以增加吞吐率,并减少API和网络调用次数。

//创建DefaultMQProducer消息生产者对象
        DefaultMQProducer producer = new DefaultMQProducer("TestProducerGroup");
        //设置NameServer
        producer.setNamesrvAddr("192.168.2.5:9876");
        //设置NameServer节点地址,多个节点间用分号分割
        try {
            //与NameServer建立长连接
            producer.start();
            String topic = "TopicOrder";
            List<Message> messageList = new ArrayList<>();
            for (int i = 1; i < 10; i++) {
                JSONObject json = new JSONObject();
                json.put("orderId", "8888");
                json.put("desc", "测试批量发送");
                //数据正文
                String data = json.toJSONString();
                Message message = new Message(topic, "PAY_TAG", data.getBytes());
                messageList.add(message);
            }
            //发送消息,获取发送结果
            SendResult result = producer.send(messageList);
            //将发送结果对象打印在控制台
            System.out.println("消息已发送:MsgId:" + result.getMsgId() + ",发送状态:"
                    + result.getSendStatus());
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            try {
                producer.shutdown();
            } catch (Exception e) {
            }
        }

注意:这里调用非常简单,将消息打包成 Collection<Message> msgs 传入方法中即可,需要注意的是批量消息的大小不能超过 1MiB(否则需要自行分割),其次同一批 batch 中 topic 必须相同。

5.事务消息(重要)

主要涉及分布式事务,即需要保证在多个操作同时成功或者同时失败时,消费者才能消费消息。RocketMQ通过发送Half消息、处理本地事务、提交(Commit)消息或者回滚(Rollback)消息实现分布式事务。

rocketMQTemplate 消息监听_后端_05

事务消息发送分为两个阶段。

第一阶段会发送一个半事务消息,半事务消息是指暂不能投递的消息,生产者已经成功地将消息发送到了 Broker,但是Broker 未收到生产者对该消息的二次确认,此时该消息被标记成“暂不能投递”状态,如果发送成功则执行本地事务,并根据本地事务执行成功与否,向 Broker 半事务消息状态(commit或者rollback),半事务消息只有 commit 状态才会真正向下游投递。

如果由于网络闪断、生产者应用重启等原因,导致某条事务消息的二次确认丢失,Broker 端会通过扫描发现某条消息长期处于“半事务消息”时,需要主动向消息生产者询问该消息的最终状态(Commit或是Rollback)。这样最终保证了本地事务执行成功,下游就能收到消息,本地事务执行失败,下游就收不到消息。总而保证了上下游数据的一致性。

整个事务消息的详细交互流程如下图所示:

rocketMQTemplate 消息监听_rocketmq_06

例子

发送两个订单信息,一个订单号是6666,一个订单号是8888

6666订单用于测试信息二次确认返回失败,mq信息不会被消费

8888用于测试二次确认失败,broker回调查询成功,mq信息被消费的情况

实现方法

1.新增监听器实现TransactionListener接口,重写方法,一个用来发送半事务二次确认消息,一个用来处理回调查询事务状态

import org.apache.rocketmq.client.producer.LocalTransactionState;
import org.apache.rocketmq.client.producer.TransactionListener;
import org.apache.rocketmq.common.message.Message;
import org.apache.rocketmq.common.message.MessageExt;

public class OrderTransactionListenerImpl implements TransactionListener {
    @Override
    public LocalTransactionState executeLocalTransaction(Message message, Object o) {
        String key = message.getKeys();
        if("6666".equals(key)){
            System.out.println("接收到orderId:"+key +"的信息");
            System.out.println("模拟本地执行方法失败的情况");
            //执行本地订单插入方法报错,执行回滚
            return LocalTransactionState.ROLLBACK_MESSAGE;
        }else if ("8888".equals(key)){
            System.out.println("接收到orderId:"+key +"的信息");
            System.out.println("模拟本地方法成功,但是未成功发送确认方法");
            //执行本地方法成功,发送MQ提交确认信息
            return LocalTransactionState.UNKNOW;
        }
        return null;
    }

    @Override
    public LocalTransactionState checkLocalTransaction(MessageExt messageExt) {
        String key = messageExt.getKeys();
        if("6666".equals(key)){
            System.out.println("这是回调方法,订单号是:"+key);
            return LocalTransactionState.ROLLBACK_MESSAGE;
        }else if ("8888".equals(key)){
            System.out.println("这是回调方法,订单号是:"+key);
            System.out.println("订单号:"+key+"的回查成功");
            //执行本地方法成功,发送MQ提交确认信息
            return LocalTransactionState.COMMIT_MESSAGE;
        }
        return null;
    }
}

2.编写producer

//使用TransactionMQProducer事务生产者创建
        TransactionMQProducer producer = new
                TransactionMQProducer("transaction_producer_group");
        //从NameServer获取配置数据
        producer.setNamesrvAddr("192.168.2.5:9876");
        //定义线程池用于回查本地事务状态
        ExecutorService executorService = new ThreadPoolExecutor(2,
                5,
                100,
                TimeUnit.SECONDS,
                new ArrayBlockingQueue<Runnable>(2000), new ThreadFactory() {
            @Override
            public Thread newThread(Runnable r) {
                Thread thread = new Thread(r);
                thread.setName("check-thread");
                return thread;
            }
        });
        //将生产者与线程池绑定
        producer.setExecutorService(executorService);
        //绑定事务监听器,用于执行代码
        TransactionListener transactionListener = new OrderTransactionListenerImpl();
        producer.setTransactionListener(transactionListener);
        //启动生产者
        try {
            producer.start();
            //定义两条记录模拟事务回滚
            List<JSONObject> orderInfos = new ArrayList<>();
            // 6666的订单模拟发送MQ消息成功,但是本地方法(比如订单插入数据库失败)失败需要回滚的情况
            JSONObject orderInfo1 = new JSONObject();
            orderInfo1.put("orderId", 6666);
            orderInfo1.put("info", "发送MQ消息成功,本地方法失败");
            orderInfos.add(orderInfo1);
            // 8888的订单用来模拟,MQ发送成功,本地方法执行成功,
            // 但是通知MQ确认消息的时候失败,然后又通过MQ回查本地事务又成功的情况
            JSONObject orderInfo2 = new JSONObject();
            orderInfo2.put("orderId", 8888);
            orderInfo2.put("info", "MQ发送成功,本地方法执行成功,再次发送MQ提交信息失败,MQ回查本地事务情况又成功");
            orderInfos.add(orderInfo2);
            for (JSONObject orderInfo : orderInfos) {
                Message message = new Message("TopicOrder", "PAY_TAG",orderInfo.getString("orderId"), orderInfo.toJSONString().getBytes());
                TransactionSendResult result = producer.sendMessageInTransaction(message, null);
                System.out.println("订单号:"+orderInfo.getString("orderId")+"的第一次MQ信息发送状态:"+result.getSendStatus());
            }
            //用于等待broker的回调
            for (int i = 1; i < 100000; i++) {
                Thread.sleep(1000);
                System.out.println(i+"秒");
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            try {
                producer.shutdown();
            } catch (Exception e) {
            }
        }

然后我们看一下控制台信息:

生产者:

rocketMQTemplate 消息监听_中间件_07

消费者:

rocketMQTemplate 消息监听_中间件_08

可以看出,订单6666因为二次确认失败,信息无法被消费,而8888虽然二次确认失败,但是通过回调查询还是成功的。所以8888成功被消费。

三、总结

普通消息:性能最好,但是消息的生产和消费是无序的。适用于大部分场景

有序消息:容易出现单点问题,如果broker宕机,会导致发送失败,有序消息场景适用

延迟消息:支持延迟特性,使用非常方便,但是灵活性较低,不能根据任何时间延迟,适用于非精确、延迟级别不多的场景

事务消息:RocketMQ是生产者事务,只有生产者参与,如果消费者处理失败则事务失败,适用于简单事务处理的场景。