关于RocketMQ的一些基础知识点及使用建议

之前一段时间团队中引入了RocketMQ以作为RabbitMQ的替代品, 一者是让使用它的较高吞吐量,其次想在其上做一些扩展开发,消息事务也是我们想用它的原因之一;

MQ相关介绍

消息队列是一种可以实现系统异步通信的中间件,常用于解决系统异步解耦和请求(TPS)削峰填谷的问题。即它面向的是开发人员,而非终端用户可以直接使用的产品;

实现一个MQ产品所常用的三种消息协议

  1. JMS (Java Message Service)
    JMS 本质上是 JAVA API。在 JMS 中定义了 Producer,Consumer,Provider 三种角色,Producer 作为消息的发送方,Consumer 作为消息的接收方,Provider 作为服务的提供者,Producer 和 Consumer 统称为 Client。JMS 定义了点对点和发布订阅两种消息模型,发布订阅模型中,通过 topic 对消息进行路由,生产者可以将消息发到指定的 topic,消费者订阅这个 topic 即可收到生产者发送的消息。
    一个生产者可以向一个或多个 topic 中发送消息,一个消费者也可以消费一个或多个 topic 中的消息,一个 topic 也可以有多个生产者或消费者,生产者和消费者只需要关联 topic,而不用关心这消息由谁发送或者消费。 Provider 为每一个 topic 维护一个或多个 queue 来保存消息,消息在 queue 中是有序的,遵循先进先出的原则,不同 queue 间的消息是无序的。点对点模式中没有 topic 的概念,生产者直接将消息发送到指定 queue,消费者也指定 queue 进行消费,消息只能被一个消费者消费,不可以被多个消费者消费。Kafka 和 RocketMQ 都实现了或部分实现了 JMS 协议。

  2. AMQP(Advanced Message Queuing Protocol)
    与 JMS 不同,AMQP 是一个应用层的网络传输协议,对报文格式进行定义,与开发语言无关。在 AMQP 中同样有生产者,消费者两种角色,消息也是保存在 queue 中的。 但不同于 JMS 用 topic 对消息进行路由,AMQP 的路由方式由 exchange 和 binding 决定。
    client 可以创建 queue,并在创建 queue 的同时通知 exchange 这个 queue 接受符合什么条件的消息,这个条件即为 Bingding key。生产者发送消息到 exchange 的时候会指定一个 router key,exchange 收到消息后会与自己所维护的 Bingding key 做比较,发送到符合条件的 queue 中。消费者在消费时指定 queue 进行消费。RabbitMQ 实现了 AMQP 协议。

  3. MQTT(Message Queuing Telemetry Transport)
    MQTT 协议是一种基于发布订阅的轻量级协议,支持 TCP 和 UDP 两种连接方式,主要应用于即时通讯,小型设备,移动应用等领域。 MQTT 中有发布者(Publish),订阅者(Subscribe)和代理服务器(Broker)三种角色。Broker 是服务的提供者,发布者和前两种协议中的生产者相同,将消息(Message)发送到 Broker,Subscribe 从 Broker 中获取消息并做业务处理。
    MQTT 的 Message 中固定消息头(Fixed header)仅有 2 字节,开销极小,除此之外分为可变头(Variable header)和消息体(payload)两部分。固定头中包含消息类型,消息级别,变长头的大小以及消息体的总长度等信息。 变长头则根据消息类别,含有不同的标识信息。 MQTT 允许客户端动态的创建主题,发布者与服务端建立会话(session)后,可以通过 Publish 方法发送数据到服务端的对应主题,订阅者通过 Subscribe 订阅主题后,服务端就会将主题中的消息推送给对应的订阅者。

RocketMQ架构设计

RocketMQ简介

Apache RocketMQ是一个分布式消息及流处理组件,具有低延迟,高性能,万亿级容量及灵活扩展等特性;

系统模块划分

这里借用RocketMQ官网的一张图
RocketMQ architecture

  • NameServer Cluster
    名称服务器提供轻量级服务发现和路由。 每个名称服务器记录完整的路由信息,提供相应的读写服务,并支持快速存储扩展。
  • Broker Cluster
    中转者通过提供轻量级的TOPIC和QUEUE机制来处理消息存储。它们支持Push和Pull模型,包含容错机制(2个副本或3个副本),并提供强大的峰值填充和按原始时间顺序累积数千亿条消息的能力。此外,Brokers还提供灾难恢复,丰富的指标统计和警报机制,所有这些都是传统的消息中间件所欠缺的。
  • Producer Cluster
    生产者支持分布式部署。 Distributed Producers通过多种负载均衡模式向Broker集群发送消息。 发送过程支持失败快速重试并具有低延迟。
  • Consumer Cluster
    消费者也支持Push和Pull模型中的分布式部署。 它还支持群集消费和消息广播。 它提供实时消息订阅机制,可以满足大多数消费者的需求。 RocketMQ的网站为感兴趣的用户提供了一个简单的快速入门指南。

RocketMQ相关使用

单机环境

一个NameServer, 一个Broker, 一个Producer, 一个或多个Consumer

启动一个生产者

    @Bean(name = "defaultMQProducer")
    public DefaultMQProducer buildDefaultMQProducer() throws MQClientException {
        //Instantiate with a producer group name.
        DefaultMQProducer producer = new DefaultMQProducer(env.getProperty("app.mq.default-producer-name"));
        // Specify name server addresses.
        producer.setNamesrvAddr(env.getProperty("app.mq.url"));
        //Launch the instance.
        producer.start();
        return producer;
    }

普通消息发送

  1. 生产消息
    /**
     * 发送一个普通消息
     * @param messageInfo 消息内容
     * @return
     */
    public Optional<SendResult> sendNormalEventMessage(EventMessageInfo messageInfo) {
        if (LOGGER.isInfoEnabled()) {
            LOGGER.info("创建MQ消息: {}", JSON.toJSONString(messageInfo));
        }
        Optional<SendResult> result = Optional.empty();
        try {
            validateMessageInfo(messageInfo);
            String topic = StringUtils.isNoneBlank(messageInfo.getTopic()) ? messageInfo.getTopic() : DEFAULT_TOPIC;
            String tags = StringUtils.isNoneBlank(messageInfo.getTags()) ? messageInfo.getTags() : DEFAULT_TAGS;
            String message = JSON.toJSONString(messageInfo.getData());
            Message msg = new Message(topic, tags, message.getBytes(RemotingHelper.DEFAULT_CHARSET));
            //Call send message to deliver message to one of brokers.
            SendResult sendResult = mqProducer.send(msg);
            result = Optional.of(sendResult);
        } catch (IllegalArgumentException e) {
            LOGGER.warn("sendNormalEventMessage IllegalArgumentException", e);
        } catch (UnsupportedEncodingException | MQClientException | RemotingException | MQBrokerException | InterruptedException e) {
            LOGGER.warn("sendNormalEventMessage exception", e);
        }
        LOGGER.info("发送消息, 结果: {}", JSON.toJSONString(result));
        return result;
    }
  1. 消费消息

    /**
     * 创建一个普通消息消费者
     * @return
     * @throws MQClientException
     */
    @Bean(name = "normalMessageConsumer")
    public DefaultMQPushConsumer buildUserMQPullConsumer() throws MQClientException {
        //Instantiate with a producer group name.
        DefaultMQPushConsumer consumer = new DefaultMQPushConsumer(env.getProperty("app.mq.default-consumer-name"));
        // Specify name server addresses.
        consumer.setNamesrvAddr(env.getProperty("app.mq.url"));
        // Subscribe one more more topics to consume.
        consumer.subscribe(env.getProperty("app.message.user.topic"), "*");
        // Register callback to execute on arrival of messages fetched from brokers.
        consumer.registerMessageListener((MessageListenerConcurrently) (msgs, context) -> {
            msgs.stream().forEach(message -> {
                try {
                    MessageQueue messageQueue = context.getMessageQueue();
                    logger.info("From brokerName [{}], QueueId [{}], Topic[{}], Tags[{}], Keys[{}], Receive New Message: {}", messageQueue.getBrokerName(), messageQueue.getQueueId(),
                        message.getTopic(), message.getTags(), message.getKeys(), new String(message.getBody(), RemotingHelper.DEFAULT_CHARSET));
                } catch (UnsupportedEncodingException e) {
                    logger.warn("exception", e);
                }
            });
            return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
        });
        //Launch the consumer instance.
        consumer.start();
        return consumer;
    }

思考:

  1. 一个生产者指定一个topic投放消息, 会在broker端生成多个queue, 消息会被轮询投放到这些队列中, 然后消费者订阅消息, 当队列数多余消费者数时, 一个消费者会从多个队列消费消息;
  2. 原则是一个queue不能被多个消费者消费。那如果不断有新的消费者上线, queue的个数是否会自动变大, 保持其数目始终大于消费者数呢? 如果queue的个数不变, 那个数超过queue的消费者就没有可消费的queue了吗?

发送一组有序消息

基本原理, 是一个queue中的消息是被顺序消费的, 所以只要我们将一组有序消息都投放到一个queue中就可以保证这组消息被消息时也是有序的!

  1. 生产消息
    /**
     * 发送一组顺序消息
     * @param messageInfo  消息内容
     * @return
     */
    public Optional<SendResult> sendOrderEventMessage(EventMessageInfo messageInfo) {
        if (LOGGER.isInfoEnabled()) {
            LOGGER.info("创建MQ消息: {}", JSON.toJSONString(messageInfo));
        }
        Optional<SendResult> result = Optional.empty();
        try {
            validateMessageInfo(messageInfo);
            String topic = StringUtils.isNoneBlank(messageInfo.getTopic()) ? messageInfo.getTopic() : DEFAULT_TOPIC;
            String tags = StringUtils.isNoneBlank(messageInfo.getTags()) ? messageInfo.getTags() : DEFAULT_TAGS;
            String[] tagsList = tags.split(",");
            String messageStr = JSON.toJSONString(messageInfo.getData());
            SendResult sendResult = null;
            for (int i = 0; i < BATCHSIZE; i++) {
                int orderId = i % 10;
                //Create a message instance, specifying topic, tag and message body.
                Message message = new Message(topic, tagsList[i % tagsList.length], "KEY" + i,
                    (i + messageStr).getBytes(RemotingHelper.DEFAULT_CHARSET));
                sendResult = mqProducer.send(message, (mqs, msg, arg) -> {
                    Integer id = (Integer) arg;
                    int index = id % mqs.size();
                    return mqs.get(index);
                }, orderId);
            }
            result = Optional.of(sendResult);
        } catch (IllegalArgumentException e) {
            LOGGER.warn("sendNormalEventMessage IllegalArgumentException", e);
        } catch (UnsupportedEncodingException | MQClientException | RemotingException | MQBrokerException | InterruptedException e) {
            LOGGER.warn("sendNormalEventMessage exception", e);
        }
        LOGGER.info("发送消息, 结果: {}", JSON.toJSONString(result));
        return result;
    }
  1. 消费消息
    /**
     * 创建一个消费有序消息消费者
     * @return
     * @throws MQClientException
     */
    @Bean(name = "orderMessageConsumer")
    public DefaultMQPushConsumer buildOrderMQPushConsumer() throws MQClientException {
        //Instantiate with a producer group name.
        DefaultMQPushConsumer consumer = new DefaultMQPushConsumer(env.getProperty("app.mq.default-consumer-name"));
        // Specify name server addresses.
        consumer.setNamesrvAddr(env.getProperty("app.mq.url"));
        // Subscribe one more more topics to consume.
        consumer.subscribe(env.getProperty("app.message.user.topic"), "A || C || E");

        consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_FIRST_OFFSET);
        // Register callback to execute on arrival of messages fetched from brokers.
        consumer.registerMessageListener(new MessageListenerOrderly() {

            private AtomicLong consumeTimes = new AtomicLong(0);

            @Override
            public ConsumeOrderlyStatus consumeMessage(List<MessageExt> msgs, ConsumeOrderlyContext context) {
                context.setAutoCommit(false);
                MessageExt message = msgs.get(0);
                MessageQueue messageQueue = context.getMessageQueue();
                try {
                    logger.info("From brokerName [{}], QueueId [{}], Topic[{}], Tags[{}], Keys[{}], Receive New Message: {}", messageQueue.getBrokerName(), messageQueue.getQueueId(),
                        message.getTopic(), message.getTags(), message.getKeys(), new String(message.getBody(), RemotingHelper.DEFAULT_CHARSET));
                } catch (UnsupportedEncodingException e) {
                    e.printStackTrace();
                }
                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;

            }
        });
        //Launch the consumer instance.
        consumer.start();
        return consumer;
    }

思考:

  1. 生成环境消费者的上线与下线会进行queue与消费者的重新分配绑定, 那样也只是说一组有序的消息被从中间隔断了, 然后这一组消息的前一部分在老的消费者之内被消费保持内部有序, 后续一部分在一个新消费者内部依然有序, 然而这前者和后者的关系是, 前者已经下线, 后者才进行的消息消费, 在客观时间轴上依然前后有序;

发送一条广播消息

要实现广播消息, 在生产者端没有什么区别, 依然是发送一条普通的消息即可; 重点在于消费者端, 在默认消费者实现类[DefaultMQPullConsumer, DefaultMQPushConsumer]里面有个属性[messageModel], 表示消息是如何被消费者消费;

messageModel 作用
MessageModel.CLUSTERING 消息会被整个consumerGroup中的所有订阅者分享消费, 一个消息只被某一个消费者消费一次
MessageModel.BROADCASTING 消息会被整个consumerGroup中的所有订阅者分别消费, 一个消息会被所有订阅的消费者消费一次
  1. 生产消息

    /**
     * 发送一条广播消息
     * @param messageInfo  消息内容
     * @return
     */
    public Optional<SendResult> sendBroadcastingEventMessage(EventMessageInfo messageInfo) {
        if (LOGGER.isInfoEnabled()) {
            LOGGER.info("创建MQ消息: {}", JSON.toJSONString(messageInfo));
        }
        Optional<SendResult> result = Optional.empty();
        try {
            validateMessageInfo(messageInfo);
            String topic = StringUtils.isNoneBlank(messageInfo.getTopic()) ? messageInfo.getTopic() : DEFAULT_TOPIC;
            String tags = StringUtils.isNoneBlank(messageInfo.getTags()) ? messageInfo.getTags() : DEFAULT_TAGS;
            String messageStr = JSON.toJSONString(messageInfo.getData());
            SendResult sendResult = null;
            for (int i = 0; i < BATCHSIZE; i++) {
                //Create a message instance, specifying topic, tag and message body.
                Message message = new Message(topic, tags, (i + messageStr).getBytes(RemotingHelper.DEFAULT_CHARSET));
                sendResult = mqProducer.send(message);
            }
            result = Optional.of(sendResult);
        } catch (IllegalArgumentException e) {
            LOGGER.warn("sendNormalEventMessage IllegalArgumentException", e);
        } catch (UnsupportedEncodingException | MQClientException | RemotingException | MQBrokerException | InterruptedException e) {
            LOGGER.warn("sendNormalEventMessage exception", e);
        }
        LOGGER.info("发送消息, 结果: {}", JSON.toJSONString(result));
        return result;
    }

  1. 消费消息
    /**
     * 创建一个消费广播消息消费者
     * @return
     * @throws MQClientException
     */
    @Bean(name = "broadcastMessageConsumer")
    public DefaultMQPushConsumer buildBroadcastMQPushConsumer() throws MQClientException {
        //Instantiate with a producer group name.
        DefaultMQPushConsumer consumer = new DefaultMQPushConsumer(env.getProperty("app.mq.default-consumer-name"));
        // Specify name server addresses.
        consumer.setNamesrvAddr(env.getProperty("app.mq.url"));
        // Subscribe one more more topics to consume.
        consumer.subscribe(env.getProperty("app.message.user.topic"), "A || C || E");

        consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_FIRST_OFFSET);
        //set to broadcast mode
        consumer.setMessageModel(MessageModel.BROADCASTING);

        // Register callback to execute on arrival of messages fetched from brokers.
        consumer.registerMessageListener((MessageListenerConcurrently) (msgs, context) -> {
            msgs.stream().forEach(message -> {
                try {
                    MessageQueue messageQueue = context.getMessageQueue();
                    logger.info("From brokerName [{}], QueueId [{}], Topic[{}], Tags[{}], Keys[{}], Receive New Message: {}", messageQueue.getBrokerName(), messageQueue.getQueueId(),
                        message.getTopic(), message.getTags(), message.getKeys(), new String(message.getBody(), RemotingHelper.DEFAULT_CHARSET));
                } catch (UnsupportedEncodingException e) {
                    logger.warn("exception", e);
                }
            });
            return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
        });
        //Launch the consumer instance.
        consumer.start();
        return consumer;
    }

发送一条定时延迟消息

生产者在发送消息时, 指定一个定时延迟消费参数(不同级别代表不同的延迟时段)

DelayTimeLevel 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
延迟时段 不延迟 1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 20h
  1. 生产消息

    /**
     * 发送一条定时消息
     * @param messageInfo  消息内容
     * @return
     */
    public Optional<SendResult> sendScheduledEventMessage(EventMessageInfo messageInfo) {
        if (LOGGER.isInfoEnabled()) {
            LOGGER.info("创建MQ消息: {}", JSON.toJSONString(messageInfo));
        }
        Optional<SendResult> result = Optional.empty();
        try {
            validateMessageInfo(messageInfo);
            String topic = StringUtils.isNoneBlank(messageInfo.getTopic()) ? messageInfo.getTopic() : DEFAULT_TOPIC;
            String tags = StringUtils.isNoneBlank(messageInfo.getTags()) ? messageInfo.getTags() : DEFAULT_TAGS;
            String messageStr = JSON.toJSONString(messageInfo.getData());
            SendResult sendResult = null;
            for (int i = 0; i < BATCHSIZE; i++) {
                //Create a message instance, specifying topic, tag and message body.
                Message message = new Message(topic, tags, (i + messageStr).getBytes(RemotingHelper.DEFAULT_CHARSET));
                // This message will be delivered to consumer 1 minutes later.
                message.setDelayTimeLevel(5);
                sendResult = mqProducer.send(message);
            }
            result = Optional.of(sendResult);
        } catch (IllegalArgumentException e) {
            LOGGER.warn("sendScheduledEventMessage IllegalArgumentException", e);
        } catch (UnsupportedEncodingException | MQClientException | RemotingException | MQBrokerException | InterruptedException e) {
            LOGGER.warn("sendScheduledEventMessage exception", e);
        }
        LOGGER.info("发送消息, 结果: {}", JSON.toJSONString(result));
        return result;
    }

  1. 消费消息

    /**
     * 创建一个消费定时消息消费者
     * @return
     * @throws MQClientException
     */
    @Bean(name = "scheduledMessageConsumer")
    public DefaultMQPushConsumer buildScheduledMQPushConsumer() throws MQClientException {
        //Instantiate with a producer group name.
        DefaultMQPushConsumer consumer = new DefaultMQPushConsumer(env.getProperty("app.mq.default-consumer-name"));
        // Specify name server addresses.
        consumer.setNamesrvAddr(env.getProperty("app.mq.url"));
        // Subscribe one more more topics to consume.
        consumer.subscribe(env.getProperty("app.message.user.topic"), "*");
        // Register callback to execute on arrival of messages fetched from brokers.
        consumer.registerMessageListener((MessageListenerConcurrently) (msgs, context) -> {
            msgs.stream().forEach(message -> {
                try {
                    MessageQueue messageQueue = context.getMessageQueue();
                    logger.info("From brokerName [{}], QueueId [{}], Topic[{}], Tags[{}], Keys[{}], Receive New Message: {}", messageQueue.getBrokerName(), messageQueue.getQueueId(),
                        message.getTopic(), message.getTags(), message.getKeys(), new String(message.getBody(), RemotingHelper.DEFAULT_CHARSET));
                } catch (UnsupportedEncodingException e) {
                    logger.warn("exception", e);
                }
            });
            return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
        });
        //Launch the consumer instance.
        consumer.start();
        return consumer;
    }

发送一批消息

将消息体积较小的小消息打包批量一次发送, 可以提高系统性能, 条件是这些消息需要具有相同的topic, tag, 同时落地存储, 并且不支持定时功能, 最后就是整个消息打包后的大小不超过1M;

  1. 生产消息
    /**
     * 发送一批打包消息
     * @param messageInfo  消息内容
     * @return
     */
    public Optional<SendResult> sendBatchEventMessage(EventMessageInfo messageInfo) {
        if (LOGGER.isInfoEnabled()) {
            LOGGER.info("创建MQ消息: {}", JSON.toJSONString(messageInfo));
        }
        Optional<SendResult> result = Optional.empty();
        try {
            validateMessageInfo(messageInfo);
            String topic = StringUtils.isNoneBlank(messageInfo.getTopic()) ? messageInfo.getTopic() : DEFAULT_TOPIC;
            String tags = StringUtils.isNoneBlank(messageInfo.getTags()) ? messageInfo.getTags() : DEFAULT_TAGS;
            String messageStr = JSON.toJSONString(messageInfo.getData());
            List<Message> messages = new ArrayList<>();
            for (int i = 0; i < BATCHSIZE; i++) {
                //Create a message instance, specifying topic, tag and message body.
                messages.add(new Message(topic, tags, i + "key", (i + messageStr).getBytes(RemotingHelper.DEFAULT_CHARSET)));
            }
            SendResult sendResult = mqProducer.send(messages);
            result = Optional.of(sendResult);
        } catch (IllegalArgumentException e) {
            LOGGER.warn("sendBatchEventMessage IllegalArgumentException", e);
        } catch (UnsupportedEncodingException | MQClientException | RemotingException | MQBrokerException | InterruptedException e) {
            LOGGER.warn("sendBatchEventMessage exception", e);
        }
        LOGGER.info("发送消息, 结果: {}", JSON.toJSONString(result));
        return result;
    }

  1. 消费消息

    /**
     * 创建一个批量消息消费者
     * @return
     * @throws MQClientException
     */
    @Bean(name = "batchMessageConsumer")
    public DefaultMQPushConsumer buildBatchMQPushConsumer() throws MQClientException {
        //Instantiate with a producer group name.
        DefaultMQPushConsumer consumer = new DefaultMQPushConsumer(env.getProperty("app.mq.default-consumer-name"));
        // Specify name server addresses.
        consumer.setNamesrvAddr(env.getProperty("app.mq.url"));
        // Subscribe one more more topics to consume.
        consumer.subscribe(env.getProperty("app.message.user.topic"), "*");
        // Register callback to execute on arrival of messages fetched from brokers.
        consumer.registerMessageListener((MessageListenerConcurrently) (msgs, context) -> {
            msgs.stream().forEach(message -> {
                try {
                    MessageQueue messageQueue = context.getMessageQueue();
                    logger.info("From brokerName [{}], QueueId [{}], Topic[{}], Tags[{}], Keys[{}], Receive New Message: {}", messageQueue.getBrokerName(), messageQueue.getQueueId(),
                        message.getTopic(), message.getTags(), message.getKeys(), new String(message.getBody(), RemotingHelper.DEFAULT_CHARSET));
                } catch (UnsupportedEncodingException e) {
                    logger.warn("exception", e);
                }
            });
            return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
        });
        //Launch the consumer instance.
        consumer.start();
        return consumer;
    }

思考: 千万别以为一批消息打包了, 到消费者端就是在一个线程中被按打包的顺序依次消费处理, 虽然打包的消息肯定是被发送到一个消费者上的,但是包会被拆解, 交由消费者中多个线程并发处理, 所以别想通过打包实现消息的顺序消费, 哈哈

发送事务消息

RocketMQ支持事物消息: 在分布式环境下通过两阶段提交实现最终一致性; 事务性消息可以确保以原子方式执行本地事务并进行消息的发送;

事务消息的使用约束

  1. 不支持定时和批量发送;
  2. 为了避免一个消息被检查多次而导致其他部分消息累积得不到处理, 我们默认限制一个消息最多被检查15次, 但是用户可以通过参数 “transactionCheckMax” 修改此默认值; 如果一条消息被检查超过最大检查数, 那broker就会丢弃此消息并默认同时输出错误日志, 用户也可以通过 override “AbstractTransactionCheckListener” class 改变此默认行为;
  3. Broker中配置的参数“transactionTimeout”可以决定一个事务消息会被检查多久, 然后再发送消息时用户可以通过设置user property “CHECK_IMMUNITY_TIME_IN_SECONDS”改变此全局配置值;
  4. 一个事务消息可以被检查和消费多次;
  5. 事务消息在做Committed时可能会投放到用户目标topic失败, 高可用是由RocketMQ自身的机制保证的; 如果你想确保事务消息不丢失并且保证完整性, 那就需要使用双重同步刷盘;
  6. 消息事务的生产者ID不能和其他类型消息的生产者ID共享, 因为事务消息允许逆向查询;

代码实现

  1. 创建一个事务消息状态检查器, 构建一个事务消息生产者

/**
 * @author Hinsteny
 * @version TransactionListenerImpl: TransactionListenerImpl 2019-05-16 16:44 All rights reserved.$
 */
public class TransactionListenerImpl implements TransactionListener {

    private final Logger logger = LogManager.getLogger(this.getClass());

    private AtomicInteger transactionIndex = new AtomicInteger(0);

    private ConcurrentHashMap<String, Integer> localTrans = new ConcurrentHashMap<>();

    @Override
    public LocalTransactionState executeLocalTransaction(Message msg, Object arg) {
        int value = transactionIndex.getAndIncrement();
        int status = value % 3;
        localTrans.put(msg.getTransactionId(), status);
        return LocalTransactionState.UNKNOW;
    }

    @Override
    public LocalTransactionState checkLocalTransaction(MessageExt msg) {
        Integer status = localTrans.get(msg.getTransactionId());
        if (null != status) {
            switch (status) {
                case 0:
                    return LocalTransactionState.UNKNOW;
                case 1:{
                    logger.info("COMMIT_MESSAGE message: {}", msg.getMsgId());
                    return LocalTransactionState.COMMIT_MESSAGE;
                }
                case 2:
                    return LocalTransactionState.ROLLBACK_MESSAGE;
            }
        }
        logger.info("COMMIT_MESSAGE message: {}", msg.getMsgId());
        return LocalTransactionState.COMMIT_MESSAGE;
    }

}

    @Bean(name = "transactionMQProducer")
    public DefaultMQProducer buildTransactionMQProducer() throws MQClientException {
        //Instantiate with a producer group name.
        TransactionMQProducer  producer = new TransactionMQProducer(env.getProperty("app.mq.default-producer-name"));
        // Specify name server addresses.
        producer.setNamesrvAddr(env.getProperty("app.mq.url"));
        TransactionListener transactionListener = new TransactionListenerImpl();
        ExecutorService executorService = new ThreadPoolExecutor(2, 5, 100, TimeUnit.SECONDS, new ArrayBlockingQueue<>(2000), r -> {
            Thread thread = new Thread(r);
            thread.setName("client-transaction-msg-check-thread");
            return thread;
        });
        producer.setExecutorService(executorService);
        producer.setTransactionListener(transactionListener);
        //Launch the instance.
        producer.start();
        return producer;
    }

  1. 发送事务消息


    /**
     * 发送事务消息
     * @param messageInfo  消息内容
     * @return
     */
    public Optional<SendResult> sendTransactionEventMessage(EventMessageInfo messageInfo) {
        if (LOGGER.isInfoEnabled()) {
            LOGGER.info("创建MQ消息: {}", JSON.toJSONString(messageInfo));
        }
        Optional<SendResult> result = Optional.empty();
        try {
            validateMessageInfo(messageInfo);
            String topic = StringUtils.isNoneBlank(messageInfo.getTopic()) ? messageInfo.getTopic() : DEFAULT_TOPIC;
            String tags = StringUtils.isNoneBlank(messageInfo.getTags()) ? messageInfo.getTags() : DEFAULT_TAGS;
            String messageStr = JSON.toJSONString(messageInfo.getData());
            Message messages = new Message(topic, tags, messageStr.getBytes(RemotingHelper.DEFAULT_CHARSET));
            SendResult sendResult = mqProducer.sendMessageInTransaction(messages, null);
            result = Optional.of(sendResult);
        } catch (IllegalArgumentException e) {
            LOGGER.warn("sendTransactionEventMessage IllegalArgumentException", e);
        } catch (UnsupportedEncodingException | MQClientException e) {
            LOGGER.warn("sendTransactionEventMessage exception", e);
        }
        LOGGER.info("发送消息, 结果: {}", JSON.toJSONString(result));
        return result;
    }

  1. 消费消息

    /**
     * 创建一个事务消息消费者
     * @return
     * @throws MQClientException
     */
    @Bean(name = "transactionMessageConsumer")
    public DefaultMQPushConsumer buildTransactionMQPushConsumer() throws MQClientException {
        //Instantiate with a producer group name.
        DefaultMQPushConsumer consumer = new DefaultMQPushConsumer(env.getProperty("app.mq.default-consumer-name"));
        // Specify name server addresses.
        consumer.setNamesrvAddr(env.getProperty("app.mq.url"));
        // Subscribe one more more topics to consume.
        consumer.subscribe(env.getProperty("app.message.user.topic"), "*");
        // Register callback to execute on arrival of messages fetched from brokers.
        consumer.registerMessageListener((MessageListenerConcurrently) (msgs, context) -> {
            msgs.stream().forEach(message -> {
                try {
                    MessageQueue messageQueue = context.getMessageQueue();
                    logger.info("From brokerName [{}], QueueId [{}], Topic[{}], Tags[{}], Keys[{}], Receive New MessageID: {}", messageQueue.getBrokerName(), messageQueue.getQueueId(),
                        message.getTopic(), message.getTags(), message.getKeys(), message.getMsgId());
                    if (logger.isDebugEnabled()) {
                        logger.debug("Message content: {}",  new String(message.getBody(), RemotingHelper.DEFAULT_CHARSET));
                    }
                } catch (UnsupportedEncodingException e) {
                    logger.warn("exception", e);
                }
            });
            return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
        });
        //Launch the consumer instance.
        consumer.start();
        return consumer;
    }

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章