主流消息队列选型对比分析

基础项对比

可用性、可靠性对比

功能性对比

对比分析

  • Kafka:系统间的流数据通道
  • RocketMQ:高性能的可靠消息传输
  • RabbitMQ:可靠消息传输

RocketMQ剖析

RocketMQ拓扑图

RocketMQ架构组成

  • Producer:消息发布的角色,支持分布式集群方式部署。Producer通过MQ的负载均衡模块选择相应的Broker集群队列进行消息投递,投递的过程支持快速失败并且低延迟。
  • Consumer:消息消费的角色,支持分布式集群方式部署。支持以push推,pull拉两种模式对消息进行消费。同时也支持集群方式和广播方式的消费,它提供实时消息订阅机制,可以满足大多数用户的需求。
  • NameServer:NameServer是一个非常简单的Topic路由注册中心,其角色类似Dubbo中的zookeeper,支持Broker的动态注册与发现。主要包括两个功能:Broker管理,NameServer接受Broker集群的注册信息并且保存下来作为路由信息的基本数据。然后提供心跳检测机制,检查Broker是否还存活;路由信息管理,每个NameServer将保存关于Broker集群的整个路由信息和用于客户端查询的队列信息。然后Producer和Conumser通过NameServer就可以知道整个Broker集群的路由信息,从而进行消息的投递和消费。NameServer通常也是集群的方式部署,各实例间相互不进行信息通讯。Broker是向每一台NameServer注册自己的路由信息,所以每一个NameServer实例上面都保存一份完整的路由信息。当某个NameServer因某种原因下线了,Broker仍然可以向其它NameServer同步其路由信息,Producer,Consumer仍然可以动态感知Broker的路由的信息。
  • BrokerServer:消息中转角色,负责存储消息、转发消息。代理服务器在RocketMQ系统中负责接收从生产者发送来的消息并存储、同时为消费者的拉取请求作准备。代理服务器也存储消息相关的元数据,包括消费者组、消费进度偏移和主题和队列消息等。

部署架构

集群工作流程

  • 启动NameServer,NameServer起来后监听端口,等待Broker、Producer、Consumer连上来,相当于一个路由控制中心。
  • Broker启动,跟所有的NameServer保持长连接,定时发送心跳包。心跳包中包含当前Broker信息(IP+端口等)以及存储所有Topic信息。注册成功后,NameServer集群中就有Topic跟Broker的映射关系。
  • 收发消息前,先创建Topic,创建Topic时需要指定该Topic要存储在哪些Broker上,也可以在发送消息时自动创建Topic。
  • Producer发送消息,启动时先跟NameServer集群中的其中一台建立长连接,并从NameServer中获取当前发送的Topic存在哪些Broker上,轮询从队列列表中选择一个队列,然后与队列所在的Broker建立长连接从而向Broker发消息。
  • Consumer跟Producer类似,跟其中一台NameServer建立长连接,获取当前订阅Topic存在哪些Broker上,然后直接跟Broker建立连接通道,开始消费消息。
 

RocketMQ设计

消息存储

  1. CommitLog:存储消息的主体。product生产的消息主要是顺序写入日志文件,当文件满了,写入下一个文件。
  2. ConsumerQueue:消息的消费队列。引入的目的主要是提高消息消费的性能,由于RocketMQ是基于主题topic的订阅模式,消息消费是针对主题进行的,如果要遍历commitlog文件中根据topic检索消息是非常低效的。
  3. FileIndex:索引文件。提供了一种可以通过key或时间区间来查询消息的方法

消息刷盘

  1. 同步刷盘:性能低,可靠性高。
  2. 异步刷盘:性能高,可靠性低。
一般线上采用异步刷盘+异步复制。如果保证绝对可靠性需要同步刷盘+同步双写,但性能很低,可以针对特别重要的消息,单独部署broker。

协议设计与编解码

在Client和Server之间完成一次消息发送时,需要对发送的消息进行一个协议约定,因此就有必要自定义RocketMQ的消息协议。同时,为了高效地在网络中传输消息和对收到的消息读取,就需要对消息进行编解码。在RocketMQ中,RemotingCommand这个类在消息传输过程中对所有数据内容的封装,不但包含了所有的数据结构,还包含了编码解码操作。
Header字段 类型 Request说明 Response说明
code int 请求操作码,应答方根据不同的请求码进行不同的业务处理 应答响应码。0表示成功,非0则表示各种错误
language LanguageCode 请求方实现的语言 应答方实现的语言
version int 请求方程序的版本 应答方程序的版本
opaque int 相当于requestId,在同一个连接上的不同请求标识码,与响应消息中的相对应 应答不做修改直接返回
flag int 区分是普通RPC还是onewayRPC的标志 区分是普通RPC还是onewayRPC的标志
remark String 传输自定义文本信息 传输自定义文本信息
extFields HashMap<String, String> 请求自定义扩展信息 响应自定义扩展信息

传输内容主要可以分为以下4部分:
(1) 消息长度:总长度,四个字节存储,占用一个int类型;
(2) 序列化类型&消息头长度:同样占用一个int类型,第一个字节表示序列化类型,后面三个字节表示消息头长度;
(3) 消息头数据:经过序列化后的消息头数据;
(4) 消息主体数据:消息主体的二进制字节数据内容;
public ByteBuffer encode() {
// 1> header length size
int length = 4; // 2> header data length
byte[] headerData = this.headerEncode();
length += headerData.length; // 3> body data length
if (this.body != null) {
length += body.length;
} ByteBuffer result = ByteBuffer.allocate(4 + length); // length
result.putInt(length); // header length
result.put(markProtocolType(headerData.length, serializeTypeCurrentRPC)); // header data
result.put(headerData); // body data;
if (this.body != null) {
result.put(this.body);
} result.flip(); return result;
}

负载均衡

RocketMQ中的负载均衡都在Client端完成,具体来说的话,主要可以分为Producer端发送消息时候的负载均衡和Consumer端订阅消息的负载均衡。

product端负载均衡

  1. 定期获取TopicPublishInfo路由信息
  2. product发送消息时选取一个messageQueue发送消息(默认的负载均衡策略:随机递增取模)
  3. 容错机制(故障延时:指对之前失败的,按一定的时间做退避。发送失败默认有会有重试(同步:2次,异步:1次)同步重试会避开上一次发失败的broker

Consumer端负载均衡

mq消息消费方式
PUSH :消息队列主动将消息推送给消费者
PULL:消费者主动去消息队列拉取
push 和pull 两种方式的对比:
push:消息实时性高,但没有考虑消费端的消费能力
pull:消息实时性低,可能造成大量无效请求
consumer获取消息的模式:
在RocketMQ中,Consumer端的两种消费模式(Push/Pull)都是基于拉模式来获取消息的,为了平衡push/pull的各自的弊端,使用了一种长轮询机制来拉取消息。Push模式只是对pull模式的一种封装,其本质实现为消息拉取线程在从服务器拉取到一批消息后,然后提交到消息消费线程池后,又继续向服务器再次尝试拉取消息。如果未拉取到消息,则延迟一下又继续拉取。
负载均衡
  1. 定时发送心跳包到broker
  2. consumer开始订阅消息会rebalance 一次
  3. 定期rebalance(20s)
  • 获取队列信息
  • 获取消费者信息
  • 排序平均分配(默认)
  • 与上次结果对比

RocketMQ功能实现分析

RocketMQ延时消息

rockeketMQ支持18个级别的延时等级,默认值为:“1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h”

实现原理

  1. 替换主题SCHEDULE_TOPIC_XXX,根据延时等级放入对应的队列
  2. 18个Queue对应18个延时等级
  3. 每个队列创建定时任务进行调度
  4. 恢复到期消息重新投递到真实的topic

消息重试

Consumer消费消息失败后,RocketMQ提供一种重试机制,令消息再消费一次。RocketMQ会为每个消费组都设置一个Topic名称为“%RETRY%+consumerGroup”的重试队列(这里需要注意的是,这个Topic的重试队列是针对消费组,而不是针对每个Topic设置的),用于暂时保存因为各种异常而导致Consumer端无法消费的消息。考虑到异常恢复起来需要一些时间,会为重试队列设置多个重试级别,每个重试级别都有与之对应的重新投递延时,重试次数越多投递延时就越大。RocketMQ对于重试消息的处理是先保存至Topic名称为“SCHEDULE_TOPIC_XXXX”的延迟队列中,后台定时任务按照对应的时间进行Delay后重新保存至“%RETRY%+consumerGroup”的重试队列中。

消费失败策略

  • 重试16次
  • 重试时间间隔递增(通过延时对列完成)
  • 失败后进入私信队列

事务消息

public class TransactionProducer {
public static void main(String[] args) throws MQClientException, InterruptedException {
TransactionListener transactionListener = new TransactionListenerImpl();
TransactionMQProducer producer = new TransactionMQProducer("please_rename_unique_group_name");
ExecutorService executorService = new ThreadPoolExecutor(2, 5, 100, TimeUnit.SECONDS, new ArrayBlockingQueue<Runnable>(2000), new ThreadFactory() {
@Override
public Thread newThread(Runnable r) {
Thread thread = new Thread(r);
thread.setName("client-transaction-msg-check-thread");
return thread;
}
}); producer.setExecutorService(executorService);
//事务监听器
producer.setTransactionListener(transactionListener);
producer.start(); String[] tags = new String[] {"TagA", "TagB", "TagC", "TagD", "TagE"};
for (int i = 0; i < 10; i++) {
try {
Message msg =
new Message("TopicTest1234", tags[i % tags.length], "KEY" + i,
("Hello RocketMQ " + i).getBytes(RemotingHelper.DEFAULT_CHARSET));
SendResult sendResult = producer.sendMessageInTransaction(msg, null);
System.out.printf("%s%n", sendResult); Thread.sleep(10);
} catch (MQClientException | UnsupportedEncodingException e) {
e.printStackTrace();
}
} for (int i = 0; i < 100000; i++) {
Thread.sleep(1000);
}
producer.shutdown();
}
} public interface TransactionListener {
/**
* When send transactional prepare(half) message succeed, this method will be invoked to execute local transaction.
*
* @param msg Half(prepare) message
* @param arg Custom business parameter
* @return Transaction state
*/
LocalTransactionState executeLocalTransaction(final Message msg, final Object arg); /**
* When no response to prepare(half) message. broker will send check message to check the transaction status, and this
* method will be invoked to get local transaction status.
*
* @param msg Check message
* @return Transaction state
*/
LocalTransactionState checkLocalTransaction(final MessageExt msg);
}

RocketMQ事务消息流程概要

上图说明了事务消息的大致方案,其中分为两个流程:正常事务消息的发送及提交、事务消息的补偿流程。
1.事务消息发送及提交:
(1) 发送消息(half消息)。
  • HALF消息:RMQ_SYS_TRANS_HALF_TOPIC(临时存放消息信息)

    • 事务消息替换主体,保存原主题和对列信息
    • 半消息对Consumer不可见,不会被投递
(2) 服务端响应消息写入结果。
(3) 根据发送结果执行本地事务(如果写入失败,此时half消息对业务不可见,本地逻辑不执行)。
(4) 根据本地事务状态执行Commit或者Rollback(Commit操作生成消息索引,消息对消费者可见)
 
2.补偿流程:
(1) 对没有Commit/Rollback的事务消息(pending状态的消息),从服务端发起一次“回查”
(2) Producer收到回查消息,检查回查消息对应的本地事务的状态
(3) 根据本地事务状态,重新Commit或者Rollback
其中,补偿阶段用于解决消息Commit或者Rollback发生超时或者失败的情况。

怎么记录二阶段的操作?

RocketMQ事务消息方案中引入了Op消息的概念,用Op消息标识事务消息已经确定的状态(Commit或者Rollback)。如果一条事务消息没有对应的Op消息,说明这个事务的状态还无法确定(可能是二阶段失败了)。引入Op消息后,事务消息无论是Commit或者Rollback都会记录一个Op操作。Commit相对于Rollback只是在写入Op消息前创建Half消息的索引。
  • OP消息:RMQ_SYS_TARNS_OP_HALF_TOPIC(记录二阶段的操作)

    • Rollback:只做记录
    • Commit:根据备份信息重新构造消息并投递
 

扩展

 

RocketMQ事务消息对业务侵入性强的解决方案

  1. 开启事务
  2. 操作本地业务数据
  3. 插入事务消息数据
  4. 提交事务
  5. 发送mq消息
  6. mq send响应
  7. mq消息发送成功删除事务消息表中的记录
  8. 定时补偿模块扫描事务消息表
  9. 补偿发送mq消息
  10. mq send 响应
  11. mq消息发送成功删除事务消息表中的记录

伪代码

@Transactional
public void pay(Order order){
PayTransaction t = buildPayTransaction(order);
payDao.append(t);
//producer.sendMessage(buildMessage(t));
final Message message = buildMessage(t);
messageDao.insert(message);
//在事务提交后执行
triggerAfterTransactionCommit(()->{
messageClient.send(message);
messageDao.delete(message);
});
}
 

事务消息表

CREATE TABLE mq_message(
id bigint NOT NULL AUTO_INCREMENT,
content varchar(255) NOT NULL,
topic char(64) NOT NULL,
tag char(64),
status tinyint,
createtime timestamp,
PRIMARY KEY(id) )

任意时间延时消息实现方案

改造步骤

  1. Dispatch改造
  2. 延时消息存储
  3. 内存索引(时间轮)
  4. 延时消息投递
 
Dispatch改造点,增加一种特殊队列存储任意时间延时 改动量比较大,可以增加一种消息类型即可改造
class CommitLogDispatcherBuildConsumeQueue implements CommitLogDispatcher {

@Override
public void dispatch(DispatchRequest request) {
final int tranType = MessageSysFlag.getTransactionValue(request.getSysFlag());
switch (tranType) {
case MessageSysFlag.TRANSACTION_NOT_TYPE:
case MessageSysFlag.TRANSACTION_COMMIT_TYPE:
DefaultMessageStore.this.putMessagePositionInfo(request);
break;
case MessageSysFlag.TRANSACTION_PREPARED_TYPE:
case MessageSysFlag.TRANSACTION_ROLLBACK_TYPE:
break;
}
}
}

最新文章

  1. Linux安装详情图解
  2. Delphi 各版本新特性功能网址收集
  3. js平滑返回顶部代码
  4. react拷贝index.html很恶心之解决办法
  5. Java EXCEL导入的两种方式JXL和POI
  6. LeetCode(6) - ZigZag Conversion
  7. Android精品课程—PullToRefresh 下拉刷新
  8. NodeJs + gm图片缩略图
  9. 听同事讲 Bayesian statistics: Part 1 - Bayesian vs. Frequentist
  10. Object-c学习之路四(oc内存管理autorelease)
  11. 大数据时代之hadoop(二):hadoop脚本解析
  12. C#3.0中的扩展方法
  13. 『线段树 Segment Tree』
  14. 如何给Windows2016新建IIS并建立网站
  15. JS 从剪贴板上传图片
  16. mysql恢复备份数据时,部分表数据丢失的问题
  17. U32592 摘果实
  18. Java学习--基本数据类型的定义和运算
  19. python tqdm函数
  20. Selenium2自动化测试实战序言

热门文章

  1. [python]Pytest+selenium+git+jenkins持续集成
  2. [python]pytest实现WEB UI自动化
  3. [第二章]c++学习笔记1(类和对象的基础2)
  4. 第五周PTA笔记 后缀表达式+后缀表达式计算
  5. 自由导入你的增量数据-根据条件将sqlserver表批量生成INSERT语句的存储过程实施笔记
  6. Kubernetes 中的 gRPC 负载均衡
  7. Django笔记&amp;教程 5-1 基础增删查改
  8. [hdu6326]Monster Hunter
  9. [atARC066F]Contest with Drinks Hard
  10. 【知识详解】JAVA基础(秋招总结)