消息中间件-rocketmq顺序消息

原理后面讲:
自定义了两个线程,每个线程循环往topic_order_test主题中,发送10条信息。

package org.apache.rocketmq.example.ordermessage;

import org.apache.rocketmq.client.exception.MQClientException;
import org.apache.rocketmq.client.producer.DefaultMQProducer;
import org.apache.rocketmq.client.producer.MessageQueueSelector;
import org.apache.rocketmq.common.message.Message;
import org.apache.rocketmq.common.message.MessageQueue;
import org.apache.rocketmq.remoting.common.RemotingHelper;

import java.util.List;

public class Producer {

    public static void main(String[] args) {
        try {
            final DefaultMQProducer mqProducer = new DefaultMQProducer("producer_order_test");
            mqProducer.setNamesrvAddr("192.168.0.11:9876;192.168.0.13:9876");
            mqProducer.start();

            new Thread(new Runnable() {
                Integer hashKey = 123;

                @Override
                public void run() {
                    sendMessage(mqProducer, hashKey);
                }
            }, "线程A").start();

            new Thread(new Runnable() {
                Integer hashKey = 124;

                @Override
                public void run() {
                    sendMessage(mqProducer, hashKey);
                }
            }, "线程B").start();

            Thread.sleep(3000);

            mqProducer.shutdown();

        } catch (MQClientException e) {
            e.printStackTrace();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    private static void sendMessage(DefaultMQProducer mqProducer, Integer hashKey) {

        try {
            for (int i = 0; i < 10; i++) {
                Message msg = new Message("topic_order_test",
                        "FILTER",i+"",
                        (hashKey+"").getBytes(RemotingHelper.DEFAULT_CHARSET)
                );
                mqProducer.send(msg, new MessageQueueSelector() {
                    @Override
                    public MessageQueue select(List<MessageQueue> mqs, Message msg, Object arg) {
                        Integer key = (Integer) arg;
                        int index = key % mqs.size();
                        return mqs.get(index);
                    }
                }, hashKey);
                System.out.println("message send,hashKey:" + hashKey);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }

    }
}

我们会发现有2个对列分别容纳10条信息,问题来了为什么没有发到一个队列?或者为什么8个队列没有均分?

为什么会发到0号和3号队列?见下图
在这里插入图片描述

在这里插入图片描述
消费线程:

package org.apache.rocketmq.example.ordermessage;

import org.apache.rocketmq.client.consumer.DefaultMQPushConsumer;
import org.apache.rocketmq.client.consumer.listener.*;
import org.apache.rocketmq.client.exception.MQClientException;
import org.apache.rocketmq.common.consumer.ConsumeFromWhere;
import org.apache.rocketmq.common.message.MessageExt;

import java.util.List;


public class Consumer {

    public static void main(String[] args) throws MQClientException {
        DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("please_rename_unique_group_name_3");
        consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_LAST_OFFSET);
        consumer.subscribe("topic_order_test", "FILTER");
        consumer.setNamesrvAddr("192.168.0.11:9876;192.168.0.13:9876");
        //设置并行消费线程数量
        consumer.setConsumeThreadMin(3);
        consumer.setConsumeThreadMax(6);
        //一个消费组 多个线程去并行消费生产者发送的消息
        consumer.registerMessageListener(new MessageListenerConcurrently() {
            @Override
            public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs,
                                                            ConsumeConcurrentlyContext context) {
                for (MessageExt messageExt : msgs) {
                    System.out.println(Thread.currentThread().getName() + "消费队列id " + messageExt.getQueueId() + ",发送消息内容:" +
                            new String(messageExt.getBody()) + ",消息索引:" + messageExt.getKeys());
                }
                return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
            }
        });

        consumer.start();
        System.out.printf("Consumer Started.%n");
    }

}

在这里插入图片描述
消费线程并行消费的时候,不能保证同一队列中的消息被顺序消费?怎么解决呢?
rocketMq官方给我们提供了MessageListenerOrderly

package org.apache.rocketmq.example.ordermessage;

import org.apache.rocketmq.client.consumer.DefaultMQPushConsumer;
import org.apache.rocketmq.client.consumer.listener.*;
import org.apache.rocketmq.client.exception.MQClientException;
import org.apache.rocketmq.common.consumer.ConsumeFromWhere;
import org.apache.rocketmq.common.message.MessageExt;

import java.util.List;


public class Consumer {

    public static void main(String[] args) throws MQClientException {
        DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("please_rename_unique_group_name_3");
        consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_LAST_OFFSET);
        consumer.subscribe("topic_order_test", "FILTER");
        consumer.setNamesrvAddr("192.168.0.11:9876;192.168.0.13:9876");
        consumer.setConsumeThreadMin(3);
        consumer.setConsumeThreadMax(6);
        //一个消费组 多个线程去并行消费生产者发送的消息
        consumer.registerMessageListener(new MessageListenerOrderly() {
            @Override
            public ConsumeOrderlyStatus consumeMessage(List<MessageExt> msgs, ConsumeOrderlyContext context) {
                for (MessageExt messageExt : msgs) {
                    System.out.println(Thread.currentThread().getName() + "消费队列id " + messageExt.getQueueId() + ",发送消息内容:" +
                            new String(messageExt.getBody()) + ",消息索引:" + messageExt.getKeys());
                }
                return ConsumeOrderlyStatus.SUCCESS;
            }
        });

        consumer.start();
        System.out.printf("Consumer Started.%n");
    }

}

每个线程消费队列中20条消息,确实按顺序消费。
在这里插入图片描述
顺序:首先什么叫做顺序?
一组数字 1,5,7,9,0,2,8
其实这些都是人为定义的,比如老师叫小朋友A将一组数字按照从小到大的顺序排列。
答案:0 ,1,2, 5,7,8,9
老师让小朋友B将一组数字按照从大到小的顺序排列。
答案:9,8,7,5,2,1,0
老师让小朋友c将一组数字按照乱序的顺序排列。
答案:0,2,7,9,8,1,5
有人可能会认为乱序叫什么顺序,其实我认为乱序也可理解为是一种顺序,只是这种排列方式毫无规律可言。

再比如生活中的点外卖:
1.进入点餐系统,比如用户点了一份酸菜鱼,然后加了购物车,点击结算,选择支付方式,微信支付,
支付成功,生成订单。此时这一系列操作可能会引发下游多个系统。
假设在用户支付成功后,

  • 生成微信支付成功订单

  • 商家接单操作

  • 在包括骑手接单

  • 骑手送货完成

  • 订单签收

我上面的列举的顺序就是符合日常生活逻辑的。假设我们采用mq,如何处理这种逻辑呢?

当然这种场景举的不是太好,因为周期比较长,现在假设默认这几步是瞬间需要完成的。

生产端保证(producer)

1.消息按顺序发送。
2.消息按顺序存储。

消费端保证(consumer)

3.消息按顺序消费。

第一种情况:如果用多线程去发送上面的几个步骤消息,那么很难保证顺序。所以只能采用单线程同步发送,就是上面一条信息发送成功,才能发送下一条信息。简称同步发送,但是这种方式有去缺陷。
比如:每条消息,可能被纷发到broker中不同的队列中去了,消息a在queue-1,消息b在queue2中,这样消费的时候,即使一个主题只有一个消费者,多个线程环境下怎么保证消费顺序呢。
第二种情况:想办法,将同一批消息发送到指定队列中,比如生产者在生产这5条消息时,给每一条消息都指定一个hashkey(用来计算该条消息应该被分配在哪个队列中),上文截图也有所涉及,这样就能保证5条消息处在同一个queue(队列满足FIFO先进先出原则)
第三种情况:我们考虑这样一种情况,同一批次消息A,B,C,D,E发送到xxTopic中,但是此topic被多个消费者订阅,这样就会出现问题。A消费者消费了A,B,C,B消费者消费D,C,E(好像类似多线程下卖票,同一张票卖多次的情况,线程不安全的,重复消费)。

  • 那么就至少要保证同一个队列同一个时刻,只能被一个消费者消费。
  • 队列中的每一条消息,即同一时刻只能有一个线程进行消费。

如何做到上面的两点?

一个队列同时只有一个消费者消费,那就是给这个queue对象上锁。
源码RebalanceImpl#updateProcessQueueTableInRebalance中,针对顺序消息的消息拉取,mq做了如下判断,
保证了一个队列同一时刻只有一个消费者线程占有。
在这里插入图片描述

实现目的:消费端从broker中拉取消费信息前,先向broker端发起对messageQueue的加锁请求,只有加锁成功,才能构建请求进行消费。
lock()的实现:
在这里插入图片描述

那么broker端收到消费端申请加锁的请求是怎么处理的?

ReblanceImplock方法调用了lockBatchMQ()方法,断点戳进去

在这里插入图片描述
然后我们具体看下broker端的tryLockBatch方法

broker端收到加锁请求的处理逻辑在RebalanceLockManager#tryLockBatch方法中,
RebalanceLockManager中关键属性如下:
在这里插入图片描述

LockEntry对象中关键属性如下:
在这里插入图片描述

在这里插入图片描述
对messageQueue进行加锁的关键逻辑如下:
如果messageQueue对应的lockEntry为空,标志队列未加锁,返回加锁成功。在这里插入图片描述
如果此队列当前已经被此客户端线程锁定,更新最新时间,并添加到已上锁的集合中(可重入加锁)
在这里插入图片描述
在这里插入图片描述
如果锁已经过期,返回加锁成功
在这里插入图片描述
总而言之,broker端通过对ConcurrentMap<String, ConcurrentHashMap<MessageQueue, LockEntry>> mqLockTable的维护来达到messageQueue加锁的目的,使得同一时刻,一个messageQueue只能被一个消费者消费。

synchronized申请线程独占锁

假设消费者对messageQueue的加锁已经成功,那么就进入到了第二个步骤,创建pullRequest进行消息拉取,消息拉取部分的代码实现在PullMessageService中,消息拉取完后,需要提交到ConsumeMessageService中进行消费,顺序消费的实现为ConsumeMessageOrderlyService,提交消息进行消费的方法为ConsumeMessageOrderlyService#submitConsumeRequest,具体实现如下:
在这里插入图片描述可以看到,构建了一个ConsumeRequest对象,并提交给了ThreadPoolExecutor来并行消费,看下顺序消费的ConsumeRequest的run方法实现

在这里插入图片描述
里面先从messageQueueLock中获取了messageQueue对应的一个锁对象,看下messageQueueLock的实现

在这里插入图片描述
其中维护了一个ConcurrentMap<MessageQueue, Object> mqLockTable,使得一个messageQueue对应一个锁对象object

获取到锁对象后,使用synchronized尝试申请线程级独占锁。

  • 如果加锁成功,同一时刻只有一个线程进行消息消费

  • 如果加锁失败,会延迟100ms重新尝试向broker端申请锁定messageQueue,锁定成功后重新提交消费请求。

至此,第三个关键点的解决思路也清晰了,基本上就两个步骤

  • 创建消息拉取任务时,消息客户端向broker端申请锁定MessageQueue,使得一个MessageQueue同一个时刻只能被一个消费客户端消费。
  • 消息消费时,多线程针对同一个消息队列的消费先尝试使用synchronized申请独占锁,加锁成功才能进行消费,使得一个MessageQueue同一个时刻只能被一个消费客户端中一个线程消费。

以上部分源码解析:参考https://blog.csdn.net/hosaos/article/details/90675978。谢谢

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值