RocketMQ本身支持顺序消息,在使用上发送顺序消息和非顺序消息有所区别

发送顺序消息

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

send方法带有参数MessageQueueSelector,MessageQueueSelector是让用户自己决定消息发送到哪一个队列,如果是局部消息的话,用来决定消息与队列的对应关系。

顺序消息消费

consumer.registerMessageListener(new MessageListenerOrderly() {
AtomicLong consumeTimes = new AtomicLong(0); @Override
public ConsumeOrderlyStatus consumeMessage(List<MessageExt> msgs, ConsumeOrderlyContext context) {
context.setAutoCommit(false);
System.out.printf("%s Receive New Messages: %s %n", Thread.currentThread().getName(), msgs);
return ConsumeOrderlyStatus.SUCCESS;
}
});

从使用上可以推断顺序消息需要从发送到消费整个过程中保证有序,所以顺序消息具体表现为

  • 发送消息是顺序的
  • broker存储消息是顺序的
  • consumer消费是顺序的

下面分别看看rmq怎么实现顺序消息

发送顺序消息

因为broker存储消息有序的前提是producer发送消息是有序的,所以这两个结合在一起说。

消息发布是有序的含义:producer发送消息应该是依次发送的,所以要求发送消息的时候保证:

  • 消息不能异步发送,同步发送的时候才能保证broker收到是有序的。
  • 每次发送选择的是同一个MessageQueue

同步发送

从刚开始的例子中发送消息的时候,会调用下面的方法

public SendResult send(Message msg, MessageQueueSelector selector, Object arg, long timeout)
throws MQClientException, RemotingException, MQBrokerException, InterruptedException {
return this.sendSelectImpl(msg, selector, arg, CommunicationMode.SYNC, null, timeout);
}

CommunicationMode.SYNC表明了producer发送消息的时候是同步发送的。同步发送表示,producer发送消息之后不会立即返回,会等待broker的response。

broker收到producer的请求之后虽然是启动线程处理的,但是在线程中将消息写入commitLog中以后会发送response给producer,producer在收到broker的response并且是处理成功之后才算是消息发送成功。

同一个MessageQueue

为了保证broker收到消息也是顺序的,所以producer只能向其中一个队列发送消息。因为只有是同一个队列才能保证消息是发往同一个broker,只有同一个broker处理发来的消息才能保证顺序。所以发送顺序消息的时候需要用户指定MessageQueue,在send调用过程中会调用下面的方法,下面的方法中回调了用户指定的select queue的方法

private SendResult sendSelectImpl(
Message msg,
MessageQueueSelector selector,
Object arg,
final CommunicationMode communicationMode,
final SendCallback sendCallback, final long timeout
) throws MQClientException, RemotingException, MQBrokerException, InterruptedException {
this.makeSureStateOK();
Validators.checkMessage(msg, this.defaultMQProducer); TopicPublishInfo topicPublishInfo = this.tryToFindTopicPublishInfo(msg.getTopic());
if (topicPublishInfo != null && topicPublishInfo.ok()) {
MessageQueue mq = null;
try {
// 调用用户指定的select方法来选出一个queue,如果是全局顺序,用户必须保证自己选出的queue是同一个
mq = selector.select(topicPublishInfo.getMessageQueueList(), msg, arg);
} catch (Throwable e) {
throw new MQClientException("select message queue throwed exception.", e);
} if (mq != null) {
return this.sendKernelImpl(msg, mq, communicationMode, sendCallback, null, timeout);
} else {
throw new MQClientException("select message queue return null.", null);
}
} throw new MQClientException("No route info for this topic, " + msg.getTopic(), null);
}

这样每次发送的消息都是同一个MessageQueue,也就是都会发送到同一个broker,这个发送消息的过程都保证了顺序,也就保证了broker存储在CommitLog中的消息也是顺序的。

顺序消费

保证了broker中物理存储的消息是顺序的,只要保证消息消费是顺序的就能保证整个过程是顺序消息了。

还是开始的例子中,顺序消费和普通消费的listener是不一样的,顺序消费需要实现的是下面这个接口

org.apache.rocketmq.client.consumer.listener.MessageListenerOrderly

在consumer启动的时候会根据listener的类型判断应该使用哪一个service来消费

// org.apache.rocketmq.client.impl.consumer.DefaultMQPushConsumerImpl#start
if (this.getMessageListenerInner() instanceof MessageListenerOrderly) {
// 顺序消息
this.consumeOrderly = true;
this.consumeMessageService =
new ConsumeMessageOrderlyService(this, (MessageListenerOrderly) this.getMessageListenerInner());
} else if (this.getMessageListenerInner() instanceof MessageListenerConcurrently) {
// 非顺序消息
this.consumeOrderly = false;
this.consumeMessageService =
new ConsumeMessageConcurrentlyService(this, (MessageListenerConcurrently) this.getMessageListenerInner());
}

consumer拉取消息是按照offset拉取的,所以consumer能保证拉取到consumer的消息是连续有序的,但是consumer拉取到消息后又启动了线程去处理消息,所以就不能保证先拉取到的消息先处理

// org.apache.rocketmq.client.impl.consumer.DefaultMQPushConsumerImpl#pullMessage
// org.apache.rocketmq.client.consumer.PullCallback#onSuccess
// 将拉取到的消息放入ProcessQueue
boolean dispathToConsume = processQueue.putMessage(pullResult.getMsgFoundList());
// 这里的consumeMessageService就是ConsumeMessageOrderlyService
DefaultMQPushConsumerImpl.this.consumeMessageService.submitConsumeRequest(
pullResult.getMsgFoundList(),
processQueue,
pullRequest.getMessageQueue(),
dispathToConsume); // org.apache.rocketmq.client.impl.consumer.ConsumeMessageOrderlyService#submitConsumeRequest
public void submitConsumeRequest(
final List<MessageExt> msgs,
final ProcessQueue processQueue,
final MessageQueue messageQueue,
final boolean dispathToConsume) {
if (dispathToConsume) {
// 拉取到的消息构造ConsumeRequest,然后放入线程池等待执行
ConsumeRequest consumeRequest = new ConsumeRequest(processQueue, messageQueue);
this.consumeExecutor.submit(consumeRequest);
}
}

上面将请求放入线程池了,所以线程执行的顺序又不确定了,那么consumer消费就变成无序的了吗?

答案显然是不会变成无序的,因为上面有一行关键的代码

processQueue.putMessage(pullResult.getMsgFoundList());

先说一下ProcessQueue这个关键的数据结构。一个MessageQueue对应一个ProcessQueue,是一个有序队列,该队列记录一个queueId下所有从brokerpull回来的消息,如果消费成功了就会从队列中删除。ProcessQueue有序的原因是维护了一个TreeMap

private final TreeMap<Long, MessageExt> msgTreeMap = new TreeMap<Long, MessageExt>();

msgTreeMap:里面维护了从broker pull回来的所有消息,TreeMap是有序的,key是Long类型的,没有指定comparator,默认是将key强转为Comparable,然后进行比较,因为key是当前消息的offset,而Long实现了Comparable接口,所以msgTreeMap里面的消息是按照offset排序的。

所以是ProcessQueue保证了拉取回来的消息是有序的,继续上面说到的启动线程执行ConsumeRequest.run方法来消费消息

Override
public void run() {
// 保证一个队列只有一个线程访问,因为顺序消息只有一个队列,也就保证了只有一个线程消费
final Object objLock = messageQueueLock.fetchLockObject(this.messageQueue);
synchronized (objLock) {
if (MessageModel.BROADCASTING.equals(ConsumeMessageOrderlyService.this.defaultMQPushConsumerImpl.messageModel())
|| (this.processQueue.isLocked() && !this.processQueue.isLockExpired())) {
final long beginTime = System.currentTimeMillis();
for (boolean continueConsume = true; continueConsume; ) {
// 从ProcessQueue中获取消息进行消费,获取出来的消息也是有序的
final int consumeBatchSize =
ConsumeMessageOrderlyService.this.defaultMQPushConsumer.getConsumeMessageBatchMaxSize(); List<MessageExt> msgs = this.processQueue.takeMessags(consumeBatchSize);
// 省略中间代码
}

从ProcessQueue中获取出来的消息是有序的,consumer保证了消费的有序性。

总结

rmq从发送消息、broker存储消息到consumer消费消息整个过程中,环环相扣,最终保证消息是有序的。

RocketMQ源码 — 十、 RocketMQ顺序消息的更多相关文章

  1. RocketMQ源码 — 三、 Producer消息发送过程

    Producer 消息发送 producer start producer启动过程如下图 public void start(final boolean startFactory) throws MQ ...

  2. rocketmq源码分析2-broker的消息接收

    broker消息接收,假设接收的是一个普通消息(即没有事务),此处分析也只分析master上动作逻辑,不涉及ha. 1. 如何找到消息接收处理入口 可以通过broker的监听端口10911顺藤摸瓜式的 ...

  3. rocketmq源码分析4-事务消息实现原理

    为什么消息要具备事务能力 参见还是比较清晰的.简单的说 就是在你业务逻辑过程中,需要发送一条消息给订阅消息的人,但是期望是 此逻辑过程完全成功完成之后才能使订阅者收到消息.业务逻辑过程 假设是这样的: ...

  4. RocketMQ源码 — 六、 RocketMQ高可用(1)

    高可用究竟指的是什么?请参考:关于高可用的系统 RocketMQ做了以下的事情来保证系统的高可用 多master部署,防止单点故障 消息冗余(主从结构),防止消息丢失 故障恢复(本篇暂不讨论) 那么问 ...

  5. RocketMQ源码详解 | Consumer篇 · 其一:消息的 Pull 和 Push

    概述 当消息被存储后,消费者就会将其消费. 这句话简要的概述了一条消息的最总去向,也引出了本文将讨论的问题: 消息什么时候才对被消费者可见? 是在 page cache 中吗?还是在落盘后?还是像 K ...

  6. RocketMQ源码分析之RocketMQ事务消息实现原理上篇(二阶段提交)

    在阅读本文前,若您对RocketMQ技术感兴趣,请加入 RocketMQ技术交流群 根据上文的描述,发送事务消息的入口为: TransactionMQProducer#sendMessageInTra ...

  7. RocketMQ 源码学习笔记————Producer 是怎么将消息发送至 Broker 的?

    目录 RocketMQ 源码学习笔记----Producer 是怎么将消息发送至 Broker 的? 前言 项目结构 rocketmq-client 模块 DefaultMQProducerTest ...

  8. RocketMQ 源码学习笔记 Producer 是怎么将消息发送至 Broker 的?

    目录 RocketMQ 源码学习笔记 Producer 是怎么将消息发送至 Broker 的? 前言 项目结构 rocketmq-client 模块 DefaultMQProducerTest Roc ...

  9. RocketMQ源码分析之从官方示例窥探:RocketMQ事务消息实现基本思想

    摘要: RocketMQ源码分析之从官方示例窥探RocketMQ事务消息实现基本思想. 在阅读本文前,若您对RocketMQ技术感兴趣,请加入RocketMQ技术交流群 RocketMQ4.3.0版本 ...

随机推荐

  1. LeetCode(63)-First Bad Version

    题目: You are a product manager and currently leading a team to develop a new product. Unfortunately, ...

  2. 容器(list集合)

    --为什么使用集合而不使用数组?why ·集合和数组相似点:都可以存储多个对象,对外作为一个整体存在: ··数组的缺点:1.长度必须在初始化时指定,且固定不变: 2.数组采用连续存储空间,删除和添加元 ...

  3. IBATIS的优缺点

    ibatis优缺点总结 .优点 简单: 易于学习,易于使用,通过文档和源代码,可以比较完全的掌握它的设计思路和实现. 实用: 提供了数据映射功能,提供了对底层数据访问的封装(例如ado.net),提供 ...

  4. JavaScript中对象数组 作业 题目如下

    var BaiduUsers = [], WechatUsers = []; var User = function(id, name, phone, gender, age, salary) { t ...

  5. JavaScript中对象数组 根据某个属性值 然后push到新的数组

    原文链接 https://segmentfault.com/q/1010000010075035 将下列对象数组中,工资大于1w的员工,增加到对象数组 WanSalary中 var BaiduUser ...

  6. iview源码解析(1)

    概述 公司技术栈开始用vue主导开发,但因为公司前端会vue的不多所以在项目中用到vue的技术不是很深,之前出去面试被接连打击,而且本来打算开始为公司vue的项目构建自己的组件库所以去下载了iview ...

  7. Flex 将默认日期格式转化成通用格式

    flex 默认日期格式如:Wed Dec 16 00:00:00 GMT+0800 2015 想要得到的通用格式如:2015-12-16 转换方法如下: var sdate:String = &quo ...

  8. 源码安装xadmin及使用

    xadmin是django的第三方后台 我们也可以使用pip来安装,但是推荐使用源码安装. 因为有些新功能以及发布在GitHub上,但是还未发布到pypi上,我们就可以提取使用这些功能. 一.安装 1 ...

  9. java算法之超级丑数

    问题描述: 写一个程序来找第 n 个超级丑数. 超级丑数的定义是正整数并且所有的质数因子都在所给定的一个大小为 k 的质数集合内. 比如给你 4 个质数的集合 [2, 7, 13, 19], 那么 [ ...

  10. Ubuntu 16.04 安装 Docker

    在Ubuntu上安装Docker, 非常简单, 我测试过 16.04, 17.04, 以及最新版 18.04,都是可以成功安装,并使用的. 第一步:  启动root账号 第二步:  配置网络,能上网 ...