1.RocketMq
RocketMQ的前身是Metaq,当Metaq3.0发布时,产品名称改为RocketMQ,有以下特点:
1. 能够保证严格的消息顺序2. 提供丰富的消息拉取模式
3. 高效的订阅者水平扩展能力
4. 实时的消息订阅机制
5. 亿级消息堆积能力
2.核心原理
2.1. 数据结构
(1)所有数据单独储存到commit Log ,完全顺序写,随机读
(2)对最终用户展现的队列实际只储存消息在Commit Log 的位置信息,并且串行方式刷盘
(3)按照MessageId查询消息
(4)根据查询的key的hashcode%slotNum得到具体的槽位置
(5)根据slotValue(slot对应位置的值)查找到索引项列表的最后一项
(6)遍历索引项列表返回查询时间范围内的结果集
2.2. 刷盘策略
rocketmq中的所有消息都是持久化的,先写入系统pagecache,然后刷盘,可以保证内存与磁盘都有一份数据,访问时,可以直接从内存读取
2.3. 内存机制
2.4. 工作模式
集群方式 | 运维特点 | 消息可靠性(master宕机情况) | 服务可用性(master宕机情况) | 其他特点 | 备注 |
---|---|---|---|---|---|
单Master | 结构简单,扩容方便,机器要求低 | 同步刷盘消息一条都不会丢 | 整体可用 未被消费的消息无法取得,影响实时性 | 性能最高 | 适合消息可靠性最高、实时性低的需求 |
多Master | 异步有毫秒级丢失 同步双写不丢失 | 差评,主备不能自动切换,且备机只能读不能写,会造成服务整体不可写。 | 不考虑,除非自己提供主从切换的方案 | ||
Master-Slave(异步复制) | 结构复杂,扩容方便 | 故障时会丢失消息 | 整体可用,实时性影响毫秒级别 该组服务只能读不能写 | 性能很高 | 适合消息可靠性中等,实时性中等的要求 |
Master-Slave(同步双写) | 结构复杂,扩容方便 | 不丢消息 | 整体可用,不影响实时性 该组服务只能读不能写 | 性能比异步低10%,所以实时性也并不比异步方式太高 | 适合消息可靠性略高,实时性中等、性能要求不高的需求 |
tip
第四种的官方介绍上,比第三种多说了一句:“不支持主从自动切换”。这句话让我很恐慌,因为第三种也是不支持的,干嘛第四种偏偏多说这一句,难道可用性上比第三种差?
于是做了实验,证明第三种和第四种可用性是一模一样的。那么不支持主从切换是什么意思?推断编写者是这个意图:
因为是主从双写的,所以数据一致性非常高,那么master挂了之后,slave本是可以立刻切换为主的,这一点与异步复制不一样。异步复制并没有这么高的一致性,所以这一句话并不是提醒,而是一个后续功能的备注,可以在双写的架构上继续发展出自动主从切换的功能。
3. 环境安装
3.1. RocketMq安装
https://github.com/alibaba/RocketMQ/releases下载3.2.6,解压
3.2. 环境变量配置
export JAVA_HOME=/usr/local/jdk1.7.0_79
export ROCKETMQ_HOME=/usr/local/alibaba-rocketmq
export CLASSPATH=/usr/local/jdk1.7.0_79/lib
export PATH=$JAVA_HOME/bin:$PATH:$ROCKETMQ_HOME/bin
4. 测试网络拓扑
因为手里没有其他服务器,105那台缺少一个slave,在同步双写模式下,发送消息会返回
SLAVE_NOT_AVAILABLE,不过消息已经发送成功,只是slave没有写成功。
5. 启停操作
这里只给出一个基本的示例,各个模式的启停在本文最后的参考文献中会有详细的说明。这里不再赘述。
新建日志文件夹
cd /usr/local/alibaba-rocketmq mkdir log touch log/ng.log touch log/ng-error.log touch log/mq.log
- 启动nameserver
nohup sh mqnamesrv 1>$ROCKETMQ_HOME/log/ng.log 2>$ROCKETMQ_HOME/log/ng-error.log &
- 验证nameserver是否启动
$tail -f $ROCKETMQ_HOME/log/ng.log
The Name Server boot success.
- 停止nameServer
sh mqshutdown namesrv
The mqnamesrv(12248) is running...
Send shutdown request to mqnamesrv(12248) OK
- 启动broker(单master)(多master,多master+slave)对应的(异步复制,同步双写)
nohup sh mqbroker -n 192.168.1.101:9876 -c $ROCKETMQ_HOME/conf/2m-noslave/broker-a.properties >$ROCKETMQ_HOME/log/mq.log &
- 验证mqbroker是否启动
tail -f $ROCKETMQ_HOME/log/mq.log
Java HotSpot(TM) 64-Bit Server VM warning: ignoring option PermSize=128m; support was removed in 8.0
Java HotSpot(TM) 64-Bit Server VM warning: ignoring option MaxPermSize=320m; support was removed in 8.0
Java HotSpot(TM) 64-Bit Server VM warning: UseCMSCompactAtFullCollection is deprecated and will likely be removed in a future release.
The broker[e010125001186.bja, 10.125.1.186:10911] boot success. and name server is 10.125.1.186:9876
- 停止broker
sh mqshutdown broker
The mqbroker(13634) is running...
Send shutdown request to mqbroker(13634) OK
6. 运维指令
- 查看集群情况
./mqadmin clusterList -n 127.0.0.1:9876
- 查看broker状态
./mqadmin brokerStatus -n 127.0.0.1:9876 -b 192.168.146.105:10911
- 查看topic列表
./mqadmin topicList -n 127.0.0.1:9876
- 查看topic状态
./mqadmin topicStatus -n 127.0.0.1:9876 -t PushTopic
- 查看topic路由
./mqadmin topicRoute -n 127.0.0.1:9876 -t PushTopic
7. 基本测试
基本测试采用Java直接编码的方式生产和消费消息,例子来源于参考文献的《RocketMQ开发教程》。
- Producer
package com.somnus.rocketmq;
import java.util.concurrent.TimeUnit;
import com.alibaba.rocketmq.client.exception.MQClientException;
import com.alibaba.rocketmq.client.producer.DefaultMQProducer;
import com.alibaba.rocketmq.client.producer.SendResult;
import com.alibaba.rocketmq.common.message.Message;
public class Producer {
public static void main(String[] args) throws MQClientException,
InterruptedException {
/**
* 一个应用创建一个Producer,由应用来维护此对象,可以设置为全局对象或者单例<br>
* 注意:ProducerGroupName需要由应用来保证唯一<br>
* ProducerGroup这个概念发送普通的消息时,作用不大,但是发送分布式事务消息时,比较关键,
* 因为服务器会回查这个Group下的任意一个Producer
*/
DefaultMQProducer producer = new DefaultMQProducer("ProducerGroupName");
producer.setNamesrvAddr("172.16.235.77:9876;172.16.235.78:9876");
producer.setInstanceName("Producer");
/**
* Producer对象在使用之前必须要调用start初始化,初始化一次即可<br>
* 注意:切记不可以在每次发送消息时,都调用start方法
*/
producer.start();
/**
* 下面这段代码表明一个Producer对象可以发送多个topic,多个tag的消息。
* 注意:send方法是同步调用,只要不抛异常就标识成功。但是发送成功也可会有多种状态,<br>
* 例如消息写入Master成功,但是Slave不成功,这种情况消息属于成功,但是对于个别应用如果对消息可靠性要求极高,<br>
* 需要对这种情况做处理。另外,消息可能会存在发送失败的情况,失败重试由应用来处理。
*/
for (int i = 0; i < 10; i++) {
try {
{
Message msg = new Message("TopicTest11",// topic
"TagA", // tag
"OrderID001", // key
("Hello MetaQ").getBytes()); // body
SendResult sendResult = producer.send(msg);
System.out.println(sendResult);
}
{
Message msg = new Message("TopicTest12",// topic
"TagB", // tag
"OrderID002", // key
("Hello MetaQ").getBytes()); // body
SendResult sendResult = producer.send(msg);
System.out.println(sendResult);
}
{
Message msg = new Message("TopicTest13",// topic
"TagC", // tag
"OrderID003", // key
("Hello MetaQ").getBytes()); // body
SendResult sendResult = producer.send(msg);
System.out.println(sendResult);
}
} catch (Exception e) {
e.printStackTrace();
}
TimeUnit.MILLISECONDS.sleep(1000);
}
/**
* 应用退出时,要调用shutdown来清理资源,关闭网络连接,从MetaQ服务器上注销自己
* 注意:我们建议应用在JBOSS、Tomcat等容器的退出钩子里调用shutdown方法
*/
producer.shutdown();
}
}
- Consumer
package com.somnus.rocketmq;
import java.util.List;
import com.alibaba.rocketmq.client.consumer.DefaultMQPushConsumer;
import com.alibaba.rocketmq.client.consumer.listener.ConsumeConcurrentlyContext;
import com.alibaba.rocketmq.client.consumer.listener.ConsumeConcurrentlyStatus;
import com.alibaba.rocketmq.client.consumer.listener.MessageListenerConcurrently;
import com.alibaba.rocketmq.client.exception.MQClientException;
import com.alibaba.rocketmq.common.message.MessageExt;
public class Consumer {
/**
* 当前例子是PushConsumer用法,使用方式给用户感觉是消息从RocketMQ服务器推到了应用客户端。<br>
* 但是实际PushConsumer内部是使用长轮询Pull方式从MetaQ服务器拉消息,然后再回调用户Listener方法<br>
*/
public static void main(String[] args) throws InterruptedException,
MQClientException {
/**
* Consumer组名,多个Consumer如果属于一个应用,订阅同样的消息,且消费逻辑一致,则应该将它们归为同一组
* 一个应用创建一个Consumer,由应用来维护此对象,可以设置为全局对象或者单例<br>
* 注意:ConsumerGroupName需要由应用来保证唯一
*/
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("ConsumerGroupName");
consumer.setNamesrvAddr("172.16.235.77:9876;172.16.235.78:9876");
consumer.setInstanceName("Consumber");
/**
* 订阅指定topic下tags分别等于TagA或TagB或TagC
*/
consumer.subscribe("TopicTest11", "TagA || TagB || TagC");
/**
* 订阅指定topic下所有消息<br>
* 注意:一个consumer对象可以订阅多个topic
*/
/*consumer.subscribe("TopicTest12", "*");*/
consumer.registerMessageListener(new MessageListenerConcurrently() {
/**
* 默认msgs里只有一条消息,可以通过设置consumeMessageBatchMaxSize参数来批量接收消息
* consumeThreadMin:消费线程池数量 默认最小值10
* consumeThreadMax:消费线程池数量 默认最大值20
*/
@Override
public ConsumeConcurrentlyStatus consumeMessage(
List<MessageExt> msgs, ConsumeConcurrentlyContext context) {
System.out.println(Thread.currentThread().getName()
+ " Receive New Messages: " + msgs.size());
MessageExt msg = msgs.get(0);
if (msg.getTopic().equals("TopicTest11")) {
// 执行TopicTest1的消费逻辑
if (msg.getTags() != null
&& msg.getTags().equals("TagA")) {
// 执行TagA的消费
System.out.println("TopicTest11------>"+new String(msg.getBody()));
} else if (msg.getTags() != null
&& msg.getTags().equals("TagB")) {
// 执行TagC的消费
} else if (msg.getTags() != null
&& msg.getTags().equals("TagC")) {
// 执行TagD的消费
}
} else if (msg.getTopic().equals("TopicTest12")) {
/*System.out.println("TopicTest2------>"+new String(msg.getBody()));*/
}
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
}
});
/**
* Consumer对象在使用之前必须要调用start初始化,初始化一次即可<br>
*/
consumer.start();
System.out.println("Consumer Started.");
}
}
运行结果:
8.宕机实验
9.参考文献
1) 《RocketMq入门(上)》
2) 《RocketMq入门(下)》
3) 《Rokectmq开发教程》
4) 《阿里Rocketmq Quict Start》
5) 《RocketMQ与Kafka对比(18项差异)》
6) 《RocketMq命令整理》
7) 《RocketMq原理简介》
RocketMQ网络部署图
RocketMQ 网络部署特点
Name Server 是一个几乎无状态节点,可集群部署,节点之间无任何信息同步。
Broker 部署相对复杂,Broker 分为Master 与Slave,一个Master 可以对应多个Slave,但是一个Slave 只能对应一个Master,Master 与Slave 的对应关系通过指定相同的BrokerName,不同的BrokerId来定 义,BrokerId为0 表示Master,非0 表示Slave。Master 也可以部署多个。每个Broker 与Name
Producer 与Name Server 集群中的其中一个节点(随机选择)建立长连接,定期从Name Server 取Topic 路由信息,并向提供Topic 服务的Master 建立长连接,且定时向Master 发送心跳。Producer 完全无 状态,可集群部署。
Consumer 与Name Server 集群中的其中一个节点(随机选择)建立长连接,定期从Name Server 取Topic 路由信息,并向提供Topic 服务的Master、Slave 建立长连接,且定时向Master、Slave 发送心跳。Consumer既可以从Master 订阅消息,也可以从Slave 订阅消息,订阅规则由Broker 配置决定。
Broker集群部署方式主要有以下几种:(Slave 不可写,但可读)
单个Master
这种方式风险较大,一旦Broker 重启或者宕机时,会导致整个服务不可用,不建议线上环境使用。
多Master模式
一个集群无 Slave,全是 Master,例如 2 个 Master 或者 3 个 Master。
优点:配置简单,单个Master 宕机或重启维护对应用无影响,在磁盘配置为 RAID10 时,即使机器宕机不可恢复情况下,由于RAID10 磁盘非常可靠,消息也不会丢(异步刷盘丢失少量消息,同步刷盘一条不丢)。性能最高。
缺点:单台机器宕机期间,这台机器上未被消费的消息在机器恢复之前不可订阅,消息实时性会受到受到影响。
先启动 NameServer,例如机器 IP 为:192.168.1.101:9876
nohup sh mqnamesrv &
- 在机器 A,启动第一个 Master
nohup sh mqbroker -n 192.168.1.101:9876 -c $ROCKETMQ_HOME/conf/2m-noslave/broker-a.properties &
- 在机器 B,启动第二个 Master
nohup sh mqbroker -n 192.168.1.102:9876 -c $ROCKETMQ_HOME/conf/2m-noslave/broker-b.properties &
多Master多Slave模式,异步复制
每个 Master 配置一个 Slave,有多对Master-Slave,HA 采用异步复制方式,主备有短暂消息延迟,毫秒级。
优点:即使磁盘损坏,消息丢失的非常少,且消息实时性不会受影响,因为 Master 宕机后,消费者仍然可以从 Slave 消费,此过程对应用透明。不需要人工干预。性能同多 Master 模式几乎一样。
缺点:Master宕机,磁盘损坏情况,会丢失少量消息。
先启动两台服务器的NameServer,例如机器 IP 为:192.168.1.101:9876 和192.168.1.102:9876
nohup sh mqnamesrv 1>$ROCKETMQ_HOME/log/ng.log 2>$ROCKETMQ_HOME/log/ng-error.log &
- 在机器 A,启动第一个 Master
nohup sh mqbroker -n 192.168.1.101:9876 -c $ROCKETMQ_HOME/conf/2m-2s-async/broker-a.properties >$ROCKETMQ_HOME/log/mq.log &
- 在机器 B,启动第二个 Master
nohup sh mqbroker -n 192.168.1.102:9876 -c $ROCKETMQ_HOME/conf/2m-2s-async/broker-b.properties >$ROCKETMQ_HOME/log/mq.log &
- 在机器 C,启动第一个 Slave
nohup sh mqbroker -n 192.168.1.101:9876 -c $ROCKETMQ_HOME/conf/2m-2s-async/broker-a-s.properties >$ROCKETMQ_HOME/log/mq.log &
- 在机器 D,启动第二个 Slave
nohup sh mqbroker -n 192.168.1.102:9876 -c $ROCKETMQ_HOME/conf/2m-2s-async/broker-b-s.properties >$ROCKETMQ_HOME/log/mq.log &
多Master多Slave模式,同步双写
每个 Master 配置一个 Slave,有多对Master-Slave,HA 采用同步双写方式,主备都写成功,向应用返回成功。
优点:数据与服务都无单点,Master宕机情况下,消息无延迟,服务可用性与数据可用性都非常高
缺点:性能比异步复制模式略低,大约低10%左右,发送单个消息的 RT 会略高。目前主宕机后,备机不能自动切换为主机,后续会支持自动切换功能。
先启动两台服务器的NameServer,例如机器 IP 为:192.168.1.101:9876 和192.168.1.102:9876
nohup sh mqnamesrv 1>$ROCKETMQ_HOME/log/ng.log 2>$ROCKETMQ_HOME/log/ng-error.log &
- 在机器 A,启动第一个 Master
nohup sh mqbroker -n 192.168.1.101:9876 -c $ROCKETMQ_HOME/conf/2m-2s-sync/broker-a.properties >$ROCKETMQ_HOME/log/mq.log &
- 在机器 B,启动第二个 Master
nohup sh mqbroker -n 192.168.1.102:9876 -c $ROCKETMQ_HOME/conf/2m-2s-sync/broker-b.properties >$ROCKETMQ_HOME/log/mq.log &
- 在机器 C,启动第一个 Slave
nohup sh mqbroker -n 192.168.1.101:9876 -c $ROCKETMQ_HOME/conf/2m-2s-sync/broker-a-s.properties >$ROCKETMQ_HOME/log/mq.log &
- 在机器 D,启动第二个 Slave
nohup sh mqbroker -n 192.168.1.102:9876 -c $ROCKETMQ_HOME/conf/2m-2s-sync/broker-b-s.properties >$ROCKETMQ_HOME/log/mq.log &
以上 Broker 与 Slave 配对是通过指定相同的brokerName 参数来配对,Master 的 BrokerId 必须是
0,Slave 的BrokerId 必须是大与 0 的数。另外一个 Master 下面可以挂载多个 Slave,同一 Master 下的多个
Slave 通过指定不同的 BrokerId 来区分。
除此之外,nameserver也需要集群。
下面以配置一主一备(同步),2个nameserver为例测试。
1、环境两台机器:
- 192.168.36.101 为主
- 192.168.36.102 为备
同时在2台机器个启动一个nameserver。安装RocketMq请参考:
http://blog.csdn.net/zhu_tianwei/article/details/40948447
2、修改配置
(1)创建目录
mkdir /usr/local/alibaba-rocketmq/log #创建日志目录
mkdir -p /usr/local/alibaba-rocketmq/data/store/commitlog #创建数据存储目录
更改日志目录
cd /usr/local/alibaba-rocketmq/conf
sed -i 's#${user.home}#${user.home}/alibaba-rocketmq#g' *.xml
(2)修改主配置
vi ./conf/2m-2s-sync/broker-a.properties
#Broker所属哪个集群,默认【DefaultCluster】
brokerClusterName=DefaultCluster
#本机主机名
brokerName=broker-a
#BrokerId,必须是大等于0的整数,0表示Master,>0表示Slave,一个Master可以挂多个Slave,Master与Slave通过BrokerName来配对,默认【0】
brokerId=0
#删除文件时间点,默认凌晨4点
deleteWhen=04
#文件保留时间,默认48小时
fileReservedTime=48
#Broker的角色 - ASYNC_MASTER 异步复制Master - SYNC_MASTER 同步双写Master - SLAVE
brokerRole=SYNC_MASTER
#刷盘方式 - ASYNC_FLUSH 异步刷盘 - SYNC_FLUSH 同步刷盘
flushDiskType=ASYNC_FLUSH
#Name Server地址
namesrvAddr=192.168.1.101:9876;192.168.1.102:9876
#Broker对外服务的监听端口,默认【10911】
listenPort=10911
defaultTopicQueueNums=4
#是否允许Broker自动创建Topic,建议线下开启,线上关闭,默认【true】
autoCreateTopicEnable=true
#是否允许Broker自动创建订阅组,建议线下开启,线上关闭,默认【true】
autoCreateSubscriptionGroup=true
mapedFileSizeCommitLog=1073741824
mapedFileSizeConsumeQueue=50000000
destroyMapedFileIntervalForcibly=120000
redeleteHangedFileInterval=120000
diskMaxUsedSpaceRatio=88
storePathRootDir=/usr/local/alibaba-rocketmq/data/store
storePathCommitLog=/usr/local/alibaba-rocketmq/data/store/commitlog
maxMessageSize=65536
flushCommitLogLeastPages=4
flushConsumeQueueLeastPages=2
flushCommitLogThoroughInterval=10000
flushConsumeQueueThoroughInterval=60000
checkTransactionMessageEnable=false
sendMessageThreadPoolNums=128
pullMessageThreadPoolNums=128
(3)修改备配置
vi ./conf/2m-2s-sync/broker-a-s.properties
#Broker所属哪个集群,默认【DefaultCluster】
brokerClusterName=DefaultCluster
#本机主机名
brokerName=broker-a
#BrokerId,必须是大等于0的整数,0表示Master,>0表示Slave,一个Master可以挂多个Slave,Master与Slave通过BrokerName来配对,默认【0】
brokerId=1
#删除文件时间点,默认凌晨4点
deleteWhen=04
#文件保留时间,默认48小时
fileReservedTime=48
#Broker的角色 - ASYNC_MASTER 异步复制Master - SYNC_MASTER 同步双写Master - SLAVE
brokerRole=SLAVE
#刷盘方式 - ASYNC_FLUSH 异步刷盘 - SYNC_FLUSH 同步刷盘
flushDiskType=ASYNC_FLUSH
#Name Server地址
namesrvAddr=192.168.1.101:9876;192.168.1.102:9876
#Broker对外服务的监听端口,默认【10911】
listenPort=10911
defaultTopicQueueNums=4
#是否允许Broker自动创建Topic,建议线下开启,线上关闭,默认【true】
autoCreateTopicEnable=true
#是否允许Broker自动创建订阅组,建议线下开启,线上关闭,默认【true】
autoCreateSubscriptionGroup=true
mapedFileSizeCommitLog=1073741824
mapedFileSizeConsumeQueue=50000000
destroyMapedFileIntervalForcibly=120000
redeleteHangedFileInterval=120000
diskMaxUsedSpaceRatio=88
storePathRootDir=/usr/local/alibaba-rocketmq/data/store
storePathCommitLog=/usr/local/alibaba-rocketmq/data/store/commitlog
maxMessageSize=65536
flushCommitLogLeastPages=4
flushConsumeQueueLeastPages=2
flushCommitLogThoroughInterval=10000
flushConsumeQueueThoroughInterval=60000
checkTransactionMessageEnable=false
sendMessageThreadPoolNums=128
pullMessageThreadPoolNums=128
实例:
1.生产者Producer.java ,TransactionMQProducer使用
package com.somnus.rocketmq;
import java.util.concurrent.TimeUnit;
import com.alibaba.rocketmq.client.exception.MQClientException;
import com.alibaba.rocketmq.client.producer.LocalTransactionExecuter;
import com.alibaba.rocketmq.client.producer.LocalTransactionState;
import com.alibaba.rocketmq.client.producer.SendResult;
import com.alibaba.rocketmq.client.producer.TransactionCheckListener;
import com.alibaba.rocketmq.client.producer.TransactionMQProducer;
import com.alibaba.rocketmq.common.message.Message;
import com.alibaba.rocketmq.common.message.MessageExt;
public class Producer {
public static void main(String[] args) throws MQClientException, InterruptedException {
/**
* 一个应用创建一个Producer,由应用来维护此对象,可以设置为全局对象或者单例<br>
* 注意:ProducerGroupName需要由应用来保证唯一,一类Producer集合的名称,这类Producer通常发送一类消息,
* 且发送逻辑一致<br>
* ProducerGroup这个概念发送普通的消息时,作用不大,但是发送分布式事务消息时,比较关键,
* 因为服务器会回查这个Group下的任意一个Producer
*/
final TransactionMQProducer producer = new TransactionMQProducer("ProducerGroupName");
// nameserver服务
producer.setNamesrvAddr("172.16.235.77:9876;172.16.235.78:9876");
producer.setInstanceName("Producer");
/**
* Producer对象在使用之前必须要调用start初始化,初始化一次即可<br>
* 注意:切记不可以在每次发送消息时,都调用start方法
*/
producer.start();
// 服务器回调Producer,检查本地事务分支成功还是失败
producer.setTransactionCheckListener(new TransactionCheckListener() {
public LocalTransactionState checkLocalTransactionState(
MessageExt msg) {
System.out.println("checkLocalTransactionState --" + new String(msg.getBody()));
return LocalTransactionState.COMMIT_MESSAGE;
}
});
/**
* 下面这段代码表明一个Producer对象可以发送多个topic,多个tag的消息。
* 注意:send方法是同步调用,只要不抛异常就标识成功。但是发送成功也可会有多种状态,<br>
* 例如消息写入Master成功,但是Slave不成功,这种情况消息属于成功,但是对于个别应用如果对消息可靠性要求极高,<br>
* 需要对这种情况做处理。另外,消息可能会存在发送失败的情况,失败重试由应用来处理。
*/
for (int i = 0; i < 10; i++) {
try {
{
Message msg = new Message("TopicTest1", // topic
"TagA", // tag
"OrderID001", // key消息关键词,多个Key用KEY_SEPARATOR隔开(查询消息使用)
("Hello MetaQA").getBytes()); // body
SendResult sendResult = producer.sendMessageInTransaction(
msg, new LocalTransactionExecuter(){
public LocalTransactionState executeLocalTransactionBranch(Message msg, Object arg) {
System.out.println("executeLocalTransactionBranch--msg=" + new String(msg.getBody()));
System.out.println("executeLocalTransactionBranch--arg=" + arg);
return LocalTransactionState.COMMIT_MESSAGE;
}
},
"$$$");
System.out.println(sendResult);
}
{
Message msg = new Message("TopicTest2", // topic
"TagB", // tag
"OrderID0034", // key 消息关键词,多个Key用KEY_SEPARATOR隔开(查询消息使用)
("Hello MetaQB").getBytes()); // body
SendResult sendResult = producer.sendMessageInTransaction(
msg, new LocalTransactionExecuter(){
public LocalTransactionState executeLocalTransactionBranch(Message msg, Object arg) {
System.out.println("executeLocalTransactionBranch--msg=" + new String(msg.getBody()));
System.out.println("executeLocalTransactionBranch--arg=" + arg);
return LocalTransactionState.COMMIT_MESSAGE;
}
},
"$$$");
System.out.println(sendResult);
}
{
Message msg = new Message("TopicTest3", // topic
"TagC", // tag
"OrderID061", // key
("Hello MetaQC").getBytes()); // body
SendResult sendResult = producer.sendMessageInTransaction(
msg, new LocalTransactionExecuter(){
public LocalTransactionState executeLocalTransactionBranch(Message msg, Object arg) {
System.out.println("executeLocalTransactionBranch--msg=" + new String(msg.getBody()));
System.out.println("executeLocalTransactionBranch--arg=" + arg);
return LocalTransactionState.COMMIT_MESSAGE;
}
},
"$$$");
System.out.println(sendResult);
}
} catch (Exception e) {
e.printStackTrace();
}
TimeUnit.MILLISECONDS.sleep(1000);
}
/**
* 应用退出时,要调用shutdown来清理资源,关闭网络连接,从MetaQ服务器上注销自己
* 注意:我们建议应用在JBOSS、Tomcat等容器的退出钩子里调用shutdown方法
*/
// producer.shutdown();
Runtime.getRuntime().addShutdownHook(new Thread(new Runnable() {
public void run() {
producer.shutdown();
}
}));
System.exit(0);
} // 执行本地事务,由客户端回调
}
2、消费者Consumer.java ,采用主动拉取方式消费。
package com.somnus.rocketmq;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import com.alibaba.rocketmq.client.consumer.DefaultMQPullConsumer;
import com.alibaba.rocketmq.client.consumer.PullResult;
import com.alibaba.rocketmq.client.exception.MQClientException;
import com.alibaba.rocketmq.common.message.MessageExt;
import com.alibaba.rocketmq.common.message.MessageQueue;
public class Consumer {
// Java缓存
private static final Map<MessageQueue, Long> offseTable = new HashMap<MessageQueue, Long>();
/**
* 主动拉取方式消费
*
* @throws MQClientException
*/
public static void main(String[] args) throws MQClientException {
/**
* 一个应用创建一个Consumer,由应用来维护此对象,可以设置为全局对象或者单例<br>
* 注意:ConsumerGroupName需要由应用来保证唯一 ,最好使用服务的包名区分同一服务,一类Consumer集合的名称,
* 这类Consumer通常消费一类消息,且消费逻辑一致
* PullConsumer:Consumer的一种,应用通常主动调用Consumer的拉取消息方法从Broker拉消息,主动权由应用控制
*/
DefaultMQPullConsumer consumer = new DefaultMQPullConsumer("ConsumerGroupName");
// //nameserver服务
consumer.setNamesrvAddr("172.16.235.77:9876;172.16.235.78:9876");
consumer.setInstanceName("Consumber");
consumer.start();
// 拉取订阅主题的队列,默认队列大小是4
Set<MessageQueue> mqs = consumer.fetchSubscribeMessageQueues("TopicTest1");
for (MessageQueue mq : mqs) {
System.out.println("Consume from the queue: " + mq);
SINGLE_MQ: while (true) {
try {
PullResult pullResult = consumer.pullBlockIfNotFound(mq, null, getMessageQueueOffset(mq), 32);
List<MessageExt> list = pullResult.getMsgFoundList();
if (list != null && list.size() < 100) {
for (MessageExt msg : list) {
System.out.println(new String(msg.getBody()));
}
}
System.out.println(pullResult.getNextBeginOffset());
putMessageQueueOffset(mq, pullResult.getNextBeginOffset());
switch (pullResult.getPullStatus()) {
case FOUND:
break;
case NO_MATCHED_MSG:
break;
case NO_NEW_MSG:
break SINGLE_MQ;
case OFFSET_ILLEGAL:
break;
default:
break;
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
consumer.shutdown();
}
private static void putMessageQueueOffset(MessageQueue mq, long offset) {
offseTable.put(mq, offset);
}
private static long getMessageQueueOffset(MessageQueue mq) {
Long offset = offseTable.get(mq);
if (offset != null) {
System.out.println(offset);
return offset;
}
return 0;
}
}
关键特性以及其实现原理
一、顺序消息
消息有序指的是一类消息消费时,能按照发送的顺序来消费。例如:一个订单产生了 3 条消息,分别是订单创建、订单付款、订单完成。消费时,要按照这个顺序消费才有意义。但同时订单之间又是可以并行消费的。
假如生产者产生了2条消息:M1、M2,要保证这两条消息的顺序,应该怎样做?你脑中想到的可能是这样:
M1发送到S1后,M2发送到S2,如果要保证M1先于M2被消费,那么需要M1到达消费端后,通知S2,然后S2再将M2发送到消费端。
这个模型存在的问题是,如果M1和M2分别发送到两台Server上,就不能保证M1先达到,也就不能保证M1被先消费,那么就需要在MQ Server集群维护消息的顺序。那么如何解决?一种简单的方式就是将M1、M2发送到同一个Server上:
这样可以保证M1先于M2到达MQServer(客户端等待M1成功后再发送M2),根据先达到先被消费的原则,M1会先于M2被消费,这样就保证了消息的顺序。
这个模型,理论上可以保证消息的顺序,但在实际运用中你应该会遇到下面的问题:
只要将消息从一台服务器发往另一台服务器,就会存在网络延迟问题。如上图所示,如果发送M1耗时大于发送M2的耗时,那么M2就先被消费,仍然不能保证消息的顺序。即使M1和M2同时到达消费端,由于不清楚消费端1和消费端2的负载情况,仍然有可能出现M2先于M1被消费。如何解决这个问题?将M1和M2发往同一个消费者即可,且发送M1后,需要消费端响应成功后才能发送M2。
但又会引入另外一个问题,如果发送M1后,消费端1没有响应,那是继续发送M2呢,还是重新发送M1?一般为了保证消息一定被消费,肯定会选择重发M1到另外一个消费端2,就如下图所示。
这样的模型就严格保证消息的顺序,细心的你仍然会发现问题,消费端1没有响应Server时有两种情况,一种是M1确实没有到达,另外一种情况是消费端1已经响应,但是Server端没有收到。如果是第二种情况,重发M1,就会造成M1被重复消费。也就是我们后面要说的第二个问题,消息重复问题。
回过头来看消息顺序问题,严格的顺序消息非常容易理解,而且处理问题也比较容易,要实现严格的顺序消息,简单且可行的办法就是:
保证生产者 - MQServer - 消费者是一对一对一的关系
但是这样设计,并行度就成为了消息系统的瓶颈(吞吐量不够),也会导致更多的异常处理,比如:只要消费端出现问题,就会导致整个处理流程阻塞,我们不得不花费更多的精力来解决阻塞的问题。
但我们的最终目标是要集群的高容错性和高吞吐量。这似乎是一对不可调和的矛盾,那么阿里是如何解决的?
世界上解决一个计算机问题最简单的方法:“恰好”不需要解决它!—— 沈询
有些问题,看起来很重要,但实际上我们可以通过合理的设计或者将问题分解来规避。如果硬要把时间花在解决它们身上,实际上是浪费的,效率低下的。从这个角度来看消息的顺序问题,我们可以得出两个结论:
1. 不关注乱序的应用实际大量存在
2. 队列无序并不意味着消息无序
最后我们从源码角度分析RocketMQ怎么实现发送顺序消息。
一般消息是通过轮询所有队列来发送的(负载均衡策略),顺序消息可以根据业务,比如说订单号相同的消息发送到同一个队列。下面的示例中,OrderId相同的消息,会发送到同一个队列:
// RocketMQ默认提供了两种MessageQueueSelector实现:随机/Hash
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);
在获取到路由信息以后,会根据MessageQueueSelector实现的算法来选择一个队列,同一个OrderId获取到的队列是同一个队列。
private SendResult send() {
// 获取topic路由信息
TopicPublishInfo topicPublishInfo = this.tryToFindTopicPublishInfo(msg.getTopic());
if (topicPublishInfo != null && topicPublishInfo.ok()) {
MessageQueue mq = null;
// 根据我们的算法,选择一个发送队列
// 这里的arg = orderId
mq = selector.select(topicPublishInfo.getMessageQueueList(), msg, arg);
if (mq != null) {
return this.sendKernelImpl(msg, mq, communicationMode, sendCallback, timeout);
}
}
}
二、消息重复
上面在解决消息顺序问题时,引入了一个新的问题,就是消息重复。那么RocketMQ是怎样解决消息重复的问题呢?还是“恰好”不解决。
造成消息的重复的根本原因是:网络不可达。只要通过网络交换数据,就无法避免这个问题。所以解决这个问题的办法就是不解决,转而绕过这个问题。那么问题就变成了:如果消费端收到两条一样的消息,应该怎样处理?
1. 消费端处理消息的业务逻辑保持幂等性
2. 保证每条消息都有唯一编号且保证消息处理成功与去重表的日志同时出现
第1条很好理解,只要保持幂等性,不管来多少条重复消息,最后处理的结果都一样。第2条原理就是利用一张日志表来记录已经处理成功的消息的ID,如果新到的消息ID已经在日志表中,那么就不再处理这条消息。
我们可以看到第1条的解决方式,很明显应该在消费端实现,不属于消息系统要实现的功能。第2条可以消息系统实现,也可以业务端实现。正常情况下出现重复消息的概率不一定大,且由消息系统实现的话,肯定会对消息系统的吞吐量和高可用有影响,所以最好还是由业务端自己处理消息重复的问题,这也是RocketMQ不解决消息重复的问题的原因。
RocketMQ不保证消息不重复,如果你的业务需要保证严格的不重复消息,需要你自己在业务端去重。
三、事务消息
RocketMQ除了支持普通消息,顺序消息,另外还支持事务消息。首先讨论一下什么是事务消息以及支持事务消息的必要性。我们以一个转帐的场景为例来说明这个问题:Bob向Smith转账100块。
在单机环境下,执行事务的情况,大概是下面这个样子:
当用户增长到一定程度,Bob和Smith的账户及余额信息已经不在同一台服务器上了,那么上面的流程就变成了这样:
这时候你会发现,同样是一个转账的业务,在集群环境下,耗时居然成倍的增长,这显然是不能够接受的。那我们如何来规避这个问题?
大事务 = 小事务 + 异步
将大事务拆分成多个小事务异步执行。这样基本上能够将跨机事务的执行效率优化到与单机一致。转账的事务就可以分解成如下两个小事务:
图中执行本地事务(Bob账户扣款)和发送异步消息应该保持同时成功或者失败中,也就是扣款成功了,发送消息一定要成功,如果扣款失败了,就不能再发送消息。那问题是:我们是先扣款还是先发送消息呢?
首先我们看下,先发送消息,大致的示意图如下:
存在的问题是:如果消息发送成功,但是扣款失败,消费端就会消费此消息,进而向Smith账户加钱。
先发消息不行,那我们就先扣款呗,大致的示意图如下:
存在的问题跟上面类似:如果扣款成功,发送消息失败,就会出现Bob扣钱了,但是Smith账户未加钱。
可能大家会有很多的方法来解决这个问题,比如:直接将发消息放到Bob扣款的事务中去,如果发送失败,抛出异常,事务回滚。这样的处理方式也符合“恰好”不需要解决的原则。RocketMQ支持事务消息,下面我们来看看RocketMQ是怎样来实现的。
RocketMQ第一阶段发送Prepared消息时,会拿到消息的地址,第二阶段执行本地事物,第三阶段通过第一阶段拿到的地址去访问消息,并修改状态。细心的你可能又发现问题了,如果确认消息发送失败了怎么办?RocketMQ会定期扫描消息集群中的事物消息,这时候发现了Prepared消息,它会向消息发送者确认,Bob的钱到底是减了还是没减呢?如果减了是回滚还是继续发送确认消息呢?RocketMQ会根据发送端设置的策略来决定是回滚还是继续发送确认消息。这样就保证了消息发送与本地事务同时成功或同时失败。
那我们来看下RocketMQ源码,是不是这样来处理事务消息的。客户端发送事务消息的部分(完整代码请查看:rocketmq-example工程下的com.alibaba.rocketmq.example.transaction.TransactionProducer):
// 未决事务,MQ服务器回查客户端
// 也就是上文所说的,当RocketMQ发现`Prepared消息`时,会根据这个Listener实现的策略来决断事务
TransactionCheckListener transactionCheckListener = new TransactionCheckListenerImpl();
// 构造事务消息的生产者
TransactionMQProducer producer = new TransactionMQProducer("groupName");
// 设置事务决断处理类
producer.setTransactionCheckListener(transactionCheckListener);
// 本地事务的处理逻辑,相当于示例中检查Bob账户并扣钱的逻辑
TransactionExecuterImpl tranExecuter = new TransactionExecuterImpl();
producer.start()
// 构造MSG,省略构造参数
Message msg = new Message(......);
// 发送消息
SendResult sendResult = producer.sendMessageInTransaction(msg, tranExecuter, null);
producer.shutdown();
接着查看sendMessageInTransaction方法的源码,总共分为3个阶段:发送Prepared消息、执行本地事务、发送确认消息。
public TransactionSendResult sendMessageInTransaction(.....) {
// 逻辑代码,非实际代码
// 1.发送消息
sendResult = this.send(msg);
// sendResult.getSendStatus() == SEND_OK
// 2.如果消息发送成功,处理与消息关联的本地事务单元
LocalTransactionState localTransactionState = tranExecuter.executeLocalTransactionBranch(msg, arg);
// 3.结束事务
this.endTransaction(sendResult, localTransactionState, localException);
}
endTransaction方法会将请求发往broker(mq server)去更新事物消息的最终状态:
1. 根据sendResult找到Prepared消息
2. 根据localTransaction更新消息的最终状态
如果endTransaction方法执行失败,导致数据没有发送到broker,broker会有回查线程定时(默认1分钟)扫描每个存储事务状态的表格文件,如果是已经提交或者回滚的消息直接跳过,如果是prepared状态则会向Producer发起CheckTransaction请求,Producer会调用DefaultMQProducerImpl.checkTransactionState()方法来处理broker的定时回调请求,而checkTransactionState会调用我们的事务设置的决断方法,最后调用endTransactionOneway让broker来更新消息的最终状态。
再回到转账的例子,如果Bob的账户的余额已经减少,且消息已经发送成功,Smith端开始消费这条消息,这个时候就会出现消费失败和消费超时两个问题?解决超时问题的思路就是一直重试,直到消费端消费消息成功,整个过程中有可能会出现消息重复的问题,按照前面的思路解决即可。
这样基本上可以解决超时问题,但是如果消费失败怎么办?阿里提供给我们的解决方法是:人工解决。大家可以考虑一下,按照事务的流程,因为某种原因Smith加款失败,需要回滚整个流程。如果消息系统要实现这个回滚流程的话,系统复杂度将大大提升,且很容易出现Bug,估计出现Bug的概率会比消费失败的概率大很多。我们需要衡量是否值得花这么大的代价来解决这样一个出现概率非常小的问题,这也是大家在解决疑难问题时需要多多思考的地方。
20160321补充:在3.2.6版本中移除了事务消息的实现,所以此版本不支持事务消息,具体情况请参考rocketmq的issues:
https://github.com/alibaba/RocketMQ/issues/65
https://github.com/alibaba/RocketMQ/issues/138
https://github.com/alibaba/RocketMQ/issues/156
四、Producer如何发送消息
Producer轮询某topic下的所有队列的方式来实现发送方的负载均衡,如下图所示:
首先分析一下RocketMQ的客户端发送消息的源码:
// 构造Producer
DefaultMQProducer producer = new DefaultMQProducer("ProducerGroupName");
// 初始化Producer,整个应用生命周期内,只需要初始化1次
producer.start();
// 构造Message
Message msg = new Message("TopicTest1",// topic
"TagA",// tag:给消息打标签,用于区分一类消息,可为null
"OrderID188",// key:自定义Key,可以用于去重,可为null
("Hello MetaQ").getBytes());// body:消息内容
// 发送消息并返回结果
SendResult sendResult = producer.send(msg);
// 清理资源,关闭网络连接,注销自己
producer.shutdown();
在整个应用生命周期内,生产者需要调用一次start方法来初始化,初始化主要完成的任务有:
1. 如果没有指定namesrv地址,将会自动寻址
2. 启动定时任务:更新namesrv地址、从namsrv更新topic路由信息、清理已经挂掉的broker、向所有broker发送心跳…
3. 启动负载均衡的服务
初始化完成后,开始发送消息,发送消息的主要代码如下:
private SendResult sendDefaultImpl(Message msg,......) {
// 检查Producer的状态是否是RUNNING
this.makeSureStateOK();
// 检查msg是否合法:是否为null、topic,body是否为空、body是否超长
Validators.checkMessage(msg, this.defaultMQProducer);
// 获取topic路由信息
TopicPublishInfo topicPublishInfo = this.tryToFindTopicPublishInfo(msg.getTopic());
// 从路由信息中选择一个消息队列
MessageQueue mq = topicPublishInfo.selectOneMessageQueue(lastBrokerName);
// 将消息发送到该队列上去
sendResult = this.sendKernelImpl(msg, mq, communicationMode, sendCallback, timeout);
}
代码中需要关注的两个方法tryToFindTopicPublishInfo和selectOneMessageQueue。前面说过在producer初始化时,会启动定时任务获取路由信息并更新到本地缓存,所以tryToFindTopicPublishInfo会首先从缓存中获取topic路由信息,如果没有获取到,则会自己去namesrv获取路由信息。selectOneMessageQueue方法通过轮询的方式,返回一个队列,以达到负载均衡的目的。
如果Producer发送消息失败,会自动重试,重试的策略:
- 重试次数 < retryTimesWhenSendFailed(可配置)
- 总的耗时(包含重试n次的耗时) < sendMsgTimeout(发送消息时传入的参数)
- 同时满足上面两个条件后,Producer会选择另外一个队列发送消息
五、消息存储
RocketMQ的消息存储是由consume queue和commit log配合完成的。
1、Consume Queue
consume queue是消息的逻辑队列,相当于字典的目录,用来指定消息在物理文件commit log上的位置。
我们可以在配置中指定consumequeue与commitlog存储的目录
每个topic下的每个queue都有一个对应的consumequeue文件,比如:
${rocketmq.home}/store/consumequeue/${topicName}/${queueId}/${fileName}
Consume Queue文件组织,如图所示:
- 根据topic和queueId来组织文件,图中TopicA有两个队列0,1,那么TopicA和QueueId=0组成一个ConsumeQueue,TopicA和QueueId=1组成另一个ConsumeQueue。
- 按照消费端的GroupName来分组重试队列,如果消费端消费失败,消息将被发往重试队列中,比如图中的%RETRY%ConsumerGroupA。
- 按照消费端的GroupName来分组死信队列,如果消费端消费失败,并重试指定次数后,仍然失败,则发往死信队列,比如图中的%DLQ%ConsumerGroupA。
死信队列(Dead Letter Queue)一般用于存放由于某种原因无法传递的消息,比如处理失败或者已经过期的消息。
Consume Queue中存储单元是一个20字节定长的二进制数据,顺序写顺序读,如下图所示:
CommitLog Offset是指这条消息在Commit Log文件中的实际偏移量
Size存储中消息的大小
Message Tag HashCode存储消息的Tag的哈希值:主要用于订阅时消息过滤(订阅时如果指定了Tag,会根据HashCode来快速查找到订阅的消息)
2、Commit Log
CommitLog:消息存放的物理文件,每台broker上的commitlog被本机所有的queue共享,不做任何区分。
文件的默认位置如下,仍然可通过配置文件修改:
${user.home} \store\${commitlog}\${fileName}
CommitLog的消息存储单元长度不固定,文件顺序写,随机读。消息的存储结构如下表所示,按照编号顺序以及编号对应的内容依次存储。
3、消息存储实现
消息存储实现,比较复杂,也值得大家深入了解,后面会单独成文来分析,这小节只以代码说明一下具体的流程。
// Set the storage time
msg.setStoreTimestamp(System.currentTimeMillis());
// Set the message body BODY CRC (consider the most appropriate setting
msg.setBodyCRC(UtilAll.crc32(msg.getBody()));
StoreStatsService storeStatsService = this.defaultMessageStore.getStoreStatsService();
synchronized (this) {
long beginLockTimestamp = this.defaultMessageStore.getSystemClock().now();
// Here settings are stored timestamp, in order to ensure an orderly global
msg.setStoreTimestamp(beginLockTimestamp);
// MapedFile:操作物理文件在内存中的映射以及将内存数据持久化到物理文件中
MapedFile mapedFile = this.mapedFileQueue.getLastMapedFile();
// 将Message追加到文件commitlog
result = mapedFile.appendMessage(msg, this.appendMessageCallback);
switch (result.getStatus()) {
case PUT_OK:break;
case END_OF_FILE:
// Create a new file, re-write the message
mapedFile = this.mapedFileQueue.getLastMapedFile();
result = mapedFile.appendMessage(msg, this.appendMessageCallback);
break;
DispatchRequest dispatchRequest = new DispatchRequest(
topic,// 1
queueId,// 2
result.getWroteOffset(),// 3
result.getWroteBytes(),// 4
tagsCode,// 5
msg.getStoreTimestamp(),// 6
result.getLogicsOffset(),// 7
msg.getKeys(),// 8
/**
* Transaction
*/
msg.getSysFlag(),// 9
msg.getPreparedTransactionOffset());// 10
// 1.分发消息位置到ConsumeQueue
// 2.分发到IndexService建立索引
this.defaultMessageStore.putDispatchRequest(dispatchRequest);
}
4、消息的索引文件
如果一个消息包含key值的话,会使用IndexFile存储消息索引,文件的内容结构如图:
索引文件主要用于根据key来查询消息的,流程主要是:
1. 根据查询的 key 的 hashcode%slotNum 得到具体的槽的位置(slotNum 是一个索引文件里面包含的最大槽的数目,例如图中所示 slotNum=5000000)
2. 根据 slotValue(slot 位置对应的值)查找到索引项列表的最后一项(倒序排列,slotValue 总是指向最新的一个索引项)
3. 遍历索引项列表返回查询时间范围内的结果集(默认一次最大返回的 32 条记录)
六、消息订阅
RocketMQ消息订阅有两种模式,一种是Push模式,即MQServer主动向消费端推送;另外一种是Pull模式,即消费端在需要时,主动到MQServer拉取。但在具体实现时,Push和Pull模式都是采用消费端主动拉取的方式。
首先看下消费端的负载均衡:
消费端会通过RebalanceService线程,10秒钟做一次基于topic下的所有队列负载:
1. 遍历Consumer下的所有topic,然后根据topic订阅所有的消息
2. 获取同一topic和Consumer Group下的所有Consumer
3. 然后根据具体的分配策略来分配消费队列,分配的策略包含:平均分配、消费端配置等
如同上图所示:如果有 5 个队列,2 个 consumer,那么第一个 Consumer 消费 3 个队列,第二 consumer 消费 2 个队列。这里采用的就是平均分配策略,它类似于我们的分页,TOPIC下面的所有queue就是记录,Consumer的个数就相当于总的页数,那么每页有多少条记录,就类似于某个Consumer会消费哪些队列。
通过这样的策略来达到大体上的平均消费,这样的设计也可以很方面的水平扩展Consumer来提高消费能力。
消费端的Push模式是通过长轮询的模式来实现的,就如同下图:
Consumer端每隔一段时间主动向broker发送拉消息请求,broker在收到Pull请求后,如果有消息就立即返回数据,Consumer端收到返回的消息后,再回调消费者设置的Listener方法。如果broker在收到Pull请求时,消息队列里没有数据,broker端会阻塞请求直到有数据传递或超时才返回。
当然,Consumer端是通过一个线程将阻塞队列LinkedBlockingQueue中的PullRequest发送到broker拉取消息,以防止Consumer一致被阻塞。而Broker端,在接收到Consumer的PullRequest时,如果发现没有消息,就会把PullRequest扔到ConcurrentHashMap中缓存起来。broker在启动时,会启动一个线程不停的从ConcurrentHashMap取出PullRequest检查,直到有数据返回。
七、RocketMQ的其他特性
前面的6个特性都是基本上都是点到为止,想要深入了解,还需要大家多多查看源码,多多在实际中运用。当然除了已经提到的特性外,RocketMQ还支持:
1. 定时消息
2. 消息的刷盘策略
3. 主动同步策略:同步双写、异步复制
4. 海量消息堆积能力
5. 高效通信 .
6. ……
其中涉及到的很多设计思路和解决方法都值得我们深入研究:
1. 消息的存储设计:既要满足海量消息的堆积能力,又要满足极快的查询效率,还要保证写入的效率。
2. 高效的通信组件设计:高吞吐量,毫秒级的消息投递能力都离不开高效的通信。
3. …….
RocketMQ最佳实践
一、Producer最佳实践
1、一个应用尽可能用一个 Topic,消息子类型用 tags 来标识,tags 可以由应用自由设置。只有发送消息设置了tags,消费方在订阅消息时,才可以利用 tags 在 broker 做消息过滤。
2、每个消息在业务层面的唯一标识码,要设置到 keys 字段,方便将来定位消息丢失问题。由于是哈希索引,请务必保证 key 尽可能唯一,这样可以避免潜在的哈希冲突。
3、消息发送成功或者失败,要打印消息日志,务必要打印 sendresult 和 key 字段。
4、对于消息不可丢失应用,务必要有消息重发机制。例如:消息发送失败,存储到数据库,能有定时程序尝试重发或者人工触发重发。
5、某些应用如果不关注消息是否发送成功,请直接使用sendOneWay方法发送消息。
二、Consumer最佳实践
-
消费过程要做到幂等(即消费端去重)
-
尽量使用批量方式消费方式,可以很大程度上提高消费吞吐量
-
优化每条消息消费过程
三、其他配置
线上应该关闭autoCreateTopicEnable,即在配置文件中将其设置为false。
RocketMQ在发送消息时,会首先获取路由信息。如果是新的消息,由于MQServer上面还没有创建对应的Topic,这个时候,如果上面的配置打开的话,会返回默认TOPIC的(RocketMQ会在每台broker上面创建名为TBW102的TOPIC)路由信息,然后Producer会选择一台Broker发送消息,选中的broker在存储消息时,发现消息的topic还没有创建,就会自动创建topic。后果就是:以后所有该TOPIC的消息,都将发送到这台broker上,达不到负载均衡的目的。
所以基于目前RocketMQ的设计,建议关闭自动创建TOPIC的功能,然后根据消息量的大小,手动创建TOPIC。
RocketMQ设计相关
RocketMQ的设计假定:
每台PC机器都可能宕机不可服务
任意集群都有可能处理能力不足
最坏的情况一定会发生
内网环境需要低延迟来提供最佳用户体验
RocketMQ的关键设计:
分布式集群化
强数据安全
海量数据堆积
毫秒级投递延迟(推拉模式)
这是RocketMQ在设计时的假定前提以及需要到达的效果。我想这些假定适用于所有的系统设计。随着我们系统的服务的增多,每位开发者都要注意自己的程序是否存在单点故障,如果挂了应该怎么恢复、能不能很好的水平扩展、对外的接口是否足够高效、自己管理的数据是否足够安全…… 多多规范自己的设计,才能开发出高效健壮的程序。
附录:RocketMQ涉及到的几个专业术语和整体架构介绍
一、RocketMQ中的专业术语
- Topic
topic表示消息的第一级类型,比如一个电商系统的消息可以分为:交易消息、物流消息…… 一条消息必须有一个Topic。
- Tag
Tag表示消息的第二级类型,比如交易消息又可以分为:交易创建消息,交易完成消息….. 一条消息可以没有Tag。RocketMQ提供2级消息分类,方便大家灵活控制。
- Queue
一个topic下,我们可以设置多个queue(消息队列)。当我们发送消息时,需要要指定该消息的topic。RocketMQ会轮询该topic下的所有队列,将消息发送出去。
- Producer 与 Producer Group
Producer表示消息队列的生产者。消息队列的本质就是实现了publish-subscribe模式,生产者生产消息,消费者消费消息。所以这里的Producer就是用来生产和发送消息的,一般指业务系统。
Producer Group是一类Producer的集合名称,这类Producer通常发送一类消息,且发送逻辑一致。
- Consumer 与 Consumer Group
消息消费者,一般由后台系统异步消费消息。
- Push Consumer
Consumer 的一种,应用通常向 Consumer 对象注册一个 Listener 接口,一旦收到消息,Consumer 对象立刻回调 Listener 接口方法。
- Pull Consumer
Consumer 的一种,应用通常主动调用 Consumer 的拉消息方法从 Broker 拉消息,主动权由应用控制。
Consumer Group是一类Consumer的集合名称,这类Consumer通常消费一类消息,且消费逻辑一致。
- Broker
消息的中转者,负责存储和转发消息。可以理解为消息队列服务器,提供了消息的接收、存储、拉取和转发服务。broker是RocketMQ的核心,它不不能挂的,所以需要保证broker的高可用。
- 广播消费
一条消息被多个Consumer消费,即使这些Consumer属于同一个Consumer Group,消息也会被Consumer Group中的每个Consumer都消费一次。在广播消费中的Consumer Group概念可以认为在消息划分方面无意义。
- 集群消费
一个Consumer Group中的Consumer实例平均分摊消费消息。例如某个Topic有 9 条消息,其中一个Consumer Group有 3 个实例(可能是 3 个进程,或者 3 台机器),那么每个实例只消费其中的 3 条消息。
- NameServer
NameServer即名称服务,两个功能:
接收broker的请求,注册broker的路由信息
接口client的请求,根据某个topic获取其到broker的路由信息
NameServer没有状态,可以横向扩展。每个broker在启动的时候会到NameServer注册;Producer在发送消息前会根据topic到NameServer获取路由(到broker)信息;Consumer也会定时获取topic路由信息。
二、RocketMQ Overview
Producer向一些队列轮流发送消息,队列集合称为Topic,Consumer如果做广播消费,则一个consumer实例消费这个Topic对应的所有队列;如果做集群消费,则多个Consumer实例平均消费这个Topic对应的队列集合。
再看下RocketMQ物理部署结构图:
RocketMQ网络部署特点:
Name Server 是一个几乎无状态节点,可集群部署,节点之间无任何信息同步。
Broker部署相对复杂,Broker分为Master与Slave,一个Master可以对应多个Slave,但是一个Slave只能对应一个Master,Master与Slave的对应关系通过指定相同的BrokerName,不同的BrokerId来定义,BrokerId=0表示Master,非0表示Slave。Master也可以部署多个。每个Broker与Name Server集群中的所有节点建立长连接,定时注册Topic信息到所有Name Server。
Producer与Name Server集群中的其中一个节点(随机选择)建立长连接,定期从Name Server取Topic路由信息,并向提供Topic 服务的Master建立长连接,且定时向Master发送心跳。Producer 完全无状态,可集群部署。
Consumer与Name Server集群中的其中一个节点(随机选择)建立长连接,定期从Name Server取Topic 路由信息,并向提供Topic服务的Master、Slave建立长连接,且定时向Master、Slave发送心跳。Consumer既可以从Master订阅消息,也可以从Slave订阅消息,订阅规则由Broker配置决定。