RocketMQ的消息模型(1)

79 阅读18分钟

1、RocketMQ客户端基本流程

RocketMQ基于Maven提供了客户端的核心依赖:

<dependency>
<groupId>org.apache.rocketmq</groupId>
<artifactId>rocketmq-client</artifactId>
<version>5.3.0</version>
</dependency> 

⼀个最为简单的消息⽣产者代码如下:

public class Producer {
    public static void main(String[] args) throws MQClientException, InterruptedException {
        //初始化⼀个消息⽣产者
        DefaultMQProducer producer = new DefaultMQProducer("please_rename_unique_group_name");
        // 指定nameserver地址
        producer.setNamesrvAddr("192.168.65.112:9876");
        // 启动消息⽣产者服务
        producer.start();
        for (int i = 0; i < 2; i++) {
            try {
                // 创建消息。消息由Topic,Tag和body三个属性组成,其中Body就是消息内容
                Message msg = new Message("TopicTest","TagA",("Hello RocketMQ" +i).getBytes(RemotingHelper.DEFAULT_CHARSET));
                //发送消息,获取发送结果
                SendResult sendResult = producer.send(msg);
                System.out.printf("%s%n", sendResult);
            } catch (Exception e) {
                e.printStackTrace();
                Thread.sleep(1000);
            }
        }
        //消息发送完后,停⽌消息⽣产者服务。
        producer.shutdown();
    }
}

⼀个简单的消息消费者代码如下:

public class Consumer {
    public static void main(String[] args) throws InterruptedException, MQClientException {
        //构建⼀个消息消费者
        DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("please_rename_unique_group_name_4");
        //指定nameserver地址
        consumer.setNamesrvAddr("192.168.65.112:9876");
        consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_LAST_OFFSET);
        // 订阅⼀个感兴趣的话题,这个话题需要与消息的topic⼀致
        consumer.subscribe("TopicTest", "*");
        // 注册⼀个消息回调函数,消费到消息后就会触发回调。
        consumer.registerMessageListener(new MessageListenerConcurrently() {
            @Override
            public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs,ConsumeConcurrentlyContext context) {
            msgs.forEach(messageExt -> {
            try {
                System.out.println("收到消息:"+new String(messageExt.getBody(),
                RemotingHelper.DEFAULT_CHARSET));
            } catch (UnsupportedEncodingException e) {}
            });
            return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
        }
        });
        //启动消费者服务
        consumer.start();
        System.out.print("Consumer Started");
    }
}

RocketMQ的客户端编程模型相对比较固定,基本都有⼀个固定的步骤。掌握这个固定步骤,对于学习其他复杂的消息模型也是很有帮助的。

  • 消息⽣产者的固定步骤
  1. 创建消息⽣产者producer,并指定⽣产者组名
  2. 指定Nameserver地址
  3. 启动producer。 这个步骤⽐较容易忘记。可以认为这是消息⽣产者与服务端建⽴连接的过程。
  4. 创建消息对象,指定主题Topic、Tag和消息体
  5. 发送消息
  6. 关闭⽣产者producer,释放资源。
  • 消息消费者的固定步骤
  1. 创建消费者Consumer,必须指定消费者组名
  2. 指定Nameserver地址
  3. 订阅主题Topic和Tag
  4. 设置回调函数,处理消息
  5. 启动消费者consumer。消费者会⼀直挂起,持续处理消息。 其中,最为关键的就是NameServer。从示例中可以看到,RocketMQ的客户端只需要指定NameServer地址,⽽不需要指定具体的Broker地址。

指定NameServer的⽅式有两种。可以在客户端直接指定,例如 consumer.setNameSrvAddr("127.0.0.19876")。然后,也可以通过读取系统环境变量NAMESRV_ADDR指定。其中第⼀种⽅式的优先级更⾼。

2、消息确认机制

RocketMQ要⽀持互联⽹⾦融场景,那么消息安全是必须优先保障的。⽽消息安全有两⽅⾯的要求,⼀⽅⾯是生产者要能确保将消息发送到Broker上。另⼀⽅⾯是消费者要能确保从Broker上争取获取到消息。

1、消息⽣产端采⽤消息确认加多次重试的机制保证消息正常发送到RocketMQ

针对消息发送的不确定性,封装了三种发送消息的⽅式。

第⼀种称为单向发送

单向发送⽅式下,消息⽣产者只管往Broker发送消息,⽽全然不关⼼Broker端有没有成功接收到消息。这就好⽐⽣产者向Broker发⼀封电⼦邮件,Broker有没有处理电⼦邮件,⽣产者并不知道。

public class OnewayProducer {
    public static void main(String[] args)throws Exception{
        DefaultMQProducer producer = new DefaultMQProducer("producerGroup");
        producer.start();
        Message message = new Message("Order","tag","order info : orderId = xxx".getBytes(StandardCharsets.UTF_8));
        producer.sendOneway(message);
        Thread.sleep(50000);
        producer.shutdown();
    }
}

sendOneway⽅法没有返回值,如果发送失败,⽣产者⽆法补救。

单向发送有⼀个好处,就是发送消息的效率更⾼。适⽤于⼀些追求消息发送效率,⽽允许消息丢失的业务场景。⽐如⽇志。

第⼆种称为同步发送

同步发送⽅式下,消息⽣产者在往Broker端发送消息后,会阻塞当前线程,等待Broker端的相应结果。这就好⽐⽣产者给Broker打了个电话。通话期间⽣产者就停下⼿头的事情,直到Broker明确表示消息处理成功了,⽣产者才继续做其他的事情。

SendResult sendResult = producer.send(msg);

SendResult来⾃于Broker的反馈。producer在send发出消息,到Broker返回SendResult的过程中,⽆法做其他的事情。

在SendResult中有⼀个SendStatus属性,这个SendStatus是⼀个枚举类型,其中包含了Broker端的各种情况。

public enum SendStatus {
    SEND_OK,
    FLUSH_DISK_TIMEOUT,
    FLUSH_SLAVE_TIMEOUT,
    SLAVE_NOT_AVAILABLE,
}

在这⼏种枚举值中,SEND_OK表示消息已经成功发送到Broker上。⾄于其他⼏种枚举值,都是表示消息在Broker端处理失败了。使⽤同步发送的机制,我们就可以在消息⽣产者发送完消息后,对发送失败的消息进⾏补救。例如重新发送。

但是此时要注意,如果Broker端返回的SendStatus不是SEND_OK,也并不表示消息就⼀定不会推送给下游的消费者。仅仅只是表示Broker端并没有完全正确的处理这些消息。因此,如果要重新发送消息,最好要带上唯⼀的系统标识,这样在消费者端,才能⾃⾏做幂等判断。也就是⽤具有业务含义的OrderID这样的字段来判断消息有没有被重复处理。

这种同步发送的机制能够很⼤程度上保证消息发送的安全性。但是,这种同步发送机制的发送效率⽐较低。毕竟,send⽅法需要消息在⽣产者和Broker之间传输⼀个来回后才能结束。如果⽹速⽐较慢,同步发送的耗时就会很⻓。

第三种称为异步发送

异步发送机制下,⽣产者在向Broker发送消息时,会同时注册⼀个回调函数。接下来⽣产者并不等待Broker的响应。当Broker端有响应数据过来时,⾃动触发回调函数进⾏对应的处理。这就好⽐⽣产者向Broker发电⼦邮件通知时,另外找了⼀个代理⼈专⻔等待Broker的响应。⽽⽣产者⾃⼰则发完消息后就去做其他的事情去了。

producer.send(msg, new SendCallback() {
    @Override
    public void onSuccess(SendResult sendResult) {
        countDownLatch.countDown();
        System.out.printf("%-10d OK %s %n", index, sendResult.getMsgId());
    }
    @Override
    public void onException(Throwable e) {
        countDownLatch.countDown();
        System.out.printf("%-10d Exception %s %n", index, e);
        e.printStackTrace();
    }
});

在SendCallback接⼝中有两个⽅法,onSuccess和onException。当Broker端返回消息处理成功的响应信息SendResult时,就会调⽤onSuccess⽅法。当Broker端处理消息超时或者失败时,就会调⽤onExcetion⽅法,⽣产者就可以在onException⽅法中进⾏补救措施。此时同样有⼏个问题需要注意。⼀是与同步发送机制类似,触发了SendCallback的onException⽅法同样并不⼀定就表示消息不会向消费者推送。如果Broker端返回响应信息太慢,超过了超时时间,也会触发onException⽅法。超时时间默认是3秒,可以通过producer.setSendMsgTimeout⽅法定制。⽽造成超时的原因则有很多,消息太⼤造成⽹络拥堵、⽹速太慢、Broker端处理太慢等都可能造成消息处理超时。

⼆是在SendCallback的对应⽅法被触发之前,⽣产者不能调⽤shutdown()⽅法。如果消息处理完之前,⽣产者线程就关闭了,⽣产者的SendCallback对应⽅法就不会触发。这是因为使⽤异步发送机制后,⽣产者虽然不⽤阻塞下来等待Broker端响应,但是SendCallback还是需要附属于⽣产者的主线程才能执⾏。如果Broker端还没有返回SendResult,⽽⽣产者主线程已经停⽌了,那么SendCallback的执⾏线程也就会随主线程⼀起停⽌,对应的⽅法⾃然也就⽆法执⾏了。

这种异步发送的机制能够⽐较好的兼容消息的安全性以及⽣产者的⾼吞吐需求,是很多MQ产品都⽀持的⽅式。RabbitMQ和Kafka都⽀持这种异步发送的机制。但是异步发送机制也并不是万能的,毕竟异步发送机制对消息⽣产者的主线业务是有侵⼊的。具体使⽤时还是需要根据业务场景考虑。

RocketMQ提供的这三种发送消息的⽅式,并不存在绝对的好坏之分。我们更多的是需要根据业务场景进⾏选择。例如在电商下单这个场景,我们就应该尽量选择同步发送或异步发送,优先保证数据安全。然后,如果下单场景的并发⽐较⾼,业务⽐较繁忙,就应该尽量优先选择异步发送的机制。这时,我们就应该对下单服务的业务进⾏优化定制,尽量适应异步发送机制的要求。这样就可以尽量保证下单服务能够⽐较可靠的将⽤户的订单消息发送到RocketMQ了。

2、消息消费者端采⽤状态确认机制保证消费者⼀定能正常处理对应的消息

我们之前分析⽣产者的可靠性问题,核⼼的解决思路就是通过确认Broker端的状态来保证⽣产者发送消息的可靠性。对于RocketMQ的消费者来说,保证消息处理可靠性的思路也是类似的。只不过这次换成了Broker等待消费者返回消息处理状态。

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;
    }
});

这个返回值是⼀个枚举值,有两个选项 CONSUME_SUCCESS和RECONSUME_LATER。如果消费者返回CONSUME_SUCCESS,那么消息⾃然就处理结束了。但是如果消费者没有处理成功,返回的是RECONSUME_LATER,Broker就会过⼀段时间再发起消息重试。

为了要兼容重试机制的成功率和性能,RocketMQ设计了⼀套⾮常完善的消息重试机制,从⽽尽可能保证消费者能够正常处理⽤户的订单信息。

1、Broker不可能⽆限制的向消费失败的消费者推送消息。如果消费者⼀直没有恢复,Broker显然不可能⼀直⽆限制的推送,这会浪费集群很多的性能。所以,Broker会记录每⼀个消息的重试次数。如果⼀个消息经过很多次重试后,消费者依然⽆法正常处理,那么Broker会将这个消息推⼊到消费者组对应的死信Topic中。死信Topic相当于windows当中的垃圾桶。你可以⼈⼯介⼊对死信Topic中的消息进⾏补救,也可以直接彻底删除这些消息。RocketMQ默认的最⼤重试次数是16次。

2、为了让这些重试的消息不会影响Topic下其他正常的消息,Broker会给每个消费者组设计对应的重试Topic。MessageQueue是⼀个具有严格FIFO特性的数据结构。如果需要重试的这些消息还是放在原来的MessageQueue中,就会对当前MessageQueue产⽣阻塞,让其他正常的消息⽆法处理。RocketMQ的做法是给每个消费者组⾃动⽣成⼀个对应的重试Topic。在消息需要重试时,会先移动到对应的重试Topic中。后续Broker只要从这些重试Topic中不断拿出消息,往消费者组重新推送即可。这样,这些重试的消息有了⾃⼰单独的队列,就不会影响到Topic下的其他消息了。

3、RocketMQ中设定的消费者组都是订阅主题和消费逻辑相同的服务备份,所以当消息重试时,Broker只要往消费者组中随意⼀个实例推送即可。这是消息重试机制能够正常运⾏的基础。但是,在客户端的具体实现时,MQDefaultMQConsumer并没有强制规定消费者组不能重复。也就是说,你完全可以实现出⼀些订阅主题和消费逻辑完全不同的消费者服务,共同组成⼀个消费组。在这种情况下,RocketMQ不会报错,但是消息的处理逻辑就⽆法保持⼀致了。这会给业务带来很⼤的麻烦。这是在实际应⽤时需要注意的地⽅。

4、Broker端最终只通过消费者组返回的状态来确定消息有没有处理成功。⾄于消费者组⾃⼰的业务执⾏是否正常,Broker端是没有办法知道的。因此,在实现消费者的业务逻辑时,应该要尽量使⽤同步实现⽅式,保证在⾃⼰业务处理完成之后再向Broker端返回状态。⽽应该尽量避免异步的⽅式处理业务逻辑。

3、消费者组也可以⾃⾏指定起始消费位点

Broker端通过Consumer返回的状态来推进所属消费者组对应的Offset。但是,这⾥还是会造成⼀种分裂,消息最终是由Consumer来处理,但是消息却是由Broker推送过来的,也就是说,Consumer⽆法确定⾃⼰将要处理的是哪些消息。这就好⽐你上班做⼀天事情,公司负责给你发⼀笔⼯资。如果⼀切正常,那么没什么问题。 但是如果出问题了呢?公司拖⽋了你的⼯资,这时,你就还是需要能到公司查账,⾄少查你⾃⼰的⼯资记录。从上⼀次发⼯资的时候计算你该拿的钱。

使⽤消息对列要如何解决这样的问题呢?这时,就可以创建另外⼀个新的消费者组,并通过ConsumerFromWhere属性指定这个消费者组的消费起点,从⽽让这个新的消费者组去消费之前发送过的历史消息。⽽这个ConsumerFromWhere属性并不是直接指定Offset的数值,因为客户端也不知道Broker端记录的Offset数值是多少。RocketMQ就提供了⼀个枚举值。名字⼀⽬了然。

public enum ConsumeFromWhere {
    CONSUME_FROM_LAST_OFFSET, //从对列的最后⼀条消息开始消费
    CONSUME_FROM_FIRST_OFFSET, //从对列的第⼀条消息开始消费
    CONSUME_FROM_TIMESTAMP; //从某⼀个时间点开始重新消费
}

另外,如果指定了ConsumerFromWhere.CONSUME_FROM_TIMESTAMP,这就表示要从⼀个具体的时间开始。具体时间点,需要通过Consumer的另⼀个属性ConsumerTimestamp。这个属性可以传⼊⼀个表示时间的字符串。

consumer.setConsumerTimestamp("20131223171201");

⼴播消息

应⽤场景:

⼴播模式和集群模式是RocketMQ的消费者端处理消息最基本的两种模式。集群模式下,⼀个消息,只会被⼀个消费者组中的多个消费者实例共同处理⼀次。⼴播模式下,⼀个消息,则会推送给所有消费者实例处理,不再关⼼消费者组。

示例代码:

消费者核⼼代码

consumer.setMessageModel(MessageModel.BROADCASTING);

启动多个消费者,⼴播模式下,这些消费者都会消费⼀次消息。

实现思路:

默认模式(也就是集群模式)下,Broker端会给每个ConsumerGroup维护⼀个统⼀的Offset,这样,当Consumer来拉取消息时,就可以通过Offset保证⼀个消息,在同⼀个ConsumerGroup内只会被消费⼀次。⽽⼴播模式的本质,是将Offset转移到Consumer端⾃⾏保管,包括Offset的记录以及更新,全都放到客户端。这样Broker推送消息时,就不再管ConsumerGroup,只要Consumer来拉取消息,就返回对应的消息。

注意点:

1、Broker端不维护消费进度,意味着,如果消费者处理消息失败了,将⽆法进⾏消息重试。

2、Consumer端维护Offset的作⽤是可以在服务重启时,按照上⼀次消费的进度,处理后⾯没有消费过的消息。如果Offset丢了,Consuer依然可以拉取消息。

⽐如⽣产者发送了1~10号消息。消费者当消费到第6个时宕机了。当他重启时,Broker端已经把第10个消息都推送完成了。如果消费者端维护好了⾃⼰的Offset,那么他就可以在服务重启时,重新向Broker申请6号到10号的消息。但是,如果消费者端的Offset丢失了,消费者服务依然可以正常运⾏,但是6到10号消息就⽆法再申请了。后续这个消费者就只能获取10号以后的消息。

过滤消息

应⽤场景:

同⼀个Topic下有多种不同的消息,消费者只希望关注某⼀类消息。

例如,某系统中给仓储系统分配⼀个Topic,在Topic下,会传递过来⼊库、出库等不同的消息,仓储系统的不同业务消费者就需要过滤出⾃⼰感兴趣的消息,进⾏不同的业务操作。

图片.png

示例代码1:简单过滤

⽣产者端需要在发送消息时,增加Tag属性。⽐如我们上⾯举例当中的⼊库、出库。核⼼代码:

String[] tags = new String[] {"TagA", "TagB", "TagC"};
for (int i = 0; i < 15; i++) {
    Message msg = new Message("TagFilterTest",
        tags[i % tags.length],
        "Hello world".getBytes(RemotingHelper.DEFAULT_CHARSET));
    SendResult sendResult = producer.send(msg);
    System.out.printf("%s%n", sendResult);
}

消费者端就可以通过这个Tag属性订阅⾃⼰感兴趣的内容。核⼼代码:

consumer.subscribe("TagFilterTest", "TagA");

这样,后续Consumer就只会出处理TagA的消息。

示例代码2:SQL过滤

通过Tag属性,只能进⾏简单的消息匹配。如果要进⾏更复杂的消息过滤,⽐如数字⽐较,模糊匹配等,就需要使⽤SQL过滤⽅式。SQL过滤⽅式可以通过Tag属性以及⽤户⾃定义的属性⼀起,以标准SQL的⽅式进⾏消息过滤。

⽣产者端在发送消息时,除了Tag属性外,还可以增加⾃定义属性。核⼼代码:

String[] tags = new String[] {"TagA", "TagB", "TagC"};
for (int i = 0; i < 15; i++) {
    Message msg = new Message("SqlFilterTest",
        tags[i % tags.length],
        ("Hello RocketMQ " + i).getBytes(RemotingHelper.DEFAULT_CHARSET));
    msg.putUserProperty("a", String.valueOf(i));
    SendResult sendResult = producer.send(msg);
    System.out.printf("%s%n", sendResult);
}

消费者端在进⾏过滤时,可以指定⼀个标准的SQL语句,定制复杂的过滤规则。核⼼代码:

consumer.subscribe("SqlFilterTest",
    MessageSelector.bySql("(TAGS is not null and TAGS in ('TagA', 'TagB'))" +
        "and (a is not null and a between 0 and 3)"));

注意:如果需要使⽤⾃定义参数进⾏过滤,需要在Broker端,将参数enablePropertyFilter设置成true。这个参数默认是false。

实现思路:

实际上,Tags和⽤户⾃定义的属性,都是随着消息⼀起传递的,所以,消费者端是可以拿到消息的Tags和⾃定义属性的。⽐如:

consumer.registerMessageListener(new MessageListenerConcurrently() {
    @Override
    public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs,
    ConsumeConcurrentlyContext context) {
    for (MessageExt msg : msgs) {
        System.out.println(msg.getTags());
        System.out.println(msg.getProperties());
    }
    System.out.printf("%s Receive New Messages: %s %n", Thread.currentThread().getName(), msgs);
    return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
    }
});

这样,剩下的就是在Consumer中对消息进⾏过滤了。Broker会在往Consumer推送消息时,在Broker端进⾏消息过滤。是Consumer感兴趣的消息,就往Consumer推送。

Tag属性的处理⽐较简单,就是直接匹配。⽽SQL语句的处理会⽐较麻烦⼀点。RocketMQ也是通过ANLTR引擎来解析SQL语句,然后再进⾏消息过滤的。

ANLTR是⼀个开源的SQL语句解析框架。很多开源产品都在使⽤ANLTR来解析SQL语句。⽐如ShardingSphere,Flink等。

注意点:

1、使⽤Tag过滤时,如果希望匹配多个Tag,可以使⽤两个竖线(||)连接多个Tag值。另外,也可以使⽤星号(*)匹配所有。

2、使⽤SQL顾虑时,SQL语句是按照SQL92标准来执⾏的。SQL语句中⽀持⼀些常⻅的基本操作:

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

2、消息过滤,其实在Broker端和在Consumer端都可以做。Consumer端也可以⾃⾏获取⽤户属性,不感兴趣的消息,直接返回不成功的状态,跳过该消息就⾏了。但是RocketMQ会在Broker端完成过滤条件的判断,只将Consumer感兴趣的消息推送给Consumer。这样的好处是减少了不必要的⽹络IO,但是缺点是加⼤了服务端的压⼒。不过在RocketMQ的良好设计下,更建议使⽤消息过滤机制。

3、Consumer不感兴趣的消息并不表示直接丢弃。通常是需要在同⼀个消费者组,定制另外的消费者实例,消费那些剩下的消息。但是,如果⼀直没有另外的Consumer,那么,Broker端还是会推进Offset。

顺序消息机制

应⽤场景:

每⼀个订单有从下单、锁库存、⽀付、下物流等⼏个业务步骤。每个业务步骤都由⼀个消息⽣产者通知给下游服务。如何保证对每个订单的业务处理顺序不乱?

示例代码:

⽣产者核⼼代码:

for (int i = 0; i < 10; i++) {
    int orderId = i;
    for(int j = 0 ; j <= 5 ; j ++){
        Message msg =
            new Message("OrderTopicTest", "order_"+orderId, "KEY" + orderId,
                ("order_"+orderId+" step " + j).getBytes(RemotingHelper.DEFAULT_CHARSET));
        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);
    }
}

通过MessageSelector,将orderId相同的消息,都转发到同⼀个MessageQueue中。

消费者核⼼代码:

consumer.registerMessageListener(new MessageListenerOrderly() {
    @Override
    public ConsumeOrderlyStatus consumeMessage(List<MessageExt> msgs, ConsumeOrderlyContext context) {
    context.setAutoCommit(true);
    for(MessageExt msg:msgs){
        System.out.println("收到消息内容 "+new String(msg.getBody()));
    }
        return ConsumeOrderlyStatus.SUCCESS;
    }
});

注⼊⼀个MessageListenerOrderly实现。

实现思路:

RocketMQ实现消息顺序消费,是需要⽣产者和消费者配合才能实现的。

图片.png

1、⽣产者只有将⼀批有顺序要求的消息,放到同⼀个MesasgeQueue上,通过MessageQueue的FIFO特性保证这⼀批消息的顺序。如果不指定MessageSelector对象,那么⽣产者会采⽤轮询的⽅式将多条消息依次发送到不同的MessageQueue上。

2、消费者需要实现MessageListenerOrderly接⼝,实际上在服务端,处理MessageListenerOrderly时,会给⼀个MessageQueue加锁,拿到MessageQueue上所有的消息,然后再去读取下⼀个MessageQueue的消息。

注意点:

1、理解局部有序与全局有序。⼤部分业务场景下,我们需要的其实是局部有序。如果要保持全局有序,那就只保留⼀个MessageQueue。性能显然⾮常低。2、⽣产者端尽可能将有序消息打散到不同的MessageQueue上,避免过于集中导致数据热点竞争。

3、消费者端只进⾏有限次数的重试。如果⼀条消息处理失败,RocketMQ会将后续消息阻塞住,让消费者进⾏重试。但是,如果消费者⼀直处理失败,超过最⼤重试次数,那么RocketMQ就会跳过这⼀条消息,处理后⾯的消息,这会造成消息乱序。

4、消费者端如果确实处理逻辑中出现问题,不建议抛出异常,可以返回ConsumeOrderlyStatus.SUSPEND_CURRENT_QUEUE_A_MOMENT作为替代。