RocketMQ

1 基本介绍

特点:

  • 能够保证严格的消息顺序`
  • 提供丰富的消息拉取模式
  • 高效的订阅者水平扩展能力
  • 实时的消息订阅机制
  • 亿级消息堆积能力

1.1 基本概念

  • 生产者(Producer):负责产生消息,生产者向消息服务器发送由业务应用程序系统生成的消息。
  • 消费者(Consumer):负责消费消息,消费者从消息服务器拉取信息并将其输入用户应用程序。
  • 消息服务器(Broker):是消息存储中心,主要作用是接收来自 Producer 的消息并存储, Consumer 从这里取得消息。
  • 名称服务器(NameServer):用来保存 Broker 相关 Topic 等元信息并给 Producer ,提供 Consumer 查找 Broker 信息。

1.2 整体流程

  1. 启动 Namesrv,Namesrv起 来后监听端口,等待 Broker、Producer、Consumer 连上来,相当于一个路由控制中心

  2. Broker 启动,跟所有的 Namesrv 保持长连接,定时发送心跳包。

心跳包中,包含当前 Broker 信息(IP+端口等)以及存储所有 Topic 信息。
注册成功后,Namesrv 集群中就有 Topic 跟 Broker 的映射关系。

  1. 收发消息前,先创建 Topic 。创建 Topic 时,需要指定该 Topic 要存储在哪些 Broker上。也可以在发送消息时自动创建Topic。

  2. Producer 发送消息。

启动时,先跟 Namesrv 集群中的其中一台建立长连接,并从Namesrv 中获取当前发送的 Topic 存在哪些 Broker 上,然后跟对应的 Broker 建立长连接,直接向 Broker 发消息。

  1. Consumer 消费消息

Consumer 跟 Producer 类似。跟其中一台 Namesrv 建立长连接,获取当前订阅 Topic 存在哪些 Broker 上,然后直接跟 Broker 建立连接通道,开始消费消息。

1.3 集群部署

  1. Producer
  • 1)Producer 自身在应用中,所以无需考虑高可用。
  • 2)Producer 配置多个 Namesrv 列表,从而保证 Producer 和 Namesrv 的连接高可用。并且,会从 Namesrv 定时拉取最新的 Topic 信息。
  • 3)Producer 会和所有 Consumer 直连,在发送消息时,会选择一个 Broker 进行发送。如果发送失败,则会使用另外一个 Broker 。
  • 4)Producer 会定时向 Broker 心跳,证明其存活。而 Broker 会定时检测,判断是否有 Producer 异常下线。
  1. Consumer.
  • 1)Consumer 需要部署多个节点,以保证 Consumer 自身的高可用。当相同消费者分组中有新的 Consumer 上线,或者老的 Consumer 下线,会重新分配 Topic 的 Queue 到目前消费分组的 Consumer 们。
  • 2)Consumer 配置多个 Namesrv 列表,从而保证 Consumer 和 Namesrv 的连接高可用。并且,会从 Consumer 定时拉取最新的 Topic 信息。
  • 3)Consumer 会和所有 Broker 直连,消费相应分配到的 Queue 的消息。如果消费失败,则会发回消息到 Broker 中。
  • 4)Consumer 会定时向 Broker 心跳,证明其存活。而 Broker 会定时检测,判断是否有 Consumer 异常下线。
  1. Namesrv
  • 1)Namesrv 需要部署多个节点,以保证 Namesrv 的高可用。
  • 2)Namesrv 本身是无状态,不产生数据的存储,是通过 Broker 心跳将 Topic 信息同步到 Namesrv 中。
  • 3)多个 Namesrv 之间不会有数据的同步,是通过 Broker 向多个 Namesrv 多写。
  1. Broker
    • 1)多个 Broker 可以形成一个 Broker 分组。每个 Broker 分组存在一个 Master 和多个 Slave 节点。
      • Master 节点,可提供读和写功能。Slave 节点,可提供读功能。
      • Master 节点会不断发送新的 CommitLog 给 Slave节点。Slave 节点不断上报本地的 CommitLog 已经同步到的位置给 Master 节点。
      • Slave 节点会从 Master 节点拉取消费进度、Topic 配置等等。
    • 2)多个 Broker 分组,形成 Broker 集群。
      • Broker 集群和集群之间,不存在通信与数据同步。
    • 3)Broker 可以配置同步刷盘或异步刷盘,根据消息的持久化的可靠性来配置。

2 简单示例

2.1 Producer

Producer 类,提供生产者 Producer 发送消息的最简示例。代码如下:

// Producer.java

public class Producer {

    public static void main(String[] args) throws MQClientException, InterruptedException {
        // <1.1> 创建 DefaultMQProducer 对象 设置的生产者分组
        DefaultMQProducer producer = new DefaultMQProducer("please_rename_unique_group_name");
        // <1.2> 设置 RocketMQ Namesrv 地址
        producer.setNamesrvAddr("127.0.0.1:9876");
        // <1.3> 启动 producer 生产者
        producer.start();
        for (int i = 0; i < 1000; i++) {
            try {
                // <2.1> 创建 Message 消息
                Message msg = new Message(
                    // Topic
                    "TopicTest",
                    // tag
                    "TagA" ,
                    // Message body
                    ("Hello RocketMQ " + i).getBytes(RemotingHelper.DEFAULT_CHARSET)
                );
                // <2.2> 同步发送消息
                SendResult sendResult = producer.send(msg);
                // <2.3> 打印发送结果
                System.out.printf("%s%n", sendResult);
            } catch (Exception e) {
                e.printStackTrace();
                Thread.sleep(1000);
            }
        }
        // <3> 关闭 producer 生产者
        producer.shutdown();
    }

}

2.2 Consumer

Consumer 类,提供消费者 Consumer 消费消息的最简示例。代码如下:

// Consumer.java

public class Consumer {

    public static void main(String[] args) throws InterruptedException, MQClientException {
        // <1> 创建 DefaultMQPushConsumer 对象 设置的消费者分组
        DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("please_rename_unique_group_name");
        // <2> 设置 RocketMQ Namesrv 地址
        consumer.setNamesrvAddr("127.0.0.1:9876");
        // <3> 设置消费进度,从 Topic 最初位置开始
        // CONSUME_FROM_FIRST_OFFSET -> 每个 Topic 队列的第一条消息
        // CONSUME_FROM_LAST_OFFSET -> 每个 Topic 队列的最后一条消息
        // CONSUME_FROM_TIMESTAMP -> 每个 Topic 队列的指定时间开始的消
        // 注意,只针对新的消费集群。如果一个集群每个 Topic 已经有消费进度,则继续使用该消费进度
        consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_FIRST_OFFSET);
        // <4> 订阅 TopicTest 主题 消费者组的消费者实例必须订阅完全相同的 Topic + Tag
        consumer.subscribe("TopicTest", "*");
        // <5> 添加消息监听器   
        // MessageListenerConcurrently 并发消费消息的监听器 
        // MessageListenerOrderly 顺序消费的监听器
        consumer.registerMessageListener(new MessageListenerConcurrently() {

            @Override
            public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs,
                ConsumeConcurrentlyContext context) {
                System.out.printf("%s Receive New Messages: %s %n", Thread.currentThread().getName(), msgs);
                // 返回成功
                return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
            }

        });
        // <6> 启动 producer 消费者
        consumer.start();
        // 打印 Consumer 启动完成
        System.out.printf("Consumer Started.%n");
    }
}

消费者分组

同一类 Consumer 的集合,这类 Consumer 通常消费同一类消息且消费逻辑一致。消费者组使得在消息消费方面,实现负载均衡和容错的目标变得非常容易。

要注意的是,消费者组的消费者实例必须订阅完全相同的 Topic + Tag

RocketMQ 支持两种消息模式:集群消费(Clustering)和广播消费(Broadcasting)。

  • 集群消费下,同一条消息只会被相同消费者分组的一个消费者所消费。
  • 广播消费下,同一条消息会被相同消费者分组的所有消费者所消费。
  • 在当前示例里,我们采用的是 DefaultMQPushConsumer 的默认消费方式,集群消费。

3 SpringBoot 示例

3.1 quickstart

3.1.1 YAML

application.yaml 配置文件:

# rocketmq 配置项,对应 RocketMQProperties 配置类
rocketmq:
  name-server: 127.0.0.1:9876 # RocketMQ Namesrv
  # Producer 配置项
  producer:
    group: demo-producer-group # 生产者分组
    send-message-timeout: 3000 # 发送消息超时时间,单位:毫秒。默认为 3000 。
    compress-message-body-threshold: 4096 # 消息压缩阀值,当消息体的大小超过该阀值后,进行消息压缩。默认为 4 * 1024B
    max-message-size: 4194304 # 消息体的最大允许大小。。默认为 4 * 1024 * 1024B
    retry-times-when-send-failed: 2 # 同步发送消息时,失败重试次数。默认为 2 次。
    retry-times-when-send-async-failed: 2 # 异步发送消息时,失败重试次数。默认为 2 次。
    retry-next-server: false # 发送消息给 Broker 时,如果发送失败,是否重试另外一台 Broker 。默认为 false
    access-key: # Access Key ,可阅读 https://github.com/apache/rocketmq/blob/master/docs/cn/acl/user_guide.md 文档
    secret-key: # Secret Key
    enable-msg-trace: true # 是否开启消息轨迹功能。默认为 true 开启。可阅读 https://github.com/apache/rocketmq/blob/master/docs/cn/msg_trace/user_guide.md 文档
    customized-trace-topic: RMQ_SYS_TRACE_TOPIC # 自定义消息轨迹的 Topic 。默认为 RMQ_SYS_TRACE_TOPIC 。
  # Consumer 配置项
  consumer:
    listeners: # 配置某个消费分组,是否监听指定 Topic 。结构为 Map<消费者分组, <Topic, Boolean>> 。默认情况下,不配置表示监听。
      test-consumer-group:
        topic1: false # 关闭 test-consumer-group 对 topic1 的监听消费
3.1.2 Producer

RocketMQTemplate ,实现三种发送消息的方式

// Demo01Producer.java

@Component
public class Demo01Producer {

    @Autowired
    private RocketMQTemplate rocketMQTemplate;

    public SendResult syncSend(Integer id) {
        // 创建 Demo01Message 消息
        Demo01Message message = new Demo01Message();
        message.setId(id);
        // 同步发送消息
        return rocketMQTemplate.syncSend(Demo01Message.TOPIC, message);
    }

    public void asyncSend(Integer id, SendCallback callback) {
        // 创建 Demo01Message 消息
        Demo01Message message = new Demo01Message();
        message.setId(id);
        // 异步发送消息
        rocketMQTemplate.asyncSend(Demo01Message.TOPIC, message, callback);
    }

    public void onewaySend(Integer id) {
        // 创建 Demo01Message 消息
        Demo01Message message = new Demo01Message();
        message.setId(id);
        // oneway 发送消息
        rocketMQTemplate.sendOneWay(Demo01Message.TOPIC, message);
    }

}
3.1.3 Consumer

实现 Rocket-Spring 定义的 RocketMQListener 接口,消费消息。代码如下

// Demo01Consumer.java

@Component
@RocketMQMessageListener(
    	// 指定消费的TOPIC
        topic = Demo01Message.TOPIC,// 指定消费者分组
        consumerGroup = "demo01-consumer-group-" + Demo01Message.TOPIC
)
public class Demo01Consumer implements RocketMQListener<Demo01Message> {

    private Logger logger = LoggerFactory.getLogger(getClass());

    @Override
    public void onMessage(Demo01Message message) {
        logger.info("[onMessage][线程编号:{} 消息内容:{}]", Thread.currentThread().getId(), message);
    }

}

一般情况下,我们建议一个消费者分组,仅消费一个 Topic 。这样做会有两个好处:

  • 每个消费者分组职责单一,只消费一个 Topic 。
  • 每个消费者分组是独占一个线程池,这样能够保证多个 Topic 隔离在不同线程池,保证隔离性,从而避免一个 Topic 消费很慢,影响到另外的 Topic 的消费。
3.1.4 ConsumerA

实现 Rocket-Spring 定义的 RocketMQListener 接口,消费消息。代码如下:

@Component
@RocketMQMessageListener(
        topic = Demo01Message.TOPIC,
        consumerGroup = "demo01-A-consumer-group-" + Demo01Message.TOPIC
)
public class Demo01AConsumer implements RocketMQListener<MessageExt> {

    private Logger logger = LoggerFactory.getLogger(getClass());

    @Override
    public void onMessage(MessageExt message) {
        logger.info("[onMessage][线程编号:{} 消息内容:{}]", Thread.currentThread().getId(), message);
    }

}

与 3.1.3 差异点:

  • 差异一:消费者分组修改成demo01-A-consumer-group-DEMO_01,这样,我们就可以测试 RocketMQ 集群消费的特性

    集群消费(Clustering):集群消费模式下,相同 Consumer Group 的每个 Consumer 实例平均分摊消息。

    • 也就是说,如果我们发送一条 Topic 为 "DEMO_01" 的消息,可以分别被 "demo01-A-consumer-group-DEMO_01""demo01-consumer-group-DEMO_01" 都消费一次。
    • 但是,如果我们启动两个该示例的实例,则消费者分组 "demo01-A-consumer-group-DEMO_01""demo01-consumer-group-DEMO_01" 都会有多个 Consumer 示例。此时,我们再发送一条 Topic 为 "DEMO_01" 的消息,只会被 "demo01-A-consumer-group-DEMO_01" 的一个 Consumer 消费一次,也同样只会被 "demo01-A-consumer-group-DEMO_01" 的一个 Consumer 消费一次。
  • 差异二: 实现 RocketMQListener 接口,在 T 泛型里,设置消费的消息对应的类不是 Demo01Message 类,而是 RocketMQ 内置的 MessageExt 类。通过 MessageExt 类,我们可以获取到消费的消息的更多信息,例如说消息的所属队列、创建时间等等属性,不过消息的内容(body)就需要自己去反序列化。当然,一般情况下,我们不会使用 MessageExt 类。

3.2 @RocketMQMessageListener

@RocketMQMessageListener注解

public @interface RocketMQMessageListener {

    String NAME_SERVER_PLACEHOLDER = "${rocketmq.name-server:}";
    String ACCESS_KEY_PLACEHOLDER = "${rocketmq.consumer.access-key:}";
    String SECRET_KEY_PLACEHOLDER = "${rocketmq.consumer.secret-key:}";
    String TRACE_TOPIC_PLACEHOLDER = "${rocketmq.consumer.customized-trace-topic:}";
    String ACCESS_CHANNEL_PLACEHOLDER = "${rocketmq.access-channel:}";

    /**
     * Consumer 所属消费者分组
     */
    String consumerGroup();

    /**
     * Topic name.
     */
    String topic();

    /**
     * 选择器类型。默认基于 Message 的 Tag 选择。
     */
    SelectorType selectorType() default SelectorType.TAG;

    /**
     * 选择器的表达式,设置为 * 时,表示全部。
     * 如果使用 SelectorType.TAG 类型,则设置消费 Message 的具体 Tag 。
     * 如果使用 SelectorType.SQL92 类型,可见 https://rocketmq.apache.org/rocketmq/filter-messages-by-sql92-in-rocketmq/ 文档
     */
    String selectorExpression() default "*";

    /**
     * 消费模式。可选择并发消费,还是顺序消费。
     */
    ConsumeMode consumeMode() default ConsumeMode.CONCURRENTLY;

    /**
     * 消息模型。可选择是集群消费,还是广播消费。
     */
    MessageModel messageModel() default MessageModel.CLUSTERING;

    /**
     * 消费的线程池的最大线程数
     */
    int consumeThreadMax() default 64;

    /**
     * 消费单条消息的超时时间
     */
    long consumeTimeout() default 30000L;

    /**
     * The property of "access-key".
     */
    String accessKey() default ACCESS_KEY_PLACEHOLDER;

    /**
     * The property of "secret-key".
     */
    String secretKey() default SECRET_KEY_PLACEHOLDER;

    /**
     * Switch flag instance for message trace.
     */
    boolean enableMsgTrace() default true;

    /**
     * The name value of message trace topic.If you don't config,you can use the default trace topic name.
     */
    String customizedTraceTopic() default TRACE_TOPIC_PLACEHOLDER;

    /**
     * Consumer 连接的 RocketMQ Namesrv 地址。默认情况下,使用 `rocketmq.name-server` 配置项即可。
     * 如果一个项目中,Consumer 需要使用不同的 RocketMQ Namesrv ,则需要配置该属性。
     */
    String nameServer() default NAME_SERVER_PLACEHOLDER;

    /**
     * 访问通道。目前有 LOCAL 和 CLOUD 两种通道
     * LOCAL ,指的是本地部署的 RocketMQ 开源项目。
     * CLOUD ,指的是阿里云的 ONS 服务。具体可见 https://help.aliyun.com/document_detail/128585.html 文档。
     */
    String accessChannel() default ACCESS_CHANNEL_PLACEHOLDER;
}

3.3 连接不同的 RocketMQ 集群

RocketMQ-Spring 考虑到开发者可能需要连接多个不同的 RocketMQ 集群,所以提供了 @ExtRocketMQTemplateConfiguration 注解,实现配置连接不同 RocketMQ 集群的 Producer 的 RocketMQTemplate Bean 对象。

@ExtRocketMQTemplateConfiguration 注解的简单使用示例,代码如下:

// 在类上,添加 @ExtRocketMQTemplateConfiguration 注解,并设置连接的 RocketMQ Namesrv 地址。
// 需要继承 RocketMQTemplate 类 从而使我们可以直接使用 @Autowire 或 @Resource 注解,注入 RocketMQTemplate Bean 属性。
@ExtRocketMQTemplateConfiguration(nameServer = "${demo.rocketmq.extNameServer:demo.rocketmq.name-server}")
public class ExtRocketMQTemplate extends RocketMQTemplate {
}

3.4 批量发送消息

在一些业务场景下,我们希望使用 Producer 批量发送消息,提高发送性能。在 RocketMQTemplate 中,提供了一个方法方法批量发送消息的方法。代码如下:

// RocketMQTemplate.java
// 通过方法参数 destination 可知,必须发送相同 Topic 的消息
// 要注意方法参数 messages ,每个集合的元素必须是 Spring Messaging 定义的 Message 消息
// 同步批量发送消息
// 有一点要注意,虽然是批量发送多条消息,但是是以所有消息加起来的大小,不能超过消息的最大大小的限制,而不是按照单条计算。所以,一次性发送的消息特别多,还是需要分批的进行批量发送。
public <T extends Message> SendResult syncSend(String destination, Collection<T> messages, long timeout) {
    // ... 省略具体代码实现
}
3.4.1 Producer

使用 RocketMQTemplate 实现批量发送消息。代码如下:

// Demo02Producer.java

@Component
public class Demo02Producer {

    @Autowired
    private RocketMQTemplate rocketMQTemplate;

    public SendResult sendBatch(Collection<Integer> ids) {
        // <X> 创建多条 Demo02Message 消息
        // 创建了 Spring Messaging 定义的 Message 消息的数组
        List<Message> messages = new ArrayList<>(ids.size());
        for (Integer id : ids) {
            // 创建 Demo02Message 消息
            Demo02Message message = new Demo02Message().setId(id);
            // 构建 Spring Messaging 定义的 Message 消息
            messages.add(MessageBuilder.withPayload(message).build());
        }
        // 同步批量发送消息
        return rocketMQTemplate.syncSend(Demo02Message.TOPIC, messages, 30 * 1000L);
    }

}
3.4.2 Consumer

实现 Rocket-Spring 定义的 RocketMQListener 接口,消费消息。代码如下:

// Demo02Consumer.java

@Component
@RocketMQMessageListener(
        topic = Demo02Message.TOPIC,
        consumerGroup = "demo02-consumer-group-" + Demo02Message.TOPIC
)
public class Demo02Consumer implements RocketMQListener<Demo02Message> {

    private Logger logger = LoggerFactory.getLogger(getClass());
	// 虽然说,Demo02Message 消息是批量发送的,但是我们还是可以和 「3.8 Demo1Consumer」 一样,逐条消费消息
    @Override
    public void onMessage(Demo02Message message) {
        logger.info("[onMessage][线程编号:{} 消息内容:{}]", Thread.currentThread().getId(), message);
    }

}

3.5 定时消息

在 RocketMQ 中,提供定时消息的功能。

不过,RocketMQ 暂时不支持任意的时间精度的延迟,而是固化了 18 个延迟级别。如下表格:

延迟级别时间延迟级别时间延迟级别时间
11s73m139m
25s84m1410m
310s95m1520m
430s106m1630m
51m117m171h
62m128m182h

如果胖友想要任一时刻的定时消息,可以考虑借助 MySQL + Job 来实现。又或者考虑使用 DDMQ(滴滴打车基于 RocketMQ 和 Kafka 改造的开源消息队列) 。

3.5.1 Producer

使用 RocketMQTemplate 实现发送定时消息。目前只支持同步异步发送定时消息。代码如下:

// Demo03Producer.java

@Component
public class Demo03Producer {

    @Autowired
    private RocketMQTemplate rocketMQTemplate;

    public SendResult syncSendDelay(Integer id, int delayLevel) {
        // 创建 Demo03Message 消息
        Message message = MessageBuilder.withPayload(new Demo03Message().setId(id))
                .build();
        // 同步发送消息
        return rocketMQTemplate.syncSend(Demo03Message.TOPIC, message, 30 * 1000,
                delayLevel);
    }

    public void asyncSendDelay(Integer id, int delayLevel, SendCallback callback) {
        // 创建 Demo03Message 消息
        Message message = MessageBuilder.withPayload(new Demo03Message().setId(id))
                .build();
        // 同步发送消息
        rocketMQTemplate.asyncSend(Demo03Message.TOPIC, message, callback, 30 * 1000,
                delayLevel);
    }

}
3.5.2 Consumer

实现 Rocket-Spring 定义的 RocketMQListener 接口,消费消息。代码如下:

// Demo03Consumer.java

@Component
@RocketMQMessageListener(
        topic = Demo03Message.TOPIC,
        consumerGroup = "demo03-consumer-group-" + Demo03Message.TOPIC
)
public class Demo03Consumer implements RocketMQListener<Demo03Message> {

    private Logger logger = LoggerFactory.getLogger(getClass());
	// 逐条消费消息。
    @Override
    public void onMessage(Demo03Message message) {
        logger.info("[onMessage][线程编号:{} 消息内容:{}]", Thread.currentThread().getId(), message);
    }

}

3.6 消费重试

RocketMQ 提供消费重试的机制。在消息消费失败的时候,RocketMQ 会通过消费重试机制,重新投递该消息给 Consumer ,让 Consumer 有机会重新消费消息,实现消费成功。

当然,RocketMQ 并不会无限重新投递消息给 Consumer 重新消费,而是在默认情况下,达到 16 次重试次数时,Consumer 还是消费失败时,该消息就会进入到死信队列

死信队列用于处理无法被正常消费的消息。当一条消息初次消费失败,消息队列会自动进行消息重试;达到最大重试次数后,若消费依然失败,则表明消费者在正常情况下无法正确地消费该消息,此时,消息队列不会立刻将消息丢弃,而是将其发送到该消费者对应的特殊队列中。

RocketMQ 将这种正常情况下无法被消费的消息称为死信消息(Dead-Letter Message),将存储死信消息的特殊队列称为死信队列(Dead-Letter Queue)。在 RocketMQ 中,可以通过使用 console 控制台对死信队列中的消息进行重发来使得消费者实例再次进行消费。

每条消息的失败重试,是有一定的间隔时间。实际上,消费重试是基于定时消息来实现,第一次重试消费按照延迟级别为 3 开始。😈 所以,默认为 16 次重试消费,也非常好理解,毕竟延迟级别最高为 18 呀。

不过要注意,只有集群消费模式下,才有消息重试。

3.6.1 Producer

使用 RocketMQ-Spring 封装提供的 RocketMQTemplate ,实现同步发送消息。代码如下:

// Demo04Producer.java

@Component
public class Demo04Producer {

    @Autowired
    private RocketMQTemplate rocketMQTemplate;

    public SendResult syncSend(Integer id) {
        // 创建 Demo04Message 消息
        Demo04Message message = new Demo04Message();
        message.setId(id);
        // 同步发送消息
        return rocketMQTemplate.syncSend(Demo04Message.TOPIC, message);
    }

}
3.6.2 Consumer

实现 Rocket-Spring 定义的 RocketMQListener 接口,消费消息。代码如下:

// Demo04Consumer.java

@Component
@RocketMQMessageListener(
        topic = Demo04Message.TOPIC,
        consumerGroup = "demo04-consumer-group-" + Demo04Message.TOPIC
)
public class Demo04Consumer implements RocketMQListener<Demo04Message> {

    private Logger logger = LoggerFactory.getLogger(getClass());

    @Override
    public void onMessage(Demo04Message message) {
        logger.info("[onMessage][线程编号:{} 消息内容:{}]", Thread.currentThread().getId(), message);
        // <X> 注意,此处抛出一个 RuntimeException 异常,模拟消费失败
        throw new RuntimeException("我就是故意抛出一个异常");
    }

}

3.7 广播消费

在一些场景下,我们需要使用广播消费

广播消费模式下,相同 Consumer Group 的每个 Consumer 实例都接收全量的消息。

3.7.1 Producer

使用 RocketMQ-Spring 封装提供的 RocketMQTemplate ,实现同步发送消息。代码如下:

// Demo05Producer.java

@Component
public class Demo05Producer {

    @Autowired
    private RocketMQTemplate rocketMQTemplate;

    public SendResult syncSend(Integer id) {
        // 创建 Demo05Message 消息
        Demo05Message message = new Demo05Message();
        message.setId(id);
        // 同步发送消息
        return rocketMQTemplate.syncSend(Demo05Message.TOPIC, message);
    }

}
3.7.2 Consumer

实现 Rocket-Spring 定义的 RocketMQListener 接口,消费消息。代码如下:

// Demo05Consumer.java

@Component
@RocketMQMessageListener(
        topic = Demo05Message.TOPIC,
        consumerGroup = "demo05-consumer-group-" + Demo05Message.TOPIC,
    	// 设置为广播消费
        messageModel = MessageModel.BROADCASTING 
)
public class Demo05Consumer implements RocketMQListener<Demo05Message> {

    private Logger logger = LoggerFactory.getLogger(getClass());

    @Override
    public void onMessage(Demo05Message message) {
        logger.info("[onMessage][线程编号:{} 消息内容:{}]", Thread.currentThread().getId(), message);
    }

}

3.8 顺序消息

RocketMQ 提供了两种顺序级别:

  • 普通顺序消息 :Producer 将相关联的消息发送到相同的消息队列。
  • 完全严格顺序 :在【普通顺序消息】的基础上,Consumer 严格顺序消费。

如下是 RocketMQ 官方文档对这两种顺序级别的定义:

  • 普通顺序消费模式下,消费者通过同一个消费队列收到的消息是有顺序的,不同消息队列收到的消息则可能是无顺序的。
  • 严格顺序消息模式下,消费者收到的所有消息均是有顺序的。
3.8.1 Producer

使用 RocketMQ-Spring 封装提供的 RocketMQTemplate ,实现三种发送顺序消息的方式。代码如下:

// Demo06Producer.java

@Component
public class Demo06Producer {

    @Autowired
    private RocketMQTemplate rocketMQTemplate;

    public SendResult syncSendOrderly(Integer id) {
        // 创建 Demo06Message 消息
        Demo06Message message = new Demo06Message();
        message.setId(id);
        // 同步发送消息
        // 调用了对应的 Orderly 方法,从而实现发送顺序消息。
        // 同时,需要传入方法参数 hashKey ,作为选择消息队列的键。
        // @param hashKey      use this key to select queue. for example: orderId, productId ...
        return rocketMQTemplate.syncSendOrderly(Demo06Message.TOPIC, message, String.valueOf(id));
    }

    public void asyncSendOrderly(Integer id, SendCallback callback) {
        // 创建 Demo06Message 消息
        Demo06Message message = new Demo06Message();
        message.setId(id);
        // 异步发送消息
        // 调用了对应的 Orderly 方法,从而实现发送顺序消息。
        // 同时,需要传入方法参数 hashKey ,作为选择消息队列的键。
        // @param hashKey      use this key to select queue. for example: orderId, productId ...
        rocketMQTemplate.asyncSendOrderly(Demo06Message.TOPIC, message, String.valueOf(id), callback);
    }

    public void onewaySendOrderly(Integer id) {
        // 创建 Demo06Message 消息
        Demo06Message message = new Demo06Message();
        message.setId(id);
        // 异步发送消息
		// 调用了对应的 Orderly 方法,从而实现发送顺序消息。
        // 同时,需要传入方法参数 hashKey ,作为选择消息队列的键。
        // @param hashKey      use this key to select queue. for example: orderId, productId ...
        rocketMQTemplate.sendOneWayOrderly(Demo06Message.TOPIC, message, String.valueOf(id));
    }

}

在 RocketMQ 中,Producer 可以根据定义 MessageQueueSelector 消息队列选择策略,选择 Topic 下的队列。目前提供三种策略:

RocketMQTemplate 在发送顺序消息时,默认采用 SelectMessageQueueByHash 策略。如此,相同的 hashKey 的消息,就可以发送到相同的 Topic 的对应队列中。这种形式,就是我们上文提到的普通顺序消息的方式。

3.8.2 Consumer

实现 Rocket-Spring 定义的 RocketMQListener 接口,消费消息。代码如下:

// Demo06Consumer.java

@Component
@RocketMQMessageListener(
        topic = Demo06Message.TOPIC,
        consumerGroup = "demo06-consumer-group-" + Demo06Message.TOPIC,
    	// 设置为顺序消费
        consumeMode = ConsumeMode.ORDERLY 
)
public class Demo06Consumer implements RocketMQListener<Demo06Message> {

    private Logger logger = LoggerFactory.getLogger(getClass());

    @Override
    public void onMessage(Demo06Message message) {
        logger.info("[onMessage][线程编号:{} 消息内容:{}]", Thread.currentThread().getId(), message);

        // sleep 2 秒,用于查看顺序消费的效果
        try {
            Thread.sleep(2 * 1000L);
        } catch (InterruptedException ignore) {
        }
    }

}

3.9 事务消息

在分布式消息队列中,目前唯一提供完整的事务消息的,只有 RocketMQ 。关于这一点,还是可以鼓吹下的。

引用一下原文

可能会有胖友怒喷艿艿,RabbitMQ 和 Kafka 也有事务消息啊,也支持发送事务消息的发送,以及后续的事务消息的 commit提交或 rollbackc 回滚。但是要考虑一个极端的情况,在本地数据库事务已经提交的时时候,如果因为网络原因,又或者崩溃等等意外,导致事务消息没有被 commit ,最终导致这条事务消息丢失,分布式事务出现问题。

相比来说,RocketMQ 提供事务回查机制,如果应用超过一定时长未 commit 或 rollback 这条事务消息,RocketMQ 会主动回查应用,询问这条事务消息是 commit 还是 rollback ,从而实现事务消息的状态最终能够被 commit 或是 rollback ,达到最终事务的一致性。

这也是为什么艿艿在上面专门加粗“完整的”三个字的原因。可能上述的描述,对于绝大多数没有了解过分布式事务的胖友,会比较陌生,所以推荐阅读如下两篇文章:

热心的艿艿:虽然说 RabbitMQ、Kafka 并未提供完整的事务消息,但是社区里,已经基于它们之上拓展,提供了事务回查的功能。例如说:Myth ,采用消息队列解决分布式事务的开源框架, 基于 Java 语言来开发(JDK1.8),支持 Dubbo,Spring Cloud,Motan 等 RPC 框架进行分布式事务。

3.9.1 Producer

使用 RocketMQ-Spring 封装提供的 RocketMQTemplate ,实现发送事务消息。代码如下:

// Demo07Producer.java

@Component
public class Demo07Producer {

    private static final String TX_PRODUCER_GROUP = "demo07-producer-group";

    @Autowired
    private RocketMQTemplate rocketMQTemplate;

    public TransactionSendResult sendMessageInTransaction(Integer id) {
        // <1> 创建 Demo07Message 消息 -> Spring Messaging Message 消息。
        Message message = MessageBuilder.withPayload(new Demo07Message().setId(id))
                .build();
        // <2> 发送事务消息
        return rocketMQTemplate.sendMessageInTransaction(TX_PRODUCER_GROUP, Demo07Message.TOPIC, message,
                id);
    }

}

调用 RocketMQTemplate#sendMessageInTransaction(...) 方法,发送事务消息。我们来看看该方法的方法参数,代码如下:

// RocketMQTemplate.java

/**
 * Send Spring Message in Transaction
 *
 * @param txProducerGroup 事务消息的生产者分组。因为 RocketMQ 是回查(请求)指定指定生产分组下的 Producer 获得事务消息的状态
 * @param destination     destination formats: `topicName:tags`  消息的 Topic + Tag 
 * @param message         message {@link org.springframework.messaging.Message}
 * @param arg             ext arg 后续调用本地事务方法的时候,会传入该arg 
 * @return TransactionSendResult
 * @throws MessagingException
 */
public TransactionSendResult sendMessageInTransaction(final String txProducerGroup, final String destination,
    final Message<?> message, final Object arg) throws MessagingException {
    try {
        TransactionMQProducer txProducer = this.stageMQProducer(txProducerGroup);
        org.apache.rocketmq.common.message.Message rocketMsg = this.createRocketMqMessage(destination, message);
        return txProducer.sendMessageInTransaction(rocketMsg, arg);
    } catch (MQClientException e) {
        throw RocketMQUtil.convert(e);
    }
}
3.9.2 TransactionListener

TransactionListenerImpl ,实现 MQ 事务的监听。代码如下:

// Demo07Producer.java
// 声明监听器的是生产者分组是 "demo07-producer-group" 的 Producer 发送的事务消息。
@RocketMQTransactionListener(txProducerGroup = TX_PRODUCER_GROUP)
// 实现 RocketMQLocalTransactionListener 接口,实现执行本地事务和检查本地事务的方法。
public class TransactionListenerImpl implements RocketMQLocalTransactionListener {

    private Logger logger = LoggerFactory.getLogger(getClass());

    @Override
    public RocketMQLocalTransactionState executeLocalTransaction(Message msg, Object arg) {
        // ... local transaction process, return rollback, commit or unknown
        logger.info("[executeLocalTransaction][执行本地事务,消息:{} arg:{}]", msg, arg);
        return RocketMQLocalTransactionState.UNKNOWN;
    }

    @Override
    public RocketMQLocalTransactionState checkLocalTransaction(Message msg) {
        // ... check transaction status and return rollback, commit or unknown
        logger.info("[checkLocalTransaction][回查消息:{}]", msg);
        return RocketMQLocalTransactionState.COMMIT;
    }

}
  • 实现#executeLocalTransaction(…)方法,实现执行本地事务。
    • 注意,这是一个模板方法。在调用这个方法之前,RocketMQTemplate 已经使用 Producer 发送了一条事务消息。然后根据该方法执行的返回的 RocketMQLocalTransactionState 结果,提交还是回滚该事务消息。
    • 这里,我们为了模拟 RocketMQ 回查 Producer 来获得事务消息的状态,所以返回了 RocketMQLocalTransactionState.UNKNOWN 未知状态。
  • 实现#checkLocalTransaction(…)方法,检查本地事务。
    • 在事务消息长事件未被提交或回滚时,RocketMQ 会回查事务消息对应的生产者分组下的 Producer ,获得事务消息的状态。此时,该方法就会被调用。
    • 这里,我们直接返回 RocketMQLocalTransactionState.COMMIT 提交状态。

一般来说,有两种方式实现本地事务回查时,返回事务消息的状态。

第一种,通过 msg 消息,获得某个业务上的标识或者编号,然后去数据库中查询业务记录,从而判断该事务消息的状态是提交还是回滚。

第二种,记录 msg 的事务编号,与事务状态到数据库中。

  • 第一步,在 #executeLocalTransaction(...) 方法中,先存储一条 idmsg 的事务编号,状态为 RocketMQLocalTransactionState.UNKNOWN 的记录。
  • 第二步,调用带有事务的业务 Service 的方法。在该 Service 方法中,在逻辑都执行成功的情况下,更新 idmsg 的事务编号,状态变更为 RocketMQLocalTransactionState.COMMIT 。这样,我们就可以伴随这个事务的提交,更新 idmsg 的事务编号的记录的状为 RocketMQLocalTransactionState.COMMIT ,美滋滋。。
  • 第三步,要以 try-catch 的方式,调用业务 Service 的方法。如此,如果发生异常,回滚事务的时候,可以在 catch 中,更新 idmsg 的事务编号的记录的状态为 RocketMQLocalTransactionState.ROLLBACK 。😭 极端情况下,可能更新失败,则打印 error 日志,告警知道,人工介入。
  • 如此三步之后,我们在 #executeLocalTransaction(...) 方法中,就可以通过查找数据库,idmsg 的事务编号的记录的状态,然后返回。

相比来说,艿艿倾向第二种,实现更加简单通用,对于业务开发者,更加友好。和有几个朋友沟通了下,他们也是采用第二种。

3.9.3 Consumer

实现 Rocket-Spring 定义的 RocketMQListener 接口,消费消息。代码如下:

@Component
@RocketMQMessageListener(
        topic = Demo07Message.TOPIC,
        consumerGroup = "demo07-consumer-group-" + Demo07Message.TOPIC
)
public class Demo07Consumer implements RocketMQListener<Demo07Message> {

    private Logger logger = LoggerFactory.getLogger(getClass());

    @Override
    public void onMessage(Demo07Message message) {
        logger.info("[onMessage][线程编号:{} 消息内容:{}]", Thread.currentThread().getId(), message);
    }

}
3.9.4 @RocketMQTransactionListener

TransactionListenerImpl中,我们已经使用了 @RocketMQTransactionListener 注解,设置 MQ 事务监听器的信息。具体属性如下:

// RocketMQTransactionListener.java

public @interface RocketMQTransactionListener {

    /**
     * 事务的生产者分组
     *
     * Declare the txProducerGroup that is used to relate callback event to the listener, rocketMQTemplate must send a
     * transactional message with the declared txProducerGroup.
     * <p>
     * <p>It is suggested to use the default txProducerGroup if your system only needs to define a TransactionListener class.
     */
    String txProducerGroup() default RocketMQConfigUtils.ROCKETMQ_TRANSACTION_DEFAULT_GLOBAL_NAME;
    /**
     * Set ExecutorService params -- corePoolSize
     */
    int corePoolSize() default 1;
    /**
     * Set ExecutorService params -- maximumPoolSize
     */
    int maximumPoolSize() default 1;
    /**
     * Set ExecutorService params -- keepAliveTime
     */
    long keepAliveTime() default 1000 * 60; //60ms
    /**
     * Set ExecutorService params -- blockingQueueSize
     */
    int blockingQueueSize() default 2000;
    /**
     * The property of "access-key"
     */
    String accessKey() default "${rocketmq.producer.access-key}";
    /**
     * The property of "secret-key"
     */
    String secretKey() default "${rocketmq.producer.secret-key}";
}

4 参考文章

芋道 RocketMQ 极简入门

芋道 Spring Boot 消息队列 RocketMQ 入门

推广

公众号二维码

公众号会推送更多更新,关注支持一下 _

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值