RocketMQ高级使用---中

在这里插入图片描述

一.先来一个车辆调度的案例分析

1.业务分析

1.车辆调度分析图
在这里插入图片描述
用户打车从派单服务到调度服务,首先将消息以顺序方式扔到RocketMQ中,然后消费的事务就会严格按照放入的顺序进行消费,用户首先拿到从RocketMQ推送的顺序消息,然后保持住,开始轮询检查Redis中的List中是否存在车辆,存在两种情况

  1. 没有拉取到车辆:如果没有拉取到车辆,然后会延时一段时间,继续进行拉取,一直拉取不到的话一直进行自旋,一直等到拿到车辆才退出自旋
  2. 拉取到车辆:如果拉取车辆就会将用户和拿到的车辆绑定到一起,开始后续操作,比如下订单等

2.司机自动接单
在这里插入图片描述
当司机上线后,开启自动接单后,主题流程如下:

  1. 会先将车辆状态设置为在Ready状态
  2. 当车辆接到用户后会将车辆设置为Running状态
  3. 用户下车后,会将车辆继续设置为Ready状态,并将车辆push进list

3.用户下车

在这里插入图片描述
如果用户点击下车,主体流程如下:

  1. 会先将用户状态设置为Stop状态
  2. 然后会解除车辆和用户的绑定
  3. 之后车辆会将会push到list的尾端,让其他的用户可以拉取到车辆信息

4.用户打车
在这里插入图片描述
用户上车后流程如下:

  1. 校验用户状态,然后将发送顺序消息到RabbitMQ
  2. 消费者获取到用户消息,开始轮询来拉取车辆信息,如果拉取不到休眠一会继续拉取,一直到拉取到
  3. 拉取到后校验是否超时,如果超时直接结束打车,否则删除RabbitMQ的超时检测Key,失效超时通知
  4. 设置用户状态为Running,后续就到了司机自动接单的流程了

2.技术分析

1.RocketMQ顺序消息

打车需要排队,我们需要让前面的人能够被消费到,不能让这个顺序乱掉,这就需要用到RocketMQ的顺序消息

2.Redis 轮询队列

我们要让车辆在队列中,从MQ拿到一个车辆后,需要再从队列中拿取一个车辆如果拿不到则需要不断的轮询,一直到拿到车辆为止,如果打车玩完成还是需要将车辆归还队列,让其他的用户来打车,将一辆车重复利用起来

二.顺序消息

1.顺序类型

1.无序消息:无序消息也指普通的消息,Producer 只管发送消息,Consumer 只管接收消息,至于消息和消息之间的顺序并没有保证,例如:

  • Producer 依次发送 orderId 为 1、2、3 的消息
  • Consumer 接到的消息顺序有可能是 1、2、3,也有可能是 2、1、3 等情况,这就是普通消息

2.全局顺序:对于指定的一个 Topic,所有消息按照严格的先入先出(FIFO)的顺序进行发布和消费

比如 Producer 发送orderId 1,3,2 的消息, 那么 Consumer 也必须要按照 1,3,2 的顺序进行消费
在这里插入图片描述

3.局部顺序:在实际开发有些场景中,我并不需要消息完全按照完全按的先进先出,而是某些消息保证先进先出就可以了

案例:就好比一个打车涉及到不同地区 北京,上海、广州、深圳,我不用管其它的订单,只保证同一个地区的订单ID能保证这个顺序就可以了
在这里插入图片描述

2.Rocket顺序消息

RocketMQ可以严格的保证消息有序,但这个顺序,不是全局顺序,只是分区(queue)顺序,要全局顺序只能一个分区

1.提出以下问题:

  1. 我们知道生产的message最终会存放在queue中,如果一个Topic关联了4个queue,如果我们不指定消息往哪个队列里放,那么默认是平均分配消息到4个queue
  2. 好比有10条消息,那么这10条消息会平均分配在这4个Queue上,那么每个queue大概放2个左右,这里有一点很重的是:同一个queue,存储在里面的message 是按照先进先出的原则

在这里插入图片描述
之所以出现上面这个场景看起来不是顺序的,是因为发送消息的时候,消息发送默认是会采用轮询的方式发送到不同的queue

2.解决方案

我们让不同的地区用不同的queue,只要保证同一个地区的订单把他们放到同一个queue那就保证消费者先进先出了

在这里插入图片描述
这就保证局部顺序了,即同一订单按照先后顺序放到同一Queue,那么获取消息的时候就可以保证先进先出

3.再次提出问题(如何保证集群有序):

  1. 这里还有很关键的一点,在一个消费者集群的情况下,消费者1先去queue拿消息,它拿到了 北京订单1,它拿完后,消费者2去queue拿到的是 北京订单2
  2. 拿的顺序是没毛病了,但关键是先拿到不代表先消费完它,会存在虽然你消费者1先拿到北京订单1,但由于网络等原因,消费者2比你真正的先消费消息,这样就会有问题了

4.解决方法(分布式锁):

Rocker采用的是分段锁,它不是锁整个Broker而是锁里面的单个queue,因为只要锁单个queue就可以保证局部顺序消费了

所以最终的消费者这边的逻辑就是

  1. 消费者1去queue拿 北京订单1,它就锁住了整个queue,只有它消费完成并返回成功后,这个锁才会释放
  2. 然后下一个消费者去拿到 北京订单2 同样锁住当前queue,这样的一个过程来真正保证对同一个Queue能够真正意义上的顺序消费,而不仅仅是顺序取出

5.消息类型对比

全局顺序与分区顺序对比

Topic消息类型 支持事务消息 支持定时/延时消息 性能
无序消息(普通、事务、定时/延时) 最高
分区顺序消息
全局顺序消息 一般

发送方式对比

Topic消息类型 支持可靠同步发送 支持可靠异步发送 支持Oneway发送
无序消息(普通、事务、定时/延时)
分区顺序消息
全局顺序消息

6.注意事项

  1. 顺序消息暂不支持广播模式
  2. 顺序消息不支持异步发送方式,否则将无法严格保证顺序
  3. 建议同一个 Group ID 只对应一种类型的 Topic
  4. 对于全局顺序消息,通过一个Topic只有一个队列来保证顺序

3.代码示例

1.生产者基本代码

public class OrderProducer {
    
    
    public static void main(String[] args) throws Exception {
    
    
        // 创建一个消息生产者,并设置一个消息生产者组
        DefaultMQProducer producer = new DefaultMQProducer();
        // 指定 NameServer 地址
        producer.setNamesrvAddr("192.168.44.128:9876;192.168.44.128:9877");
        // 指定组名
        producer.setProducerGroup("order_producer_group");
        // 初始化 Producer,整个应用生命周期内只需要初始化一次
        producer.start();
        for (int i = 0; i < 10; i++) {
    
    
            // 创建一条消息对象,指定其主题、标签和消息内容
            Message msg = new Message();
            msg.setTopic("rocket_order_test_1");
            msg.setTags("order_tag" + i);
            msg.setBody(("第"+ i + "条消息").getBytes(RemotingHelper.DEFAULT_CHARSET));
            //发送消息并返回结果
            SendResult sendResult = producer.send(msg);
            System.out.printf("%s%n", sendResult);
        }
        // 一旦生产者实例不再被使用则将其关闭,包括清理资源,关闭网络连接等
        producer.shutdown();
    }
}

上面的代码发送消息不是有序的
在这里插入图片描述
2.如果我们要发送局部有序改怎么办呢?这时我们可以修改第18行代码

SendResult sendResult = producer.send(msg,new SelectMessageQueueByHash(),i % 4);

3.跟踪send()方法源码如下:
在这里插入图片描述
4.在次运行代码结果如下
在这里插入图片描述

这时我们可以看到同一个队列,放进了同一个broker中了

5.为什么用new SelectMessageQueueByHash()就可以同一个队列放到同一个broker中呢?再次跟进源码
在这里插入图片描述

如果我们需要自定义可以直接实现MessageQueueSelector接口就可以了

7.消费者代码:

public class OrderConsumer {
    
    
    public static void main(String[] args) throws MQClientException {
    
    
        // 创建一个消息消费者,并设置一个消息消费者组
        DefaultMQPushConsumer consumer = new DefaultMQPushConsumer();
        consumer.setConsumerGroup("order_consumer_group");
        // 指定 NameServer 地址
        consumer.setNamesrvAddr("192.168.44.128:9876;192.168.44.128:9877");
        // 订阅指定 Topic 下的所有消息
        consumer.subscribe("rocket_order_test_1", "*");
        // 注册消息监听器
        consumer.setConsumeMessageBatchMaxSize(10);
        consumer.registerMessageListener(new MessageListenerOrderly() {
    
    
            @Override
            public ConsumeOrderlyStatus consumeMessage(List<MessageExt> msgs, ConsumeOrderlyContext context) {
    
    
                System.out.println(msgs);
                for (MessageExt msg : msgs) {
    
    
                    try {
    
    
                        String s = new String(msg.getBody(), RemotingHelper.DEFAULT_CHARSET);
                        System.out.println(s);
                    } catch (UnsupportedEncodingException e) {
    
    
                        throw new RuntimeException(e);
                    }
                }
                return ConsumeOrderlyStatus.SUCCESS;
            }
        });
        // 消费者对象在使用之前必需要调用 start 初始化
        consumer.start();
        System.out.println("消息消费者已启动");
    }
}

三.消息投递策略

1.先上图(RocketMQ 的消息模型整体图):
在这里插入图片描述
一个Topic(消息主题)可能对应多个实际的消息队列(MessgeQueue):在底层实现上,为了提高MQ的可用性和灵活性,一个Topic在实际存储的过程中,采用了多队列的方式,具体形式如上图所示,每个消息队列在使用中应当保证先入先出(FIFO,First In First Out)的方式进行消费

那么,基于这种模型,就会引申出两个问题

  1. 生产者: 在发送相同Topic的消息时,消息体应当被放置到哪一个消息队列(MessageQueue)中?
  2. 消费者: 在消费消息时,应当从哪些消息队列中拉取消息?

1.生产者投递策略

生产者投递策略就是讲如何将一个消息投递到不同的queue中

1.轮询算法投递:默认投递方式(基于Queue队列轮询算法投递)

2.顺序投递策略:在有些场景下,需要保证同类型消息投递和消费的顺序性,例如下面案例:

例如,假设现在有TOPIC topicTest,该 Topic下有4个Queue队列,该Topic用于传递订单的状态变迁,假设订单有状态:未支付、已支付、发货中(处理中)、发货成功、发货失败。

在时序上,生产者从时序上可以生成如下几个消息:

订单T0000001:未支付 --> 订单T0000001:已支付 --> 订单T0000001:发货中(处理中) --> 订单T0000001:发货失败

消息发送到MQ中之后,可能由于轮询投递的原因,消息在MQ的存储可能如下图:
在这里插入图片描述
这种情况下,我们希望消费者消费消息的顺序和我们发送是一致的, 然而,有上述MQ的投递和消费机制,我们无法保证顺序是正确的,对于顺序异常的消息,消费者 即使有一定的状态容错,也不能完全处理好这么多种随机出现组合情况

基于上述的情况,RockeMQ采用了这种实现方案:对于相同订单号的消息,通过一定的策略,将其放置在一个 queue队列中,然后消费者再采用一定的策略(一个线程独立处理一个queue,保证处理消息的顺序性),能够保证消费的顺序性

在这里插入图片描述

生产者在消息投递的过程中,使用了 MessageQueueSelector 作为队列选择的策略接口,其定义如下:

public interface MessageQueueSelector {
    
    
     /**
       * 根据消息体和参数,从一批消息队列中挑选出一个合适的消息队列
       * @param mqs  待选择的MQ队列选择列表
       * @param msg  待发送的消息体
       * @param arg  附加参数
       * @return  选择后的队列
       */
      MessageQueue select(final List<MessageQueue> mqs, final Message msg, final Object arg);
}

3.自带实现类

投递策略 策略实现类 说明
随机分配策略 SelectMessageQueueByRandom 使用了简单的随机数选择算法
基于Hash分配策略 SelectMessageQueueByHash 根据附加参数的Hash值,按照消息队列列表的大小取余数,得到消息队列的index
基于机器机房位置分配策略 SelectMessageQueueByMachineRoom 开源的版本没有具体的实现,基本的目的应该是机器的就近原则分配

2.消费者分配队列

1.RocketMQ对于消费者消费消息有两种形式

  1. BROADCASTING:广播式消费,这种模式下,一个消息会被通知到每一个消费者
  2. CLUSTERING: 集群式消费,这种模式下,一个消息最多只会被投递到一个消费者上进行消费模式如下
    在这里插入图片描述
    对于使用了消费模式为MessageModel.CLUSTERING进行消费时,需要保证一个消息在整个集群中只需要被消费一次,实际上,在RoketMQ底层,消息指定分配给消费者的实现,是通过queue队列分配给消费者的方式完成的:也就是说,消息分配的单位是消息所在的queue队列

将queue队列指定给特定的消费者后,queue队列内的所有消息将会被指定到消费者进行消费

RocketMQ定义了策略接口AllocateMessageQueueStrategy,对于给定的消费者分组,和消息队列列表、消费者列表,当前消费者应当被分配到哪些queue队列,定义如下:

/**
 * 为消费者分配queue的策略算法接口
 */
public interface AllocateMessageQueueStrategy {
    
    

    /**
     * Allocating by consumer id
     *
     * @param consumerGroup 当前 consumer群组
     * @param currentCID 当前consumer id
     * @param mqAll 当前topic的所有queue实例引用
     * @param cidAll 当前 consumer群组下所有的consumer id set集合
     * @return 根据策略给当前consumer分配的queue列表
     */
    List<MessageQueue> allocate(
        final String consumerGroup,
        final String currentCID,
        final List<MessageQueue> mqAll,
        final List<String> cidAll
    );

    /**
     * 算法名称
     *
     * @return The strategy name
     */
    String getName();
}

相应地,RocketMQ提供了如下几种实现:

算法名称 含义
AllocateMessageQueueAveragely 平均分配算法
AllocateMessageQueueAveragelyByCircle 基于环形平均分配算法
AllocateMachineRoomNearby 基于机房临近原则算法
AllocateMessageQueueByMachineRoom 基于机房分配算法
AllocateMessageQueueConsistentHash 基于一致性hash算法
AllocateMessageQueueByConfig 基于配置分配算法

2.平均分配算法:这里所谓的平均分配算法,并不是指的严格意义上的完全平均,如上面的例子中,10个queue,而消费者只有4个,无法是整除关系,除了整除之外的多出来的queue,将依次根据消费者的顺序均摊

按照上述例子来看,10/4=2,即表示每个消费者平均均摊2个queue;而10%4=2,即除了均摊之外,多出来2个queue还没有分配,那么,根据消费者的顺序consumer-1、consumer-2、consumer-3、consumer-4,则多出来的2个queue将分别给consumer-1和consumer-2。最终效果图如下:
在这里插入图片描述
3.环形平均分配:环形平均算法,是指根据消费者的顺序,依次在由queue队列组成的环形图中逐个分配

.cn/9730577e3019419cb93b4fbe1715a19d.png)

这种算法最终分配的结果是:
consumer-1: #0,#4,#8
consumer-2: #1, #5, # 9
consumer-3: #2,#6
consumer-4: #3,#7

四.RocketMQ消息保障

1.生产端保障

生产端保障需要从一下几个方面来保障

  1. 使用可靠的消息发送方式
  2. 注意生产端重试
  3. 生产禁止自动创建topic

1.消息发送保障

  1. 同步发送:同步发送就是指 producer 发送消息后,会在接收到 broker 响应后才继续发下一条消息的通信方式
    在这里插入图片描述

使用场景:

由于这种同步发送的方式确保了消息的可靠性,同时也能及时得到消息发送的结果,故而适合一些发送比较重要的消息场景,比如说重要的通知邮件、营销短信等等,在实际应用中,这种同步发送的方式还是用得比较多的

注意事项:

这种方式具有内部重试机制,即在主动声明本次消息发送失败之前,内部实现将重试一定次数,默认为2次(DefaultMQProducer#getRetryTimesWhenSendFailed),发送的结果存在同一个消息可能被多次发送给broker,这里需要应用的开发者自己在消费端处理幂等性问题

  1. 异步发送:异步发送是指 producer 发出一条消息后,不需要等待 broker 响应,就接着发送下一条消息的通信方式,需要注意的是,不等待 broker 响应,并不意味着 broker 不响应,而是通过回调接口来接收 broker 的响应,所以要记住一点,异步发送同样可以对消息的响应结果进行处理
    在这里插入图片描述

使用场景:

由于异步发送不需要等待 broker 的响应,故在一些比较注重 RT(响应时间)的场景就会比较适用,比如,在一些视频上传的场景,我们知道视频上传之后需要进行转码,如果使用同步发送的方式来通知启动转码服务,那么就需要等待转码完成才能发回转码结果的响应,由于转码时间往往较长,很容易造成响应超时,此时,如果使用的是异步发送通知转码服务,那么就可以等转码完成后,再通过回调接口来接收转码结果的响应了

2.消息发送总结

  1. 发送方式对比
发送方式 发送 TPS 发送结果反馈 可靠性 适用场景
同步发送 一般 不丢失 重要的通知场景
异步发送 不丢失 比较注重 RT(响应时间)的场景
单向发送 最快 可能丢失 可靠性要求并不高的场景
  1. 使用场景对比

在实际使用场景中,利用何种发送方式,可以总结如下:
1.当发送的消息不重要时,采用one-way方式,以提高吞吐量
2.当发送的消息很重要是,且对响应时间不敏感的时候采用sync方式
3.当发送的消息很重要,且对响应时间非常敏感的时候采用async方式

3.发送状态

发送消息时,将获得包含SendStatus的SendResult,首先,我们假设Message的isWaitStoreMsgOK = true(默认为true),如果没有抛出异常,我们将始终获得SEND_OK,以下是每个状态的说明如下

  1. FLUSH_DISK_TIMEOUT:如果设置了 FlushDiskType=SYNC_FLUSH (默认是 ASYNC_FLUSH),并且 Broker 没有在 syncFlushTimeout (默认是 5 秒)设置的时间内完成刷盘,就会收到此状态码
  2. FLUSH_SLAVE_TIMEOUT:如果设置为 SYNC_MASTER,并且 slave Broker 没有在 syncFlushTimeout 设定时间内完成同步,就会收到此状态码
  3. SLAVE_NOT_AVAILABLE:如果设置为 SYNC_MASTER,并没有配置 slave Broker,就会收到此状态码
  4. SEND_OK:这个状态可以简单理解为,没有发生上面列出的三个问题状态就是SEND_OK,需要注意的是,SEND_OK 并不意味着可靠,如果想严格确保没有消息丢失,需要开启 SYNC_MASTER or SYNC_FLUSH
  5. 注意事项:如果收到了 FLUSH_DISK_TIMEOUT, FLUSH_SLAVE_TIMEOUT,意味着消息会丢失,有2个选择,一是无所谓,适用于消息不关紧要的场景,二是重发,但可能产生消息重复,这就需要consumer进行去重控制,如果收到了 SLAVE_NOT_AVAILABLE 就要赶紧通知管理员了

4.MQ发送端重试保障:如果由于网络抖动等原因,Producer程序向Broker发送消息时没有成功,即发送端没有收到Broker的ACK,导致最终Consumer无法消费消息,此时RocketMQ会自动进行重试

DefaultMQProducer可以设置消息发送失败的最大重试次数,并可以结合发送的超时时间来进行重试的处理,具体API如下:

//设置消息发送失败时的最大重试次数
public void setRetryTimesWhenSendFailed(int retryTimesWhenSendFailed) {
    
    
   this.retryTimesWhenSendFailed = retryTimesWhenSendFailed;
}
 
//同步发送消息,并指定超时时间
public SendResult send(Message msg,
                      long timeout) throws MQClientException, RemotingException, MQBrokerException, InterruptedException {
    
    
   return this.defaultMQProducerImpl.send(msg, timeout);
  1. 重试解惑
网上很多地方说的超时异常会重试的说法都是错误的:

是因为下面测试代码的超时时间设置为5毫秒 ,按照正常肯定会报超时异常,但设置1次重试和3000次的重试,虽然最终都会报下面异常,但输出错误时间报显然不应该是一个级别,但测试发现无论设置的多少次的重试次数,报异常的时间都差不多

  1. 测试代码
public class RetryProducer {
    
    
    public static void main(String[] args) throws UnsupportedEncodingException, InterruptedException, RemotingException, MQClientException, MQBrokerException {
    
    
        //创建一个消息生产者,并设置一个消息生产者组
        DefaultMQProducer producer = new DefaultMQProducer("rocket_test_consumer_group");

        //指定 NameServer 地址
        producer.setNamesrvAddr("127.0.0.1:9876");
        //设置重试次数(默认2次)
        producer.setRetryTimesWhenSendFailed(300000);
        //初始化 Producer,整个应用生命周期内只需要初始化一次
        producer.start();
        Message msg = new Message(
                /* 消息主题名 */
                "topicTest",
                /* 消息标签 */
                "TagA",
                /* 消息内容 */
                ("Hello Java demo RocketMQ ").getBytes(RemotingHelper.DEFAULT_CHARSET));
        //发送消息并返回结果,设置超时时间 5ms 所以每次都会发送失败
        SendResult sendResult = producer.send(msg, 5);

        System.out.printf("%s%n", sendResult);
        // 一旦生产者实例不再被使用则将其关闭,包括清理资源,关闭网络连接等
        producer.shutdown();
    }
}
  1. 重试总结:
  1. 如果是异步发送默认重试次数是两次,通过递归的方式进行重试
  2. 对于同步而言,超时异常也是不会再去重试
  3. 同步发送重试是在一个for 循环里去重试,所以它是立即重试而不是隔一段时间去重试

5.禁止自动创建topic

  • .自动创建TOPIC流程
    • 消息发送时如果根据topic没有获取到路由信息,则会根据默认的Topic(TBW102)去获取,获取到路由信息后选择一个队列进行发送,发送时报文会带上默认的topic以及默认的队列数量
    • 消息到达broker后,broker检测没有topic的路由信息,则查找默认topic的路由信息,查到表示开启了自动创建topic,则会根据消息内容中的默认的队列数量在本broker上创建topic,然后进行消息存储
    • broker创建topic后并不会马上同步给namesrv,而是每30进行汇报一次,更新namesrv上的topic路由信息,producer会每30s进行拉取一次topic的路由信息,更新完成后就可以正常发送消息,更新之前一直都是按照默认的topic查找路由信息
  • 为什么不能开启自动创建
    • 上述 broker 中流程会有一个问题,就是在producer更新路由信息之前的这段时间,如果消息只发送到了broker-a,则broker-b上不会创建这个topic的路由信息,broker互相之间不通信,当producer更新之后,获取到的broker列表只有broker-a,就永远不会轮询到broker-b的队列(因为没有路由信息),所以我们生产通常关闭自动创建broker,而是采用手动创建的方式

6.发端端规避

1.注意了,这里我们发现,有可能在实际的生产过程中,我们的 RocketMQ 有几台服务器构成的集群
2.其中有可能是一个主题 TopicA 中的 4 个队列存储在 Broker1、Broker2、Broker3 服务器上

在这里插入图片描述

如果这个时候 Broker2 挂了,我们知道,但是生产者不知道(因为生产者客户端每隔 30S 更新一次路由,但是 NamServer 与 Broker 之间的心跳检测间隔是 10S,所以生产者最快也需要 30S 才能感知 Broker2 挂了),所以发送到 queue2 的消息会失败,RocketMQ 发现这次消息发送失败后,就会将 Broker2排除在消息的选择范围,下次再次发送消息时就不会发送到 Broker2,这样做的目的就是为了提高发送消息的成功率

  1. 问题梳理:例如在发送之前 sendWhichQueue 该值为 broker-a 的 q1,如果由于此时 broker-a 的突发流量异常大导致消息发送失败,会触发重试,按照轮循机制,下一个选择的队列为 broker-a 的 q2 队列,此次消息发送大概率还是会失败,即尽管会重试 2 次,但都是发送给同一个 Broker 处理,此过程会显得不那么靠谱,即大概率还是会失败,那这样重试的意义将大打折扣。故 RocketMQ 为了解决该问题,引入了故障规避机制,在消息重试的时候,会尽量规避上一次发送的 Broker,回到上述示例,当消息发往 broker-a q1 队列时返回发送失败,那重试的时候,会先排除 broker-a 中所有队列,即这次会选择 broker-b q1 队列,增大消息发送的成功率。
  2. 规则策略:但 RocketMQ 提供了两种规避策略,该参数由 sendLatencyFaultEnable 控制,用户可干预,表示是否开启延迟规避机制,默认为不开启。(DefaultMQProducer中设置这两个参数)
  • sendLatencyFaultEnable 设置为 false:默认值,不开启,延迟规避策略只在重试时生效,例如在一次消息发送过程中如果遇到消息发送失败,规避 broekr-a,但是在下一次消息发送时,即再次调用 DefaultMQProducer 的 send 方法发送消息时,还是会选择 broker-a 的消息进行发送,只要继续发送失败后,重试时再次规避 broker-a
  • sendLatencyFaultEnable 设置为 true:开启延迟规避机制,一旦消息发送失败会将 broker-a “悲观”地认为在接下来的一段时间内该 Broker 不可用,在为未来某一段时间内所有的客户端不会向该 Broker 发送消息,这个延迟时间就是通过 notAvailableDuration、latencyMax 共同计算的,就首先先计算本次消息发送失败所耗的时延,然后对应 latencyMax 中哪个区间,即计算在 latencyMax 的下标,然后返回 notAvailableDuration 同一个下标对应的延迟值
  1. 注意事项:如果所有的 Broker 都触发了故障规避,并且 Broker 只是那一瞬间压力大,那岂不是明明存在可用的 Broker,但经过你这样规避,反倒是没有 Broker 可用了,那岂不是更糟糕了?针对这个问题,会退化到队列轮循机制,即不考虑故障规避这个因素,按自然顺序进行选择进行兜底

2.消费端保障

1.注意幂等性:同一个消息被消费多次和消费一次的结果一样

  • 至少一次送达:的消息交付策略,和消息重复消费是一对共生的因果关系,要做到不丢消息就无法避免消息重复消费,原因很简单,试想一下这样的场景:客户端接收到消息并完成了消费,在消费确认过程中发生了通讯错误,从Broker的角度是无法得知客户端是在接收消息过程中出错还是在消费确认过程中出错,为了确保不丢消息,重发消息是唯一的选择
  • 既然重发消息不可避免,那么业务人员就必须自己设计消息幂等性。有了消息幂等消费约定的基础,即解决了业务问题,也给RocketMQ带来了好处。RocketMQ能够有针对性地采取一些性能优化措施,例如:并行消费、消费进度同步机制等,这也是RocketMQ性能优异的原因之一

2.消费组消费模式:从不同的维度划分,Consumer支持以下消费模式

  • 广播消费模式下:消息消费失败不会进行重试,消费进度保存在Consumer端
  • 集群消费模式下:消息消费失败有机会进行重试,消费进度集中保存在Broker端
  1. 集群消费

使用相同 Group ID 的订阅者属于同一个集群,同一个集群下的订阅者消费逻辑必须完全一致(包括 Tag 的使用),这些订阅者在逻辑上可以认为是一个消费节点

在这里插入图片描述
注意事项:

1.消费端集群化部署, 每条消息只需要被处理一次。
2.由于消费进度在服务端维护, 可靠性更高。
3.集群消费模式下,每一条消息都只会被分发到一台机器上处理。如果需要被集群下的每一台机器都处理,请使用广播模式。
4.集群消费模式下,不保证每一次失败重投的消息路由到同一台机器上,因此处理消息时不应该做任何确定性假设。

  1. 广播消费

广播消费指的是:一条消息被多个consumer消费,即使这些consumer属于同一个ConsumerGroup,消息也会被ConsumerGroup中的每个Consumer都消费一次,广播消费中ConsumerGroup概念可以认为在消息划分方面无意义

在这里插入图片描述

注意事项:

1.广播消费模式下不支持顺序消息
2.广播消费模式下不支持重置消费位点
3.每条消息都需要被相同逻辑的多台机器处理
4.消费进度在客户端维护,出现重复的概率稍大于集群模式
5.广播模式下,消息队列 RocketMQ 保证每条消息至少被每台客户端消费一次,但是6.并不会对消费失败的消息进行失败重投,因此业务方需要关注消费失败的情况
7.广播模式下,客户端每一次重启都会从最新消息消费。客户端在被停止期间发送至服务端的消息将会被自动跳过, 请谨慎选择
8.广播模式下,每条消息都会被大量的客户端重复处理,因此推荐尽可能使用集群模式。
9.目前仅 Java 客户端支持广播模式
10.广播模式下服务端不维护消费进度,所以消息队列 RocketMQ 控制台不支持消息堆积查询、消息堆积报警和订阅关系查询功能

  1. 集群模式模拟广播

如果业务需要使用广播模式,也可以创建多个 Group ID,用于订阅同一个 Topic

  • 推消息模式下:消费进度的递增是由RocketMQ内部自动维护的;
  • 拉消息模式下:消费进度的变更需要上层应用自己负责维护,RocketMQ只提供消费进度保存和查询功能

推模式(PUSH)

由消息中间件(MQ消息服务器代理)主动地将消息推送给消费者;采用Push方式,可以尽可能实时地将消息发送给消费者进行消费。但是,在消费者的处理消息的能力较弱的时候(比如,消费者端的业务系统处理一条消息的流程比较复杂,其中的调用链路比较多导致消费时间比较久。概括起来地说就是“慢消费问题”),而MQ不断地向消费者Push消息,消费者端的缓冲区可能会溢出,导致异常

拉模式(PULL)

由消费者客户端主动向消息中间件(MQ消息服务器代理)拉取消息;采用Pull方式,如何设置Pull消息的频率需要重点去考虑,举个例子来说,可能1分钟内连续来了1000条消息,然后2小时内没有新消息产生(概括起来说就是“消息延迟与忙等待”)。如果每次Pull的时间间隔比较久,会增加消息的延迟,即消息到达消费者的时间加长,MQ中消息的堆积量变大;若每次Pull的时间间隔较短,但是在一段时间内MQ中并没有任何消息可以消费,那么会产生很多无效的Pull请求的RPC开销,影响MQ整体的网络性能

  1. 消息确认机制
  • 确认消费:业务实现消费回调的时候,当且仅当此回调函数返回ConsumeConcurrentlyStatus.CONSUME_SUCCESS,RocketMQ才会认为这批消息(默认是1条)是消费完成的
  • 消费异常:如果这时候消息消费失败,例如数据库异常,余额不足扣款失败等一切业务认为消息需要重试的场景,只要返回ConsumeConcurrentlyStatus.RECONSUME_LATER,RocketMQ就会认为这批消息消费失败了
  1. 消息重试机制
  • 顺序消息的重试:对于顺序消息,当消费者消费消息失败后,RocketMQ消息队列会自动不断地进行消息重试(每次间隔时间为1秒),这时,应用会出现消息消费被阻塞的情况,因此,建议您使用顺序消息时,务必保证应用能够及时监控并处理消费失败的情况,避免阻塞现象的发生
  • 无序消息的重试:无序消息的重试只针对集群消费方式生效;广播方式不提供失败重试特性,即消费失败后,失败消息不再重试,继续消费新的消息
  • 重试次数:RocketMQ消息队列默认允许每条消息最多重试16次,每次重试的间隔时间如下:
第几次重试 与上次重试的间隔时间 第几次重试 与上次重试的间隔时间
1 10秒 9 7分钟
2 30秒 10 8分钟
3 1分钟 11 9分钟
4 2分钟 12 10分钟
5 3分钟 13 20分钟
6 4分钟 14 30分钟
7 5分钟 15 1小时
8 6分钟 16 2小时
  • 如果消息重试16次后仍然失败,消息将不再投递。如果严格按照上述重试时间间隔计算,某条消息在一直消费失败的前提下,将会在接下来的4小时46分钟之内进行16次重试,超过这个时间范围消息将不再重试投递

  • 生产端重试区别:消费者和生产者的重试还是有区别的,主要有两点:

    • 默认重试次数:Product默认是2次,而Consumer默认是16次
    • 重试时间间隔:Product是立刻重试,而Consumer是有一定时间间隔的
    • 注意:而对于Consumer在广播情况下重试失效
  • 重试配置方式:消费失败后,重试配置方式,集群消费方式下,消息消费失败后期望消息重试,需要在消息监听器接口的实现中明确进行配置(三种方式任选一种)

    • 方式1:返回RECONSUME_LATER(推荐)
    • 方式2:返回Null
    • 方式3:抛出异常
  • 无需重试:集群消费方式下,消息失败后期望消息不重试,需要捕获消费逻辑中可能抛出的异常,最终返回Action.CommitMessage,此后这条消息将不会再重试

3.死信队列

在正常情况下无法被消费(超过最大重试次数)的消息称为死信消息(Dead-Letter Message),存储死信消息的特殊队列就称为死信队列(Dead-Letter Queue)

当一条消息初次消费失败,消息队列 RocketMQ 会自动进行消息重试。当达到最大重试次数后,若消费依然失败,则表明消费者在正常情况下无法正确地消费该消息。此时,消息队列 RocketMQ 不会立刻将消息丢弃,而是将其发送到该消费者对应的特殊队列中。 在RocketMQ中,这种正常情况下无法被消费的消息称为死信消息(Dead-Letter Message),存储死信消息的特殊队列称为死信队列(Dead-Letter Queue)

  1. 死信消息特性
  • 不会再被消费者正常消费
  • 有效期与正常消息相同,均为 3 天。3 天后会被自动删除,故死信消息应在产生的 3 天内及时处理
  1. 死信队列特性
  • 一个死信队列对应一个消费者组,而不是对应单个消费者实例
  • 一个死信队列包含了对应的 Group ID 所产生的所有死信消息,不论该消息属于哪个 Topic
  • 若一个 Group ID 没有产生过死信消息,则 RocketMQ 不会为其创建相应的死信队列

五.Redis 轮询队列(拓展)

redis队列中存放车辆信息,调度系统从队列中获取车辆信息,打车完成后再将车辆信息放回队列中

在这里插入图片描述
相关代码:

  1. redis获取车辆(从list左侧弹出一个车辆)
/**
  * 从Redis List列表中拿取一个车辆ID
  * 如果没有获取到延时10S
  *
  * @return
  */
public String takeVehicle() {
    
    
    //从Redis List列表中拿取一个车辆ID
    return redisTemplate.opsForList().leftPop(DispatchConstant.VEHICLE_QUEUE, 1, TimeUnit.SECONDS);
}

2.redis压入车辆(检查车辆状态,并从右侧压入车辆)

/**
  * 设置车辆状态为Ready
  *
  * @param vehicleId
  */
public void readyDispatch(String vehicleId) {
    
    
    //检查车辆状态
    DispatchConstant.DispatchType vehicleDispatchType = taxiVehicleStatus(vehicleId);
    //如果车辆时运行状态
    if (vehicleDispatchType.isRunning() || vehicleDispatchType.isReady()) {
    
    
        redisTemplate.opsForValue().set(DispatchConstant.VEHICLE_STATUS_PREFIX + vehicleId, DispatchConstant.DispatchType.READY.toString());
        //从右侧压入车辆
        redisTemplate.opsForList().rightPush(DispatchConstant.VEHICLE_QUEUE, vehicleId);
    }
}

猜你喜欢

转载自blog.csdn.net/weixin_44702984/article/details/131129552
今日推荐