RocketMQ 网络架构图
RocketMQ分布式消息队列的网络部署架构图如下图所示
于上图中几个角色的说明:
(1) NameServer: RocketMQ集群的命名服务器(也可以说是注册中心),它本身是无状态的(实际情况下可能存在每个NameServer实例上的数据有短暂的不一致现象,但是通过定时更新,在大部分情况下都是一致的),用于管理集群的元数据( 例如,KV配置、Topic、Broker的注册信息)。nameserver存有全量的路由信息,提供对等的读写服务,支持快速扩缩容。nameserver接收client(producer/consumer)的请求,根据消息的topic获取相应的broker路由信息。集群部署后,节点之间无任何信息同步。
(2)Broker(Master): RocketMQ消息代理服务器主节点,起到串联Producer的消息发送和Consumer的消息消费,和将消息的落盘存储的作用;
(3)Broker(Slave): RocketMQ消息代理服务器备份节点,主要是通过同步/异步的方式(同步复制:消息发给master的同时也会将消息发送给slave 异步双写:只发送到master,再由master异步同步到slave)将主节点的消息同步过来进行备份,为RocketMQ集群的高可用性提供保障;
(4)Producer(消息生产者): 在这里为普通消息的生产者,主要基于RocketMQ-Client模块将消息发送至RocketMQ的主节点(Broker Master)。一般由业务系统负责产生消息。消息有3种发送方式:同步、异步、单向。
(5)ProducerGroup(生产组): 通常具有同样作用(同样topic)的一些producer可以归为同一个group。在事务消息机制中,如果发送某条事务消息后的producer-A宕机,使得事务消息一直处于PREPARED状态并超时,则broker会回查同一个group的其他producer,确认这条消息应该commit还是rollback。
其他核心概念:
(1)topic(主题): 一种消息的逻辑分类(消息的类型),比如说你有订单类的消息,也有库存类的消息,那么就需要进行分类存储。生产者方面:发消息时需指定topic,可以有1-n个生产者发布1个topic的消息,也1个生产者可以发布不同topic的消息。消费者方面:收消息时需订阅topic,可以有1-n个消费者组订阅1个topic的消息,1个消费者组可以订阅不同topic的消息。1个消息必须指定1个topic,topic允许自动创建与手工创建,topic创建时需要指定broker,可以指定1个或多个,name server就是通过broker与topic的映射关系来做路由。producer和consumer在生产和消费消息时,都需要指定消息的 topic,当topic匹配时,consumer 才会消费到producer发送的消息。topic与broker是多对多的关系,一个topic分布在多个broker上,一个broker可以配置多个topic。
(2)message(消息): message是消息的载体。每个message必须指定一个topic,相当于寄信的地址。message还有一个可选的tag设置,以便消费端可以基于tag进行过滤消息。message还有扩展的kv结构,例如你可以设置一个业务key到你的消息中,在broker上查找消息并诊断问题。
(3)tag(标签): 标签可以被认为是对topic的进一步细化。一般在相同业务模块中通过引入标签来标记不同用途的消息。区分相同topic下不同种类的消息。生产到哪个topic的哪个tag下,消费者也是从topic的哪个tag进行消费,即实现消息的过滤。
(4)queue(队列): queue是消息的物理管理单位,而topic是逻辑管理单位。一个topic下可以有多个queue,默认自动创建是4个,手动创建是8个。queue的引入使得消息存储可以分布式集群化,具有了水平扩展的能力。1个message只能属于1个queue、1个topic。在rocketmq中,所有消息队列都是持久化,长度无限的数据结构,所谓长度无限是指队列中的每个存储单元都是定长,访问其中的存储单元使用offset来访问,offset 为 java long 类型,64 位,理论上在 100年内不会溢出,所以认为是长度无限,另外队列中只保存最近几天的数据,之前的数据会按照过期时间来删除。也可以认为 Message Queue是一个长度无限的数组,offset就是下标。
rocketmq中,producer将消息发送给broker时,需要指定发送到哪一个queue中,默认情况下,producer会轮询的将消息发送到每个queue中,顺序是随机的,但总体上每个queue的消息数量均分,所有broker下的queue合并成一个list去轮询,也可以由程序员通过MessageQueueSelector接口来指定具体发送到哪个queue中。
对于consumer而言,会为每个consumer分配固定的队列(如果队列总数没有发生变化),consumer从固定的队列中去拉取没有消费的消息进行处理。消费端会通过RebalanceService线程,10秒钟做一次基于topic下的所有队列负载,获取同一个Consumer Group下的所有Consumer实例数或Topic的queue的个数是否改变,通知所有Consumer实例重新做一次负载均衡算法。
(5) offset(消费进度): 理解成消费进度,可自增。
(6) commit log(存储文件): 虽然每个topic下面有很多message queue,但是message queue本身并不存储消息。真正的消息存储会写在CommitLog的文件,message queue只是存储CommitLog中对应的位置信息,方便通过message queue找到对应存储在CommitLog的消息。 不同的topic,message queue都是写到相同的CommitLog 文件,也就是说CommitLog完全的顺序写。
对于上面图中几条通信链路的关系:
(1)NamerServer和NamerServer: nameserver互相独立,彼此没有通信关系,单台nameserver挂掉,不影响其他nameserver。
(2)Broker和NamerServer: Broker(Master or Slave)均会和每一个NameServer实例来建立TCP连接,定时注册topic&broker的路由信息到所有name server中。Broker在启动的时候会注册自己配置的Topic信息到NameServer集群的每一台机器中。即每一个NameServer均有该broker的Topic路由配置信息。其中,Master与Master之间无连接,Master与Slave之间有连接;
(2)Producer、Consumer与NamerServer: 每一个Producer会与NameServer集群中的一个实例建立TCP连接,从这个NameServer实例上拉取Topic路由信息;如果该nameserver挂掉,消费者会自动连接下一个nameserver,直到有可用连接为止,并能自动重连。
(3)Producer、Consumer和Broker: Producer会和它要发送的topic相关联的Master的Broker代理服务器建立TCP连接,用于发送消息以及定时的心跳信息;集群消费模式下,一个消费者集群多台机器共同消费一个topic的多个队列,一个队列只会被一个消费者消费。如果某个消费者挂掉,分组内其它消费者会接替挂掉的消费者继续消费。
客户端发送普通消息的demo方法
public class ProducerTest {
//nameserver地址
private String namesrvAddr = "192.168.152.129:9876;192.168.152.130:9876";
private final DefaultMQProducer producer = new DefaultMQProducer("ProducerTest");
private String TOPIC_TEST = "TOPIC_TEST";
private String TAG_TEST = "TAG_TEST";
/**
* 初始化
*/
public void start() {
try {
System.out.println("MQ:启动ProducerTest生产者");
producer.setNamesrvAddr(namesrvAddr);
producer.start();
//发送消息
for(int i=0;i<100;i++) {
sendMessage("hello mq" + i);
}
} catch (MQClientException e) {
System.out.println("MQ:启动ProducerTest生产者失败:" + e.getResponseCode() + e.getErrorMessage());
throw new RuntimeException(e.getMessage(), e);
}
}
public void sendMessage(String data) {
System.out.println("MQ: 生产者发送消息 :{}" + data);
Message message = null;
try {
//转换成字符数组
byte[] messageBody = data.getBytes(RemotingHelper.DEFAULT_CHARSET);
//创建消息对象
message = new Message(TOPIC_TEST, TAG_TEST, String.valueOf(System.currentTimeMillis()), messageBody);
//同步发送消息
SendResult sendResult = producer.send(message);
//异步发送消息
/*producer.send(message, new SendCallback() {
public void onSuccess(SendResult sendResult) {
System.out.println("MQ: CouponProducer生产者发送消息" + sendResult);
}
public void onException(Throwable throwable) {
System.out.println(throwable.getMessage() + throwable);
}
});*/
//单向发送 只发送消息,不等待服务器响应,只发送请求不等待应答。
//producer.sendOneway(message);
} catch (Exception e) {
if (message != null) {
System.out.println("producerGroup:ProducerTest,Message:{}" + JSON.toJSON(message));
}
System.out.println("MQ: CouponProducer error :" + e);
}
}
@PreDestroy
public void stop() {
if (producer != null) {
producer.shutdown();
System.out.println("MQ:关闭ProducerTest生产者");
}
}
public static void main(String[] args) {
ProducerTest producerTest = new ProducerTest();
producerTest.start();
}
}
RocketMQ发送普通消息的全流程解读
消息生产者发送消息的demo代码还是较为简单的,核心就几行代码,但在深入研读RocketMQ的Client模块后,发现其发送消息的核心流程还是有一些复杂的。下面将主要从DefaultMQProducer的启动流程、send发送方法和Broker代理服务器的消息处理三方面分别进行分析和阐述。
DefaultMQProducer的启动流程
在客户端发送普通消息的demo代码部分,我们先是将DefaultMQProducer实例启动起来,里面调用了默认生成消息的实现类—DefaultMQProducerImpl的start()方法。
public class DefaultMQProducer extends ClientConfig implements MQProducer {
//处理发送消息的类内部类
protected final transient DefaultMQProducerImpl defaultMQProducerImpl;
public DefaultMQProducer(String producerGroup, RPCHook rpcHook) {
this.createTopicKey = "TBW102";
this.defaultTopicQueueNums = 4;
this.sendMsgTimeout = 3000;
this.compressMsgBodyOverHowmuch = 4096;
this.retryTimesWhenSendFailed = 2;
this.retryTimesWhenSendAsyncFailed = 2;
this.retryAnotherBrokerWhenNotStoreOK = false;
this.maxMessageSize = 4194304;
this.producerGroup = producerGroup;
//将自己作为参数传入,实现了两个类相互引用
this.defaultMQProducerImpl = new DefaultMQProducerImpl(this, rpcHook);
}
public DefaultMQProducer(String producerGroup) {
this(producerGroup, (RPCHook)null);
}
...
public void start() throws MQClientException {
//调用内部类的start方法
this.defaultMQProducerImpl.start();
}
...
}
让我们看看内部类DefaultMQProducerImpl是如何做的
public class DefaultMQProducerImpl implements MQProducerInner {
//重载的方法,默认传入true
public void start() throws MQClientException {
this.start(true);
}
//启动方法
public void start(boolean startFactory) throws MQClientException {
switch(this.serviceState) {
//第一次调用进入这个case
case CREATE_JUST:
//更改状态防止多次启动
this.serviceState = ServiceState.START_FAILED;
//验证groupname
this.checkConfig();
//获取了jvm进程id作为producer的intancename名。
if (!this.defaultMQProducer.getProducerGroup().equals("CLIENT_INNER_PRODUCER")) {
this.defaultMQProducer.changeInstanceNameToPID();
}
//获取MQClientManager单例对象,调用getAndCreateMQClientInstance,获取MQClientInstance(mq客户端对象实例)
this.mQClientFactory = MQClientManager.getInstance().getAndCreateMQClientInstance(this.defaultMQProducer, this.rpcHook);