浅谈RocketMQ 原理及使用
官网:http://rocketmq.apache.org/
背景
1、为什么使用 MQ
MQ 的作用:
1)削峰填谷
2) 异步处理
3) 系统解耦
2、 为什么使用rocketMQ
早期阿里曾经基于ActiveMQ研发消息系统, 随着业务消息的规模增大,瓶颈逐渐显现,后来也考虑过Kafka,但因为在低延迟和高可靠性方面没有选择,最后才自主研发了RocketMQ, 各方面的性能都比目前已有的消息队列要好,RocketMQ和Kafka在概念和原理上都非常相似,所以也经常被拿来对比;RocketMQ默认采用长轮询的拉模式, 单机支持千万级别的消息堆积,可以非常好的应用在海量消息系统中。
优点: 高吞吐,低延迟,高可用,事务消息,顺序消费
对比:
概述
系统架构
概念
Name Server
提供路由信息。NameServer 无状态节点,节点之间无信息同步。类似于简单的注册中心(zk 的服务发现)
Broker
rabbitMQ 也有broker ,与rabbitMQ broker功能类似。 Broker 主要 接收 producer 消息,存储消息。push 给 consumer或者 接收处理 consumer poll消息请求;
部署时,Broker 分为 Master 和 slave (一对多) ,通过broker name关联。每个 broker 都需要与nameserver 集群中所有服务器连接,用于同步注册的topic信息到nameserver,Name Server定时(每隔10s)扫描所有存活broker的连接,如果Name Server超过2分钟没有收到心跳,则Name Server断开与Broker的连接。
Producer
生产者,发送消息。Producer与Name Server集群中的其中一个节点(随机选择)建立长连接,定期从Name Server取Topic路由信息,并向提供Topic服务的Master建立长连接,且定时向Master发送心跳。Producer完全无状态,可集群部署。
Consumer
Consumer与Name Server集群中的其中一个节点(随机选择)建立长连接,定期从Name Server取Topic路由信息,并向提供Topic服务的Master、Slave建立长连接,且定时向Master、Slave发送心跳。Consumer既可以从Master订阅消息,也可以从Slave订阅消息,订阅规则由Broker配置决定。
由于NameServer 只存储路由信息,无状态,且NameServer 之间无数据同步。所以NameServer 中的数据,依赖于broker同步。由于没有类似zk的watch等机制,所以需要与broker, producer, consumer 等所有连接之间建立心跳机制。
单个broker 跟所有NameServer保持心跳请求,心跳间隔为30秒,心跳请求中包括当前Broker所有的Topic信息。Namesrv会反查Broer的心跳信息, 如果某个Broker在2分钟之内都没有心跳,则认为该Broker下线,调整Topic跟Broker的对应关系。但此时Namesrv不会主动通知Producer、Consumer有Broker宕机。 Consumer跟Broker是长连接,会每隔30秒发心跳信息到Broker。Broker端每10秒检查一次当前存活的Consumer,若发现某个Consumer 2分钟内没有心跳, 就断开与该Consumer的连接,并且向该消费组的其他实例发送通知,触发该消费者集群的负载均衡(rebalance)。 Producer 每30秒从Namesrv获取Topic跟Broker的映射关系,更新到本地内存中。再跟Topic涉及的所有Broker建立长连接,每隔30秒发一次心跳。 在Broker端也会每10秒扫描一次当前注册的Producer,如果发现某个Producer超过2分钟都没有发心跳,则断开连接。
事务消息
1、为什么需要事务消息?
生产着 与 MQ:
生产者本地操作 与 消息 投递 一致性。
本地成功,消息发送失败;或者消息发送成功,本地失败;
消费者与MQ:
消费者本地业务操作 与 消息接收 保持一致性
存在接收消息,消费者执行失败场景。
2、rocketMQ 是如何实现事务消息的
1、消费者,通过失败重试消费。
2、生产者与MQ 保持一致:
步骤5,通过 生产者 实现 TransactionCheckListener 监听 来实现。
// 代码示例
顺序消费
1、为什么需要顺序消费?
某些业务场景下,业务操作需要按顺序执行。如 新增,更新,查询,删除, 4条消息。如果生产不按照顺序消费,则最终结果会与预期不一致。
2、如何实现顺序消费?
消息队列(Message Queue),队列 queue 本质上 FIFO 就是按照顺序推送消息。
有不按顺序消费问题,MQ支持多队列。以及在分布式环境下,多机消费,或者多线程消费。无法保证消费顺序
produce在发送消息的时候,把消息发到同一个队列(queue)中,消费者注册消息监听器为MessageListenerOrderly,这样就可以保证消费端只有一个线程去消费消息。
// 代码示例
原理分析
发送消息流程
producer 发送普通消息。
DefaultMQProducerImpl:sendDefaultImpl
1、校验producer状态正常,校验消息格式
2、根据 topic ,从NameServer 中获取路由信息(路由信息中包含broker以及queues信息)
3、组装发送到broker的请求,并根据不同发送方式发送请求到broker,
4、处理发送结果
producer 发送事务消息
DefaultMQProducerImpl:sendMessageInTransaction
1、校验 producer是否正确配置 TransactionListener,校验消息格式
2、组装 Half 消息并发送
3、如果发送消息正常,则执行本地事务逻辑,记录本地事务执行后的结果状态;如果消息发送失败,则标记本地事务为回滚
4、根据本地事务状态,决定是进一步发送消息到broker,还是回滚
5、返回发送结果
Broker 接收处理producer 发送消息请求
SendMessageProcessor:processRequest
Broker 职责,需要处理 producer请求,并存储消息(存储消息,后续流程再发送到消费者)
1、校验 消息中 topic 配置
2、如果请求中的消息队列编号小于0,则随机选择一个消息队列
3、处理重试的消息请求,如果重试次数大于配置的最大重试次数,则将消息后续放入到死信队列处理
4、创建broker 内部的消息请求,并区分是否为事务消息,区别存储消息到 commitLog 中
5、处理最终的发送结果,并把结果响应给producer
Consumer 从 broker 拉取消息
RocketMQ 提供了 push 和 poll 两种模式。但是主要是依靠 consumer 不断的轮询拉取,达到 broker push消息的效果
DefaultMQPushConsumerImpl:pullMessage
1、 PullMessageService 是 Consumer 从 broker 拉取消息
2、拉取消息后,两个操作:1)添加消息到ProcessQueue中;2)提交消息消费任务
3、ConsumeMessageService ,进行消息消费 (核心类:ConsumeRequest)
4、消费之后的处理:1)消费失败,则提交消息消费任务可进行重试,及发送消费失败消息到broker;2)消费成功的消息,则从 processQueue 中移除;3)更新消费进度,定时同步到broker
消息存储
RocketMQ 最重要 最核心的 消息存储
CommitLog 与 ConsumeQueue
CommitLog:消息存储主体,存储producer写入的消息。单个文件大小默认1G ,文件名长度为20位,左边补零,剩余为起始偏移量,比如00000000000000000000代表了第一个文件,起始偏移量为0,文件大小为1G=1073741824;当第一个文件写满了,第二个文件为00000000001073741824,起始偏移量为1073741824,以此类推。消息主要是顺序写入日志文件,当文件满了,写入下一个文件;
ConsumeQueue:逻辑队列,不实际存储消息内容,可以理解为消息的索引,ConsumeQueue 保存了指定Topic下的队列消息在CommitLog中的起始物理偏移量offset,消息的size以及消息tag的hashCode值。
数据和索引部分相分离的存储架构设计的优点:高性能的消息持久化刷盘;消息查询等性能高;消费者消费消息性能高。
1、如果没有commitLog,ConsumeQueue 实际存储消息内容,则不利于统一的消息持久化,不利于事务消息处理,消息查询等
2、如果只有commitLog,没有逻辑队列ConsumeQueue,由于RocketMQ是基于主题topic的订阅模式,消息消费是针对主题进行的,如果要遍历commitlog文件中根据topic检索消息是非常低效的。
commitLog 文件的存储与读取
顺序消费的实现
Producer : 把需要保证顺序的消息,放到同一队列中
Consumer 严格的顺序消费:
Broker
消息队列锁(分布式锁):
1)集群模式下,consumer 需要先从broker获取锁之后,才能进行消息的拉取和消费;
2) 广播模式下,Consumer 则无需该锁
Consumer 消息队列锁(consumer本地锁):Consumer 获得该锁才能操作消息队列。
Consumer 消息处理队列消费锁(本地锁) :Consumer 获得该锁才能消费消息队列。
Consumer 消费流程:
ConsumeRequest
1、获取broker 分布式锁。
2、获取consumer消费队列锁
3、获得消费消息
4、获取消息队列消费锁
5、消费消息
6、释放消费锁
7、处理消费结果
8、循环执行步骤3
9、循环结束,则释放消费队列锁。循环结束的条件: 未获取broker分布式锁或者获取的分布式锁已经过期。
事务消息的核心处理,事务回查
(1) 处理存储事务消息时,会备份消息及主体。然后生成新的half topic,这样消息就不会被消费
RocketMQ的具体实现策略是:写入的如果事务消息,对消息的Topic和Queue等属性进行替换,同时将原来的Topic和Queue信息存储到消息的属性中,正因为消息主题被替换,故消息并不会转发到该原主题的消息消费队列,消费者无法感知消息的存在,不会消费
// CommitLog:asyncPutMessage
if (tranType == MessageSysFlag.TRANSACTION_NOT_TYPE
|| tranType == MessageSysFlag.TRANSACTION_COMMIT_TYPE) {
// Delay Delivery
if (msg.getDelayTimeLevel() > 0) {
if (msg.getDelayTimeLevel() > this.defaultMessageStore.getScheduleMessageService().getMaxDelayLevel()) {
msg.setDelayTimeLevel(this.defaultMessageStore.getScheduleMessageService().getMaxDelayLevel());
}
topic = TopicValidator.RMQ_SYS_SCHEDULE_TOPIC;
queueId = ScheduleMessageService.delayLevel2QueueId(msg.getDelayTimeLevel());
// Backup real topic, queueId
MessageAccessor.putProperty(msg, MessageConst.PROPERTY_REAL_TOPIC, msg.getTopic());
MessageAccessor.putProperty(msg, MessageConst.PROPERTY_REAL_QUEUE_ID, String.valueOf(msg.getQueueId()));
msg.setPropertiesString(MessageDecoder.messageProperties2String(msg.getProperties()));
msg.setTopic(topic);
msg.setQueueId(queueId);
}
}
(2) broker 定时发起事务回查,检查producer本地事务执行状态
定时发起任务回查
TransactionalMessageCheckService:onWaitEnd
TransactionalMessageServiceImpl:check
实际检查本地事务状态:
DefaultMQProducerImpl:checkTransactionState