1.RocketMQ概述
RocketMQ简介
RocketMQ是一个统一的消息传递引擎,轻量级的数据处理平台
MQ的作用
下面以订单系统为例
1.解耦
引入消息队列之前,下单完成之后,需要订单服务去调用库存服务减库存,调用消息服务去发送消息……引入消息队列之后,可以把订单完成的消息丢进队列里,下游服务自己去调用就行了,这样就完成了订单服务和其它服务的解耦合。
2.限流消峰
RocketMQ可以将系统的超量请求暂存其中,等到系统不繁忙时慢慢处理,从而避免了请求的丢失或者系统被压垮。
3.异步
例如订单支付之后,我们要扣减库存、增加积分、发送消息等,这样一来这个链路就长了,链路一长,响应时间就变长了。引入消息队列,除了更新订单状态,其它的都可以异步去做,这样一来就来,就能降低响应时间。
4.数据收集
分布式系统会产生许多数据,如:业务日志、监控数据、用户行为等,对这些数据进行实时或批量采集汇总,然后进行大数据分析。
2.RocketMQ的工作原理
1.基本概念
1.1 Message
Message是消息系统中所传输的信息,生产和消费的最小单位,每条消息必须属于一个主题(Topic)。
1.2 Topic
Topic(主题)表示一类消息的集合,是RocketMQ进行消息订阅的基本单位。
1.3 Queue
Queue是Topic中实际存储消息的物理实体,一个Topic中可以包含有多个Queue,每个Queue中存放的就是该topic的消息(Message)
1.4 Tag
Tag表示为消息设置的标签,区分同一Topic下不同类型的消息,来自同一业务的消息可以根据不同的目的或类型设置不同的标签,如:Topic:水果,Tag:西瓜或Tag:苹果。标签有助于保持你的代码干净和连贯,并且还可以为 RocketMQ 提供的查询系统提供帮助。
1.5 NameServer
NameServer是整个RocketMQ的“大脑”,是Broker的注册中心,支持Broker的动态路由和发现。一般为保证NameServer的高可用会部署多台NameServer服务器,但是彼此之间互不通信。这种设计会造成NameServer服务器在某一时刻数据不完整,但对消息的发送不会造成重大影响,只是短暂的消息发送不均衡,这也是RocketMQ NameServer的一个设计亮点。
1.6 Broker
Broker是RocketMQ的核心,提供消息的接收、存储和消费等功能。一般需要保证Broker的高可用会部署多台服务器(主从),分为master和slave,其中一台master可以对应多台slave(master和slave对应关系可以通过指定配置文件中相同的BrokerName,不同的BrokerId来定义,BrokerId为0表示master,BrokerId大于0表示slave),当master挂掉后,consumer可以消费slave。
1.7 Producer
消息的生产者,会先和NameServer集群中的随机一台建立连接,从NameServer中获取Topic路由信息,并向提供Topic服务的Broker Master建立连接,Producer无状态,支持多种消息发送方式。
1.8 Consumer
消息的消费者,同样与NameServer建立连接,从NameServer中获取Topic路由信息,并向提供Topic服务的Broker Master,Slave建立连接,支持集群消费和广播消费两种模式。
2.工作流程
2.1 系统架构
2.2 工作流程
1)启动NameServer,NameServer开始监听端口,等待Broker、Producer、Consumer连接。
2)启动Broker,Broker会向所有NameServer建立连接,每隔30s发送心跳包,包含Topic等信息。
3)Producer与NameServer建立连接获取最新的Broker信息,根据算法策略(随机、hash等)选择一台Broker建立连接并推送消息。
4)Consumer与Producer类似,与NameServer建立连接获取所定阅的Topic路由信息,根据算法策略(平均、一致性hash等)选择一个Queue建立连接并开始消费消息。
3.消息的生产
Producer可以将消息写入某个Broker中的某个Queue中。
3.1 消息发送流程
1)Producer向NameServe获取Broker的路由信息;
2)Producer根据指定的Queue选择算法从Broker路由信息中选择一个Queue;
3)Producer对消息进行处理(超过4M进行压缩等)后发送到选择的Queue上;
4)根据消息发送方式(同步发送、异步发送、单向发送)对Broker响应返回的结果进行后续处理;
3.1 Queue选择算法
Queue的选择分两种情况:指定选择算法、不指定选择算法。
3.1.1 发送时不指定选择算法
Producer在发送消息时可以配置是否开启故障延时机制,默认为false(不开启)。
开启故障延时机制时,选择Queue步骤为:
1)遍历主题中包含的所有队列,判断对应的Broker是否可用,如果可以使用则返回队列。
2)如果所有对应的Broker都不可使用,选择一个故障延时间时间戳较前的Broker,选择一个写队列返回。
不开启故障延时机制时,选择Queue步骤为:
1)上次选择的Broker为空时,直接轮询选择一个队列。
2)上次选择的Broker不为空时,则从上一次Broker的后一位开始轮询,找到不同与上一次的Broker返回。
开启启故障延时代码示例:
// 定义一个Producer
DefaultMQProducer producer = new DefaultMQProducer(groupName);
// 设置NameServer地址
producer.setNamesrvAddr(namesrvAddr);
// 开启故障延时机制
producer.setSendLatencyFaultEnable(true);
// 启动producer
producer.start();
3.1.2 发送时指定选择算法
RocketMQ提供了三种选择算法:
1)SelectMessageQueueByRandom – 随机
2)SelectMessageQueueByHash – hash
3)SelectMessageQueueByMachineRoom – 同机房
最后我们也可以自定义选择算法(实现MessageQueueSelector接口)
// 定义一个Producer
DefaultMQProducer producer = new DefaultMQProducer(groupName);
// 设置NameServer地址
producer.setNamesrvAddr(namesrvAddr);
// 启动producer
producer.start();
Message msg = new Message("TopicTest" ,"TagA",
("Hello RocketMQ ").getBytes(RemotingHelper.DEFAULT_CHARSET));
producer.send(msg, new MessageQueueSelector() {
@Override
public MessageQueue select(List<MessageQueue> mqs, Message msg, Object arg) {
// 自定义算法或指定算法
SelectMessageQueueByRandom selectMessageQueueByRandom = new SelectMessageQueueByRandom();
return selectMessageQueueByRandom.select(mqs, msg,arg);
}
}, null);
producer.shutdown();
3.2 发送方式
同步发送:消息发送到Broker,等待Broker返回响应结果后才会发送下一个消息。
异步发送:消息发送到Broker,不必等待Broker返回响应结果可以继续发送下一条消息,Broker处理完后调用回调函数。
单向发送:只负责发送消息,不等待Broker响应也没有回调函数。
4.消息的存储
RocketMQ存储文件主要包括:commitlog文件、consumequeue文件、index文件。RocketMQ将所有主题的消息存储在同一个文件中,确保消息发送时按顺序写文件,尽最大的能力确保消息发送的高性能和高吞吐量。
RocketMQ中的消息存储在本地文件系统中,这些相关文件默认在当前用户主目录下的store目录中。
·commitlog:消息存储目录,所有消息都存储在commitlog文件中
·config:运行期间的一些配置信息存储目录
·consumerqueue:消息消费队列存储目录
·index:消息索引文件存储目录
·abort:该文件在Broker启动后会自动创建,正常关闭Broker,该文件会自动消失
·checkpoint:文件检查点(存储commitLog文件、consumerqueue、index文件最后一次刷盘时间戳)
4.1 基本概念
1. commitlog
消息主题及元数据的存储主体,commitlog目录中存放着很多的mappedFile文件,当前Broker中的所有消息都是落盘到这些mappedFile文件中的。mappedFile文件大小为1G(小于等于1G),文件名由20位十进制数构成,表示当前文件的第一条消息的起始位移偏移量。
2. consumequeue
consumequeue文件可以看作基于topic的commitlog索引文件,所以consumequeue文件的组织方式为topic/queue/file三层组织结构,如下图:
consumequeue文件名也由20位数字构成,表示当前文件的第一个索引条目的起始位移偏移量。与commitlog文件名不同的是,其后续文件名是固定的。因为consumequeue文件大小是固定不变的。
每个consumequeue文件可以包含30w个索引条目,每个索引条目包含了三个消息重要属性:8个字节的commitlog物理偏移量、4个字节的消息长度、8个字节的tag哈希码。所以每个文件的大小是固定的30w * 20字节,约5.72MB。
3. index
index提供了一种可以通过key或时间区间来查询消息的方法,一个index文件可以保存 2000W个索引,index的底层存储设计为在文件系统中实现HashMap结构,故RocketMQ的索引文件其底层实现为hash索引。
4.2 刷盘策略
RocketMQ文件的存储是基于磁盘的顺序写消息,为提升性能RocketMQ又引入了内存映射,将磁盘文件映射到内存中,以操作内存的方式操作磁盘,将性能又提升一个档次。
在Java中可通过NIO的FileChannel的map方式创建内存映射文件,在Linux服务中由该方法创建文件使用的是操作系统的页缓存(pagecache),如果RocketMQ Broker进程异常退出,存储在缓存页中的数据不会丢失,不过如果是机器断电,存储在缓存页中的数据将会丢失。那么Broker在收到客户端消息后,是存储到页缓存就返回成功,还是持久化到磁盘文件中才返回成功呢?RocketMQ提供了两种策略:
同步刷盘:消息追加到内存映射文件后,立即将数据从内存写入磁盘。优点是能保证消息不丢失,但是是以牺牲写入性能为代价。
异步刷盘:消息存储到pagecache后就立即返回成功,然后开启一个异步线程定时将内存中的数据写入磁盘。
如果Broker为主从部署,消息需要从Master复制到Slave上,同样有同步和异步两种复制方式
同步复制:是等Master和Slave均写成功后才反馈给客户端写成功状态;
异步复制:是Master写成功后即可反馈给客户端写成功状态;
5.消息的消费
5.1 消费模式
消息消费是以消费组的模式开展,一个消费组可以包含多个消费者,消费组之间有集群模式和广播模式。
集群模式:当前主题(topic)下的同一条消息只允许被其中的一个消费者消费。
广播模式:当前主题(topic)下的同一条消息将被集群内的所有消费者消费一次。
两种模式下的消费进度保存:
广播模式下消费进度保存在consumer端。因为广播模式下消费组中每个消费者都会消费所有消息,但它们的消费进度是不同。所以消费者各自保存各自的消费进度。
集群模式下消费进度保存在broker中。消费组中的所有消费者共同消费同一个topic中的消息,同一条消息只会被消费一次。消费进度会参与到了消费的负载均衡中,故消费进度是需要共享的。Broker存储消息进度文件,如下:
消费进度(offset)管理
1. 集群消费模式下,消费者消费完消息后会向Broker提交消费进度offset,其提交方式分为两种:
同步提交: 消费者在消费完一批消息后会向broker提交这些消息的offset,然后等待broker的成功响应。若在等待超时之前收到了成功响应,则继续读取下一批消息进行消费(从ACK中获取nextBeginOffset)。若没有收到响应,则会重新提交,直到获取到响应。而在这个等待过程中,消费者是阻塞的。会影响了消费者的吞吐量。
异步提交: 消费者在消费完一批消息后向broker提交offset,但无需等待Broker的成功响应,可以继续读取并消费下一批消息。这种方式增加了消费者的吞吐量。但需要注意,Broker在收到提交的offset后,还是会向消费者进行响应的。
2.当消费模式为广播消费时,offset使用本地模式存储。因为每条消息会被所有的消费者消费,每个消费者管理自己的消费进度,各个消费者之间不存在消费进度的交集。Consumer在广播消费模式下offset相关数据以json的形式持久化到Consumer本地磁盘文件中,默认文件路径为当前用户主目录下的.rocketmq_offsets/{clientId}/{group}/Offsets.json 。其中{clientId}为当前消费者id,默认为ip@DEFAULT;${group}为消费者组名称。
5.2 消费机制
消息服务器与消费者之间的消息传送方式也有两种:推模式(push)和拉模式(pull)。
pull:拉取型消费者(Pull Consumer)主动从消息服务器拉取信息,只要批量拉取到消息,用户应用就会启动消费过程,所以 Pull 称为主动消费型。
push:推送型消费者(Push Consumer)封装了消息的拉取、消费进度和其他的内部维护工作,将消息到达时执行的回调接口留给用户应用程序来实现。所以 Push 称为被动消费类型,但其实从实现上看还是从消息服务器中拉取消息,不同于 Pull 的是 Push 首先要注册消费监听器,当监听器处触发后才开始消费消息。
5.3 Rebalance
Rebalance机制讨论的前提是:集群消费。Rebalance即再均衡,指的是,将一个Topic下的多个Queue在同一个Consumer Group中的多个Consumer间进行重新分配的过程。一般在生产过程中会根据消息的消费情况,增加或减少Broker与Consumer的数量,这时就会触发Rebalance机制。
优点
Rebalance机制的本意是为了提升消息的并行消费能力。例如,一个Topic下5个队列,在只有1个消费者的情况下,这个消费者将负责消费这5个队列的消息。如果此时我们增加一个消费者,那么就可以给其中一个消费者分配2个队列,给另一个分配3个队列,从而提升消息的并行消费能力。
缺点
Rebalance的在提升消费能力的同时,也带来一些问题:
消费暂停:在只有一个Consumer时,其负责消费所有队列;在新增了一个Consumer后会触发Rebalance的发生。此时原Consumer就需要暂停部分队列的消费,等到这些队列分配给新的Consumer后,这些暂停消费的队列才能继续被消费。
消费重复:Consumer 在消费新分配给自己的队列时,必须接着之前Consumer 提交的消费进度的offset继续消费。然而默认情况下,offset是异步提交的,这个异步性导致提交到Broker的offset与Consumer实际消费的消息并不一致。这个不一致的差值就是可能会重复消费的消息。
分配算法
RocketMQ默认提供5种分配算法:
1)AllocateMessageQueueAveragely:平均分配,推荐使用。
举例来说,如果有8个消费消费队列,q1、q2、q3、q4、q5、q6、q7、q8,有3个消费者c1、c2、c3,那么根据该算法,消息队列分配如下,
c1:q1、q2、q3
c2:q4、q5、q6
c3:q7、q8
2)AllocateMessageQueueAveragelyByCircle:平均轮询分配,推荐使用。
同样像上面例子,如果有8个消费消费队列,q1、q2、q3、q4、q5、q6、q7、q8,有3个消费者c1、c2、c3,那么根据该算法,消息队列分配如下,
c1:q1、q4、q7
c2:q2、q5、q8
c3:q3、q6
3)AllocateMessageQueueConsistentHash:一致性哈希。因为消息队列负载信息不容易跟踪,所以不推荐使用。
4)AllocateMessageQueueByConfig:根据配置,为每一个消费者配置固定的消费队列。
5)AllocateMessageQueueByMachineRoom:根据Broker部署机房名,对每个消费者负责不同的Broker上的队列。
5.4 消息过滤
消息者在进行消息订阅时,除了可以指定要订阅消息的Topic外,还可以对指定Topic中的消息根据指定条件进行过滤,对于指定Topic消息的过滤有两种过滤方式:Tag过滤与SQL过滤。
1.Tag过滤
通过consumer的subscribe()方法指定要订阅消息的Tag。如果订阅多个Tag的消息,Tag间使用或运算
符(双竖线||)连接,如:
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("CID_EXAMPLE");
consumer.subscribe("TOPIC", "TAGA || TAGB || TAGC");
2.SQL过滤
SQL过滤是一种通过特定表达式对事先埋入到消息中的用户属性进行筛选过滤的方式。通过SQL过滤,可以实现对消息的复杂过滤。不过,只有使用push模式的消费者才能使用SQL过滤。默认情况下Broker没有开启消息的SQL过滤功能,需要在Broker加载的配置文件中添加如下属性,以开启该功能:
enablePropertyFilter = true
SQL过滤举例:
// 生产者
DefaultMQProducer producer = new DefaultMQProducer("pg");
producer.setNamesrvAddr("rocketMQ01:9876");
producer.start();
byte[] body = ("Hi").getBytes();
Message msg = new Message("myTopic", "myTag", body);
// 设置属性
msg.putUserProperty("age", "18");
SendResult sendResult = producer.send(msg);
// 消费者
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("pg");
consumer.setNamesrvAddr("rocketMQ01:9876");
// 订阅topic及设置SQL过滤
consumer.subscribe("myTopic", MessageSelector.bySql("age between 1 and 20"));
...
6.ACL
6.1 什么是ACL
ACL(Access Control List,访问控制表)与我们在应用系统中接触到的用户、资源、权限、角色有些类似,在RocketMQ中对应如下对象:
1)用户:用户是访问控制的基本要素,支持用户名,密码。
2)资源:需要保护的对象。在RocketMQ中,消息发送涉及的topic和消息消费涉及的消费组都应该进行保护,故可抽象为资源。
3)权限:可以简单地将权限理解为可以对资源进行操作,在RocketMQ ACL中主要包含topic的发送权限、对topic的订阅权限等。
4)角色:RocketMQ定义了两种角色,管理员和非管理员
RocketMQ通过引入ACL功能,可以对topic的发送与订阅进行强管控。公司内部各个项目组如果需要消费topic中的数据,必须得到运维团队的授权,极大提高了MQ集群的安全性,便于管理。
6.2 如何使用ACL
客户端(项目组)要使用ACL功能,首先需要开启Broker服务端的ACL功能。
Broker端开启ACL
1)在Broker端配置文件中添加下面参数
aclEnable = true
2)将RocketMQ中的distribution/conf/acl/plain_acl.yml文件复制到${ROCKETMQ_HOME}/conf目录下,其配置如下:
RocketMQ ACL中的可选权限编号如下:
1)DENY:拒绝
2)PUB:拥有发送权限
3)SUB:拥有订阅权限
注意:因为要修改Broker端的配置文件,所以需要重启Broker服务器才能生效,但是后续修改pain_acl.yml文件不用重启Broker服务器。
客户端使用ACL
1.发送端代码示例:
public static void producer() throws MQClientException {
DefaultMQProducer producer = new DefaultMQProducer("ProducerGroupName",getAclRPCHook());
producer.setNamesrvAddr("rocketMQ01:9876");
producer.start();
for (int i = 0; i < 128; i++){
try {
{
Message msg = new Message("TopicTest",
"TagA",
"OrderID188",
"Hello world".getBytes(RemotingHelper.DEFAULT_CHARSET));
SendResult sendResult = producer.send(msg);
System.out.printf("%s%n", sendResult);
}
} catch (Exception e) {
e.printStackTrace();
}
producer.shutdown();
}
}
static RPCHook getAclRPCHook() {
return new AclClientRPCHook(new SessionCredentials("RocketMQ","1234567"));
}
2.消费端代码示例:
public static void pushConsumer() throws MQClientException {
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("consumerGroup",
getAclRPCHook(), new AllocateMessageQueueAveragely());
consumer.setNamesrvAddr("rocketMQ01:9876");
consumer.subscribe("TopicTest", "*");
consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_FIRST_OFFSET);
consumer.registerMessageListener(new MessageListenerConcurrently() {
@Override
public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs,
ConsumeConcurrentlyContext context) {
printBody(msgs);
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
}
});
consumer.start();
System.out.printf("Consumer Started.%n");
}
static RPCHook getAclRPCHook() {
return new AclClientRPCHook(new SessionCredentials("RocketMQ","1234567"));
}
7. 主从同步
RocketMQ的Broker分为Master和Slave两个角色,为了保证高可用性(HA), Master角色的机器接收到消息后,要把内容同步到Slave机器上,这样一旦Master宕机,Slave机器依然可以提供服务。大致步骤如下:
Master端:
1)监听端口
org.apache.rocketmq.store.ha.HAService.AcceptSocketService#beginAccept
2)建立连接
org.apache.rocketmq.store.ha.HAService.AcceptSocketService#run
3)读取slave上报的maxOffset
org.apache.rocketmq.store.ha.HAConnection.ReadSocketService#run
4)传输数据给slave
org.apache.rocketmq.store.ha.HAConnection.WriteSocketService#run
Slave端:
1)连接master
org.apache.rocketmq.store.ha.HAService.HAClient#connectMaster
2)定时报告maxOffset给master
org.apache.rocketmq.store.ha.HAService.HAClient#run
3)接收master传输来的数据
org.apache.rocketmq.store.ha.HAService.HAClient#processReadEvent
交互图:
8. 主从切换
RocketMQ 4.5.0版本之前只提供了主从同步功能,主从同步时如果master宕机了生产者就无法向master写入消息,但消费者可以继续从slave节点消费消息,即集群的主节点一旦宕机,整个Broker集群的写负载能力会名显下降,这时如果从节点可以代替主节点提供写入消息的能力就能极大地提供集群的可用性,所以RocketMQ 4.5.0开始正式支持这一特性。
8.1 Raft协议简介
在复制组内实现主从切换的一个基本前提是复制组内各个节点的数据必须一致,否则主从切换后将会造成数据丢失。Raft协议是目前分布式领域一个非常重要的一致性协议,RocketMQ的主从切换机制也是介于Raft协议实现的。Raft协议主要包含有两个部分:Leader选举和日志复制。
8.1.1 Leader选举
Raft协议的核心思想的在一个复制组内选举一个Leader节点,后续统一由Leader节点处理客户端的读写请求,从节点只是从Leader节点复制数据,即一个复制组在接收客户端的读写请求之前,要先从复制组选择一个Leader节点,这个过程称为Leader选举。
Raft协议的选举过程如下:
1)各个节点的初始状态为Follower,每个节点会设置一个计时器,每个节点的计时器时间是150~300ms的一个随机值。
2)节点计时器到期后,状态会从Follower变为Candidate,进入该状态的节点会发起一轮投票,首先为自己投上一票,然后向集群中的其他节点发起“拉票”,期待得到超过半数的选票支持。
3)当集群内的节点收到投票请求后,如果该节点本轮未进行投票,则投赞成票,否则投反对票,然后返回结果并重置计时器继续倒数计时。如果计时器到期,则状态会由Follower变更为Candidate。
4)如果节点收到的赞成票超过集群节点数的一半,则状态变更为主节点。如果本轮没有节点得到集群超过半数的赞成票,则继续下一轮投票。
5)主节点会定时向集群内的所有从节点发送心跳包。从节点收到心跳包后重置计时器,这是主节点维持其“统治地位”的手段。因为从节点一旦计时器到期,就会由Follower变更为Candidate,以此来尝试发起新的一轮选举。
8.1.2 日志复制
客户端向DLedger集群发起一个写数据的请求,Leader节点收到写请求后先将数据存入Leader节点,然后将数据广播给它的所有从节点。从节点收到Leader节点的数据推送后对数据进行存储,然后向主节点汇报存储的结果。Leader节点会对该日志的存储结果进行仲裁,如果超出集群数量一半都成功存储了该数据,则向客户端返回写入成功,否则向客户端返回写入失败。
8.2 RocketMQ主从切换配置
主从切换的核心参数:
// 是否开启主从切换机制,默认为false
enableDLegerCommitLog = true
// 节点所属的raft复制组,建议与brokerName保持一致
dLegerGroup = broker-a
// 集群节点信息,多个节点用英文冒号隔开,单个条目遵循ID、IP地址、端口,这里的端口用于Raft集群内部通信
dLegerPees = n0-100.100.218.103:40911;n1-100.100.218.104:40911;n2-100.100.218.105:40911
// 当前节点ID,取自dLegerPees中条目的开头
dLegerSelfId = n0
// 日志文件的存储根目录,为了支持平滑升级,改值应该与storePathCommitLog设置为补同目录
storePathRootDir = ~/store/dledger_store
在broker配置文件配置上述参数后,再将store/config目录下的所有文件复制到dledger store的config目录下
cd ~/store/
cp config/* dledger_store/config/
之后依次启动Broker则主从切换配置完成。
9. 死信队列
死信队列用于处理无法被正常消费的消息,即死信消息。
当一条消息初次消费失败,消息队列 RocketMQ 会自动进行消息重试;达到最大重试次数后,若消费依然失败,则表明消费者在正常情况下无法正确地消费该消息,此时,消息队列 RocketMQ 不会立刻将消息丢弃,而是将其发送到该消费者对应的特殊队列中,该特殊队列称为死信队列(Dead-Letter Queue,DLQ),而其中的消息则称为死信消息(Dead-Letter Message,DLM)。
9.1 死信队列的特征
死信队列具有如下特征:
1)死信队列中的消息不会再被消费者正常消费,即DLQ对于消费者是不可见的。
2)死信存储有效期与正常消息相同,均为 3 天(commitlog文件的过期时间),3 天后会被自动删除。
3)死信队列就是一个特殊的Topic,名称为%DLQ%consumerGroup@consumerGroup ,即每个消
费者组都有一个死信队列。
4)如果一个消费者组未产生死信消息,则不会为其创建相应的死信队列。
当一条消息进入死信队列,就意味着系统中某些地方出现了问题,从而导致消费者无法正常消费该消息,比如代码中原本就存在Bug。因此,对于死信消息,通常需要开发人员进行特殊处理。
3.RocketMQ的应用
3.1 普通消息
Producer对于消息的发送方式也有多种选择,不同的方式会产生不同的系统效果。
3.1.1 同步发送
同步发送消息是指,Producer发出⼀条消息后,会在收到MQ返回的ACK之后才发下一条消息。该方式的消息可靠性最高,但消息发送效率低。在发送时使用send方法则为同步发送。
DefaultMQProducer producer = new DefaultMQProducer("please_rename_unique_group_name");
producer.setNamesrvAddr("rocketMQ01:9876");
producer.start();
Message msg = new Message("TopicTest" /* Topic */,
"TagA" /* Tag */,
("Hello RocketMQ ").getBytes(RemotingHelper.DEFAULT_CHARSET)
// Producer的send方法为同步方式发送消息
SendResult sendResult = producer.send(msg);
producer.shutdown();
3.1.2 异步发送
异步发送消息是指,Producer发出消息后无需等待MQ返回ACK,直接发送下一条消息。该方式的消息可靠性可以得到保障,消息发送效率也可以。在发送时使用send方法指定回调函数则为异步发送。
DefaultMQProducer producer = new DefaultMQProducer("please_rename_unique_group_name");
producer.setNamesrvAddr("rocketMQ01:9876");
producer.start();
Message msg = new Message("TopicTest" /* Topic */,
"TagA" /* Tag */,
("Hello RocketMQ ").getBytes(RemotingHelper.DEFAULT_CHARSET)
// 指定回调方法
producer.send(msg, new SendCallback() {
// 发送成功时回调
@Override
public void onSuccess(SendResult sendResult) {
// do something
}
// 发送异常时回调
@Override
public void onException(Throwable e) {
// do something
}
}
3.1.2 单向消息
单向发送消息是指,Producer仅负责发送消息,不等待、不处理MQ的ACK。该发送方式时MQ也不返回ACK。该方式的消息发送效率最高,但消息可靠性较差。在发送消息时使用sendOneway方法则为单向发送。
DefaultMQProducer producer = new DefaultMQProducer("please_rename_unique_group_name");
producer.setNamesrvAddr("rocketMQ01:9876");
producer.start();
Message msg = new Message("TopicTest" /* Topic */,
"TagA" /* Tag */,
("Hello RocketMQ ").getBytes(RemotingHelper.DEFAULT_CHARSET)
// Producer的send方法为同步方式发送消息
SendResult sendResult = producer.sendOneway(msg);
3.2 顺序消息
顺序消息是指消息的消费顺序和产生顺序相同,在有些业务逻辑下,必须保证顺序,比如订单的生成、付款、发货,这个消息必须按顺序处理才行。顺序消息分为全局顺序消息和部分顺序消息.
全局顺序消息指某个 Topic 下的所有消息都要保证顺序;
部分顺序消息只要保证每一组消息被顺序消费即可,比如订单消息,只要保证同一个订单 ID 个消息能按顺序消费即可。
3.2.1 部分顺序消息
部分顺序消息相对比较好实现,生产端需要做到把同 ID 的消息发送到同一个 Message Queue ;在消费过程中,要做到从同一个Message Queue读取的消息顺序处理——消费端不能并发处理顺序消息,这样才能达到部分有序。
部分顺序消息相对比较好实现,生产端需要做到把同 ID 的消息发送到同一个 Message Queue ;在消费过程中,要做到从同一个Message Queue读取的消息顺序处理——消费端不能并发处理顺序消息,
发送端使用 MessageQueueSelector 类来控制 把消息发往哪个 Message Queue 。
DefaultMQProducer producer = new DefaultMQProducer("please_rename_unique_group_name");
producer.setNamesrvAddr("rocketMQ01:9876");
producer.start();
for (int i = 0; i < 100; i++) {
int orderId = i * 10;
Message msg =new Message("OrderTopic", "order",("Hello RocketMQ " + i).getBytes(RemotingHelper.DEFAULT_CHARSET));
// 发送时自定义消息选择器
SendResult sendResult = producer.send(msg, new MessageQueueSelector() {
@Override
public MessageQueue select(List<MessageQueue> qs, Message msg, Object arg) {
Integer id = (Integer) arg;
int index = id % qs.size();
return mqs.get(index);
}
}, orderId);
}
消费端通过使用 MessageListenerOrderly 来解决单 Message Queue 的消息被并发处理的问题。
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("please_rename_unique_group_name");
consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_FIRST_OFFSET);
consumer.subscribe("OrderTopic", "order");
// 使用MessageListenerOrderly
consumer.registerMessageListener(new MessageListenerOrderly() {
AtomicLong consumeTimes = new AtomicLong(0);
@Override
public ConsumeOrderlyStatus consumeMessage(List<MessageExt> msgs, ConsumeOrderlyContext context) {
context.setAutoCommit(true);
this.consumeTimes.incrementAndGet();
if ((this.consumeTimes.get() % 2) == 0) {
return ConsumeOrderlyStatus.SUCCESS;
} else if ((this.consumeTimes.get() % 5) == 0) {
context.setSuspendCurrentQueueTimeMillis(3000);
return ConsumeOrderlyStatus.SUSPEND_CURRENT_QUEUE_A_MOMENT;
}
return ConsumeOrderlyStatus.SUCCESS;
}
});
consumer.start();
3.2.2 全局顺序消息
RocketMQ 默认情况下不保证顺序,比如创建一个 Topic ,默认四个写队列,四个读队列,这时候一条消息可能被写入任意一个队列里;在数据的读取过程中,可能有多个 Consumer ,每个 Consumer 也可能启动多个线程并行处理,所以消息被哪个 Consumer 消费,被消费的顺序和写人的顺序是否一致是不确定的。
要保证全局顺序消息, 需要先把 Topic 的读写队列数设置为 一,然后Producer Consumer 的并发设置,也要是一。简单来说,为了保证整个 Topic全局消息有序,只能消除所有的并发处理,各部分都设置成单线程处理 ,这时候就完全牺牲RocketMQ的高并发、高吞吐的特性了。
3.3 延时消息
3.3.1 什么是延时消息
当消息写入到Broker后,在指定的时间后才可被消费处理的消息,称为延时消息。典型的应用场景是,电商交易中超时未支付关闭订单的场景。RocketMQ是支持延时消息的,只需要在生产消息的时候设置消息的延时级别。
3.3.2 延时等级
RocketMQ延时消息的延时时长不支持随意时长的延迟,是通过特定的延时等级来指定的。延时等级定义在RocketMQ服务端的MessageStoreConfig类中的,如下变量:
我们也可以自定义延时等级,可以在Broker加载的配置文件中(RocketMQ安装目录下的conf目录中)增加如下配置(增加了1天的配置):
messageDelayLevel = 1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h 1d
3.3.3 延时消息举例
在生产消息的时候设置消息的延时级别
DefaultMQProducer producer = new DefaultMQProducer("please_rename_unique_group_name");
producer.setNamesrvAddr("rocketMQ01:9876");
producer.start();
Message msg =new Message("delayTopic", "delay","Hello, delay".getBytes(RemotingHelper.DEFAULT_CHARSET));
// 设置延时等级为3,即10s后推送给消费者
// 1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h
message.setDelayTimeLevel(3);
SendResult sendResult = producer.sendOneway(msg);
3.4 事务消息
3.4.1 半消息
半消息:是指暂时还不能被 Consumer 消费的消息,Producer 成功发送到 Broker 端的消息,但是此消息被标记为 “暂不可投递” 状态,只有等 Producer 端执行完本地事务后经过二次确认了之后,Consumer 才能消费此条消息。
3.4.2 事务消息流程
依赖半消息,可以实现分布式消息事务,其中的关键在于二次确认以及消息回查,RocketMQ事务消息流程为:
1)producer 发送事务消息到broker;
2)broker收到消息并将消息持久化存储,返回ACK给producer(此时consume无法消费该消息);
3)producer开始执行本地事务;
4)producer根据事务的执行结果向broker提交确认,commit或rollback;
5)broker如果收到commit,则将消息标记为可投递,此时consumer可以消费该消息,如果收到的是rollback则将消息删除;
6)如果broker长时间未收到producer的提交结果会对该消息进行回查;
7)最终根据查询的事务最终状态执行第5步;
举例:
/**
* 定义一个事务监听
*/
public class TransactionListenerImpl implements TransactionListener {
private AtomicInteger transactionIndex = new AtomicInteger(0);
private ConcurrentHashMap<String, Integer> localTrans = new ConcurrentHashMap<>();
/**
* 消息成功发送到broker,本地事务方法
* @param msg Half(prepare) message
* @param arg Custom business parameter
* @return
*/
@Override
public LocalTransactionState executeLocalTransaction(Message msg, Object arg) {
// 假设接收到TAGA的消息就表示扣款操作成功,TAGB的消息表示扣款失败,
// TAGC表示扣款结果不清楚,需要执行消息回查
if (StringUtils.equals("TAGA", msg.getTags())) {
return LocalTransactionState.COMMIT_MESSAGE;
} else if (StringUtils.equals("TAGB", msg.getTags())) {
return LocalTransactionState.ROLLBACK_MESSAGE;
} else if (StringUtils.equals("TAGC", msg.getTags())) {
return LocalTransactionState.UNKNOW;
}
return LocalTransactionState.UNKNOW;
}
/**
* 回查方法
* @param msg Check message
* @return
*/
@Override
public LocalTransactionState checkLocalTransaction(MessageExt msg) {
System.out.println("执行消息回查" + msg.getTags());
return LocalTransactionState.COMMIT_MESSAGE;
}
}
生产者代码
// 消息监听
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"};
for (int i = 0; i < 3; i++) {
byte[] body = ("Hi," + i).getBytes();
Message msg = new Message("TTopic", tags[i], body);
// 发送事务消息,第二个参数用于指定在执行本地事务时要使用的业务参数
SendResult sendResult = producer.sendMessageInTransaction(msg,null);
}
3.5 批量消息
3.5.1 发送限制
生产者进行消息发送时可以一次发送多条消息,这样可以大大的提升消息的发送效率,不过在发送时需要注意:
1)批量发送的消息必须具有相同的Topic;
2)批量发送的消息必须具有相同的刷盘策略;
3)批量发送的消息不能是延时消息和事务消息;
3.5.2 发送大小
RocketMQ默认情况下,一批发送的消息总大小不能超过4MB,如果想超过该值,有两种解决方案:
1)将批量消息进行拆分,拆分成若干个不大于4MB的消息集合分多次批量发送;
2)在Producer和Broker端修改属性(Producer发送时设置maxMessageSize属性,Broker端修改其配置文件中的maxMessageSize属性);
生产者
DefaultMQProducer producer = new DefaultMQProducer("please_rename_unique_group_name");
producer.setNamesrvAddr("rocketMQ01:9876");
// 修改最大消息限制
producer.setMaxMessageSize(5 * 1024 * 1024);
producer.start();
String topic = "BatchTest";
List<Message> messages = new ArrayList<>();
messages.add(new Message(topic, "Tag", "OrderID001", "Hello world 0".getBytes()));
messages.add(new Message(topic, "Tag", "OrderID002", "Hello world 1".getBytes()));
messages.add(new Message(topic, "Tag", "OrderID003", "Hello world 2".getBytes()));
producer.send(messages);
消费者
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("cg");
consumer.setNamesrvAddr("rocketMQ01:9876");
consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_FIRST_OFFSET);
consumer.subscribe("BatchTest", "*");
// 指定每次可以消费10条消息,默认为1
consumer.setConsumeMessageBatchMaxSize(10);
// 指定每次可以从Broker拉取40条消息,默认为32
consumer.setPullBatchSize(40);
consumer.registerMessageListener(new MessageListenerConcurrently() {
@Override
public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs,
ConsumeConcurrentlyContext context) {
for (MessageExt msg : msgs) {
System.out.println(msg);
}
// 消费成功的返回结果
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
}
});
附1 RocketMQ集群搭建实践
1.前期准备
主要搭建一个双主双从异步复制的Broker集群,准备两台liunx,具体分配如下:
序号 | IP/域名 | 功能 | 角色 |
---|---|---|---|
1 | rocketMQ01 | NameServer + Broker | master(a)+ slave(b) |
2 | rocketMQ02 | NameServer + Broker | master(b)+ slave(a) |
两台linux软硬件要求如下:
系统要求64位,jdk1.8及已上版本
官网下载最新安装包:
上传至linux并解压:
unzip rocketmq-all-4.9.3-bin-release.zip
如果本地内存不足,可以修改初始内存:
修改runserver.sh
打开安装包/bin/runserver.sh文件,修改如下配置:
修改runbroker.sh
打开安装包/bin/runbroker.sh文件,修改如下配置:
两台liunx都按照上述操作准备完成,并保证网络互通。
2.修改配置文件
修改启动配置文件,文件在rocketMQ解压目录的conf/2m-2s-async目录中(首先修改rocketMQ01)
修改broker-a.properties,将该配置文件内容修改为如下:
修改broker-b-s.properties,将该配置文件内容修改为如下:
继续修改rocketMQ02启动配置文件,文件在rocketMQ解压目录的conf/2m-2s-async目录中
修改broker-b.properties,将该配置文件内容修改为如下:
修改broker-a-s.properties,将该配置文件内容修改为如下:
附 Broker配置文件部分属性说明:
#指定整个broker集群的名称,或者说是RocketMQ集群的名称
brokerClusterName=rocket-MS
#指定master-slave集群的名称。一个RocketMQ集群可以包含多个master-slave集群
brokerName=broker-a
#0 表示 Master,>0 表示 Slave
brokerId=0
#nameServer地址,分号分割
namesrvAddr=nameserver1:9876;nameserver2:9876
#默认为新建Topic所创建的队列数
defaultTopicQueueNums=4
#是否允许 Broker 自动创建Topic,建议生产环境中关闭
autoCreateTopicEnable=true
#是否允许 Broker 自动创建订阅组,建议生产环境中关闭
autoCreateSubscriptionGroup=true
#Broker对外提供服务的端口,即Broker与producer与consumer通信的端口
listenPort=10911
#HA高可用监听端口,即Master与Slave间通信的端口,默认值为listenPort+1
haListenPort=10912
#指定删除消息存储过期文件的时间为凌晨4点
deleteWhen=04
#指定未发生更新的消息存储文件的保留时长为48小时,48小时后过期,将会被删除
fileReservedTime=48
#指定commitLog目录中每个文件的大小,默认1G
mapedFileSizeCommitLog=1073741824
#指定ConsumeQueue的每个Topic的每个Queue文件中可以存放的消息数量,默认30w条
mapedFileSizeConsumeQueue=300000
#在清除过期文件时,如果该文件被其他线程所占用(引用数大于0,比如读取消息),此时会阻止
此次删除任务,同时在第一次试图删除该文件时记录当前时间戳。该属性则表示从第一次拒绝删除
后开始计时,该文件最多可以保留的时长。在此时间内若引用数仍不为0,则删除仍会被拒绝。不过
时间到后,文件将被强制删除
destroyMapedFileIntervalForcibly=120000
#指定commitlog、consumequeue所在磁盘分区的最大使用率,超过该值,则需立即清除过期文
件
diskMaxUsedSpaceRatio=88
#指定store目录的路径,默认在当前用户主目录中
storePathRootDir=~/store
#commitLog目录路径
storePathCommitLog=~/store/commitlog
#consumeueue目录路径
storePathConsumeQueue=~/store/consumequeue
#index目录路径
storePathIndex=~/store/index
#checkpoint文件路径
storeCheckpoint=~/store/checkpoint
#abort文件路径
abortFile=~/store/abort
#指定消息的最大大小
maxMessageSize=65536
#Broker的角色
# - ASYNC_MASTER 异步复制Master
# - SYNC_MASTER 同步双写Master
# - SLAVE
brokerRole=SYNC_MASTER
#刷盘策略
# - ASYNC_FLUSH 异步刷盘
# - SYNC_FLUSH 同步刷盘
flushDiskType=SYNC_FLUSH
#发消息线程池数量
sendMessageThreadPoolNums=128
#拉消息线程池数量
pullMessageThreadPoolNums=128
#强制指定本机IP,需要根据每台机器进行修改。官方介绍可为空,系统默认自动识别,但多网卡
时IP地址可能读取错误
brokerIP1=100.100.218.9
3.部署启动
启动NameServer集群
进入RocketMQ目录下分别启动两台设备上的NameServer,启动命令相同:
nohup sh bin/mqnamesrv &
tail -f ~/logs/rocketmqlogs/namesrv.log
启动Broker集群
进入RocketMQ目录下分别启动两台设备上的master,注意指定的要加载的配置文件是不同的
nohup sh bin/mqbroker -c conf/2m-2s-async/broker-a.properties &
tail -f ~/logs/rocketmqlogs/broker.log
nohup sh bin/mqbroker -c conf/2m-2s-async/broker-b.properties &
tail -f ~/logs/rocketmqlogs/broker.log
进入RocketMQ目录下分别启动两台设备上的slave,注意指定的要加载的配置文件是不同的
nohup sh bin/mqbroker -c conf/2m-2s-async/broker-b-s.properties &
tail -f ~/logs/rocketmqlogs/broker.log
nohup sh bin/mqbroker -c conf/2m-2s-async/broker-a-s.properties &
tail -f ~/logs/rocketmqlogs/broker.log
到这一步,RocketMQ集群部署完成,RocketMQ自带测试功能,可以简单测试
发送消息(RocketMQ安装目录下,本次安装是4.9.3版本,不同版本测试路径可能不一样):
export NAMESRV_ADDR=localhost:9876
sh bin/tools.sh org.apache.rocketmq.example
接收消息(RocketMQ安装目录下):
sh bin/tools.sh org.apache.rocketmq.example.quickstart.Consumer
4.控制台安装(rocketMQ-console)
RocketMQ有一个可视化的dashboard,通过该控制台可以直观的查看到很多数据,本地部署如下:
1.下载
下载地址:https://github.com/apache/rocketmq-externals/releases(网络限制可能打不开)
其他下载:https://download.csdn.net/download/qh631315724/85354534
2.修改配置
修改其src/main/resources中的application.properties配置文件:
1)原来的端口号为8080,可以修改为一个不常用的。
2)指定RocketMQ的name server地址。
3.打包
在rocketmq-console目录下运行maven的打包命令,得到jar文件:
mvn clean package -Dmaven.test.skip=true
4.启动
java -jar rocketmq-console-ng-1.0.1.jar
5.访问
注 可以部署到远程服务器上
附2 RocketMQ源码
1. 源码下载
首先就是到 Github 网站上下载源码。
下载地址:https://github.com/apache/rocketmq
下载完成成后解压,导入idea
2.导入源码
导入完成后的目录为
各方模块的作用为:
acl:用户权限相关。
broker:RocketMQ 的 Broker 相关的代码,用来启动 Broker 进程。
client:RocketMQ 的 Producer、Consumer 这些客户端的代码,用来生产消息、消费消息。
common:公共模块。
dev:开发相关的一些信息
distribution:用来部署 RocketMQ 的,比如 bin 目录 ,conf 目录。
example:使用 RocketMQ 的例子。
filter:RocketMQ 的一些过滤器。
logging:RocketMQ 日志相关的。
namesvr:NameServer 的源码。
openmessaging:开放消息标准。
remoting:RocketMQ 的远程网络通信模块的代码,基于netty实现。
srvutil:里面有很多工具类。
store:消息如何在Broker上进行存储的。
style:代码检查相关的。
test:测试相关的。
tools:命令行监控工具相关。
3 启动NameServer
在 namesrv 模块找到类 org.apache.rocketmq.namesrv.NamesrvStartup ,并运行它的 main 函数:
运行程序时出现以下报错:
Please set the ROCKETMQ_HOME variable in your environment to match the location of the RocketMQ installation
Run -> Edit Configurations… 打开以下界面,点击:
你可以把这个值设置成你当前项目目录的路径(例如,我的项目路径是ROCKETMQ_HOME=E:\rocketMQ\soure\rocketmq-rocketmq-all-4.9.3):
ROCKETMQ_HOME 表示 RocketMQ 安装的根目录。
再次启动保错(缺少配置文件logback_namesrv.xml)
Connected to the target VM, address: '127.0.0.1:15521', transport: 'socket'
ch.qos.logback.core.joran.spi.JoranException: Could not open [E:\rocketMQ\soure\conf\logback_namesrv.xml].
at ch.qos.logback.core.joran.GenericConfigurator.doConfigure(GenericConfigurator.java:85)
at ch.qos.logback.core.joran.GenericConfigurator.doConfigure(GenericConfigurator.java:72)
at org.apache.rocketmq.namesrv.NamesrvStartup.createNamesrvController(NamesrvStartup.java:119)
at org.apache.rocketmq.namesrv.NamesrvStartup.main0(NamesrvStartup.java:57)
at org.apache.rocketmq.namesrv.NamesrvStartup.main(NamesrvStartup.java:51)
Caused by: java.io.FileNotFoundException: E:\rocketMQ\soure\conf\logback_namesrv.xml (系统找不到指定的路径。)
at java.io.FileInputStream.open(Native Method)
at java.io.FileInputStream.<init>(FileInputStream.java:138)
at ch.qos.logback.core.joran.GenericConfigurator.doConfigure(GenericConfigurator.java:80)
... 4 more
我们把路径为 项目目录\distribution\conf\logback_namesrv.xml 拷贝到 项目目录\conf\logback_namesrv.xml
再次启动Nameserver,控制台打印出以下文本,则表示 NameServer 启动成功了:
The Name Server boot success. serializeType=JSON
4 启动Broker
在 broker 模块找到类 org.apache.rocketmq.broker.BrokerStartup ,并运行它的 main 函数:
运行程序时出现以下报错:
Please set the ROCKETMQ_HOME variable in your environment to match the location of the RocketMQ installationDisconnected from the target VM, address: '127.0.0.1:16462', transport: 'socket'
还是设置环境变量 ROCKETMQ_HOME,方法同本文 NameServer中的方法。配置完成后再次启动保错:
ch.qos.logback.core.joran.spi.JoranException: Could not open [E:\rocketMQ\soure\rocketmq-rocketmq-all-4.9.3\conf\logback_broker.xml].
at ch.qos.logback.core.joran.GenericConfigurator.doConfigure(GenericConfigurator.java:85)
at ch.qos.logback.core.joran.GenericConfigurator.doConfigure(GenericConfigurator.java:72)
at org.apache.rocketmq.broker.BrokerStartup.createBrokerController(BrokerStartup.java:188)
at org.apache.rocketmq.broker.BrokerStartup.main(BrokerStartup.java:57)
Caused by: java.io.FileNotFoundException: E:\rocketMQ\soure\rocketmq-rocketmq-all-4.9.3\conf\logback_broker.xml (系统找不到指定的文件。)
at java.io.FileInputStream.open(Native Method)
at java.io.FileInputStream.<init>(FileInputStream.java:138)
at ch.qos.logback.core.joran.GenericConfigurator.doConfigure(GenericConfigurator.java:80)
... 3 more
类似的,我们把路径为 项目目录\distribution\conf\logback_broker.xml 拷贝到 项目目录\conf\logback_broker.xml中后再次启动。控制台打印如下信息,表示 Broker 启动成功:
The broker[你的主机名, 你的主机IPv4地址:10911] boot success. serializeType=JSON
5. 运行Producer
在 example 模块找到类 org.apache.rocketmq.example.quickstart.Producer :
修改Nameserver为你本地地址后
运行Producer,发现报错:
org.apache.rocketmq.client.exception.MQClientException: No route info of this topic: TopicTest
See http://rocketmq.apache.org/docs/faq/ for further details.
at org.apache.rocketmq.client.impl.producer.DefaultMQProducerImpl.sendDefaultImpl(DefaultMQProducerImpl.java:675)
at org.apache.rocketmq.client.impl.producer.DefaultMQProducerImpl.send(DefaultMQProducerImpl.java:1367)
at org.apache.rocketmq.client.impl.producer.DefaultMQProducerImpl.send(DefaultMQProducerImpl.java:1311)
at org.apache.rocketmq.client.producer.DefaultMQProducer.send(DefaultMQProducer.java:335)
at org.apache.rocketmq.example.quickstart.Producer.main(Producer.java:68)
这个错误的原因通常有:
1)Broker禁止自动创建Topic,且用户没有通过手工方式创建Topic
2)Broker没有正确连接到Name Server
3)Producer没有正确连接到Name Server
第三点排除,我们已经在代码中明确指定了 Name Server 的地址。
所以修改Broker启动配置文件, 我们把路径为 项目目录\distribution\conf\broker.conf 拷贝到 项目目录\conf\broker.conf 中。
在 broker.conf 下面追加以下两个属性:
namesrvAddr=localhost:9876
autoCreateTopicEnable=true
然后在 Run -> Edit Configurations… 修改程序启动参数:
启动参数 -c 项目目录\conf\broker.conf 表示指定配置文件路径,然后重新启动Broker。
启动成功后,再次运行Producer,发送成功。
6. 运行Consumer
在 example 模块找到类 org.apache.rocketmq.example.quickstart.Consumer ,修改Nameserver为你本地地址后启动。
至此,源码搭建完成完