应用场景
Git地址:https://github.com/apache/rocketmq/tree/release-4.3.0
1、 在解压后的文件夹中执行maven命令,获取程序运行包(生成的包在rocketmq-distribution/target路径下):
mvn -Prelease-all -DskipTests clean install -U
2、rocketmq是一个集群模型的消息队列,这里我们用两台服务器来部署rocketmq,为了方便和区分,分别把两台服务器标注一下角色,如下节点配置:
IP | 角色 | 模式 |
---|---|---|
106.53.92.xxx | nameServer1,brokerServer1 | Master1 |
47.105.189.xx | nameServer2,brokerServer2 | Master2 |
3、创建消息队列信息保存路径
/usr/local/rocketmq/store/index
/usr/local/rocketmq/store/commitlog
/usr/local/rocketmq/store/consumequeue
4、修改broker配置
brokerClusterName=rocketmq-cluster
# broker 名字,不同文件命名不一样
brokerName=broker-a
# 0表示Master >0 表示slave
brokerId=0
brokerIP1=本机IP
# nameServer地址,多个使用分号分割
namesrvAddr=rocketmq-nameserver1:9876
# 默认创建的队列数
defaultTopicQueueNums=4
# 是否允许broker 自动创建topic
autoCreateTopicEnable=true
# 是否允许broker自动创建订阅组
autoCreateSubscriptionGroup=true
# Broker 对外服务的监听端口
listenPort=10911
# 删除文件时间点,默认凌晨4点
deleteWhen=04
# 文件保留时间,默认48小时
fileReservedTime=120
# commitLog每个文件的大小默认1G
mapedFileSizeCommitLog=1073741824
# consumQueue每个文件默认存30W条
mapedFileSizeConsumeQueue=300000
# 检测物理文件磁盘空间
diskMaxUsedSpaceRatio=88
#存储路径
storePathRootDir=/usr/local/rocketmq/store
# commitLog存储路径
storePathCommitLog=/usr/local/rocketmq/store/commitlog
# 消费队列存储路径
storePathConsumeQueue=/usr/local/rocketmq/store/consumequeue
# 消息索引存储路径
storePathIndex=/usr/local/rocketmq/store/index
# checkpoint 文件存储路径
storeCheckpoint=/usr/local/rocketmq/store/checkpoint
# abort 文件存储路径
abortFile=/usr/local/rocketmq/store/abort
# ASYNC_MASTER 异步赋值 SYNC_MASTER 同步刷盘 SLAVE
brokerRole=ASYNC_MASTER
# 刷盘方式 ASYNC_FLUSH 异步刷盘 SYNC_FLUSH同步刷盘
flushDiskType=ASYNC_FLUSH
如果配置主从模式,则需要修改broke-a-s.properties
# 0表示Master >0 表示slave
brokerId=1
brokerIP1=本机IP
# ASYNC_MASTER 异步赋值 SYNC_MASTER 同步刷盘 SLAVE
brokerRole=SLAVE
5、 修改bin 下的runbroker.sh和 runserver.sh
JAVA_OPT="${JAVA_OPT} -server -Xms1g -Xmx1g -Xmn1g"
6、运行
// 启动nameserver
nohup sh ./mqnamesrv &
// 启动broker
nohup sh ./mqbroker -c ../conf/2m-2s-async/broker-a.properties > /dev/null 2&>1 &
// 关闭broker
sh mqshutdown broker
// 关闭nameServer
sh mqshutdown namesrv
可以用netstat -ntlp 查看一下端口占用情况
6、RocketMQ-Console(Git地址)
进入console模块,修改application.properties
rocketmq.config.namesrvAddr=106.53.92.208:9876
// 在pom目录下打包
$ mvn clean package -Dmaven.test.skip=true
// 运行
$ java -jar target/rocketmq-console-ng-1.0.0.jar
// 如果配置文件没有填写Name Server
$ java -jar target/rocketmq-console-ng-1.0.0.jar --rocketmq.config.namesrvAddr='10.0.74.198:9876;10.0.74.199:9876'
消息生产者
public class Producer {
private static final Logger logger = LoggerFactory.getLogger("Customer");
public static void main(String[] args) throws Exception {
DefaultMQProducer producer = new DefaultMQProducer("rocketmq-cluster");
// 设置nameserver 地址
producer.setNamesrvAddr(MQConstant.NAMESERVER1);
// 启动实例
producer.start();
for (int i = 0; i < 10; i++) {
// 加入TagA TagB 两个标签用于测试
String tag = "TagA";
if(i%2==0){ tag = "TagB"; }
// 创建消息
Message msg = new Message("test", tag ,"key"+i,
("Hello RocketMQ " + i).getBytes(RemotingHelper.DEFAULT_CHARSET)
);
// 发送消息
SendResult sendResult = producer.send(msg);
logger.info(JSONObject.toJSONString(sendResult));
}
// 一旦生产者实例不再使用,则关闭该实例
producer.shutdown();
}
}
消息消费者
public class Customer {
private static final Logger logger = LoggerFactory.getLogger("Customer");
public static void main(String[] args) throws Exception {
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("rocketmq-cluster");
// nameserver 地址
consumer.setNamesrvAddr(MQConstant.NAMESERVER1);
// 设置消息offset 位置
consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_LAST_OFFSET);
// 订阅消息主题
consumer.subscribe("test","*");
consumer.registerMessageListener(new MessageListenerConcurrently() {
@Override
public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> list, ConsumeConcurrentlyContext consumeConcurrentlyContext) {
MessageExt me = list.get(0);
//for (MessageExt me : list){
try {
String topic = me.getTopic();
String tags = me.getTags();
String keys = me.getKeys();
String msgBody = "";
msgBody = new String(me.getBody(),"utf-8");
logger.info("topic:{}, tags:{}, keys:{} {}",topic,tags,keys,msgBody);
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
//}
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
}
});
consumer.start();
System.out.println("消费者已启动");
}
}
消费者中, 如果消息第一次消费失败怎么办?
customer 中可以添加消息重试机制,当消息第一次失败可以可以进行重试,代码如下
public class Customer {
private static final Logger logger = LoggerFactory.getLogger("Customer");
public static void main(String[] args) throws Exception {
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("rocketmq-cluster");
// nameserver 地址
consumer.setNamesrvAddr(MQConstant.NameServerAndSlave);
// 设置消息offset 位置
consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_LAST_OFFSET);
// 订阅消息主题
consumer.subscribe("test","*");
consumer.registerMessageListener(new MessageListenerConcurrently() {
@Override
public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> list, ConsumeConcurrentlyContext consumeConcurrentlyContext) {
MessageExt me = list.get(0);
//for (MessageExt me : list){
try {
String topic = me.getTopic();
String tags = me.getTags();
String keys = me.getKeys();
String msgBody = "";
msgBody = new String(me.getBody(),"utf-8");
int i = 2/0;
logger.info("topic:{}, tags:{}, keys:{} {}",topic,tags,keys,msgBody);
} catch (Exception e) {
e.printStackTrace();
int reconsumeTimes = me.getReconsumeTimes();
logger.info("重试次数:{}",reconsumeTimes);
if(reconsumeTimes==3){
logger.error("日志补偿。。。");
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
}
return ConsumeConcurrentlyStatus.RECONSUME_LATER;
}
//}
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
}
});
consumer.start();
System.out.println("消费者已启动");
}
}
2.1、延迟消息
2.2、发送消息到指定队列
// 发送到指定的队列中去
SendResult sendResult = producer.send(msg, new MessageQueueSelector() {
@Override
public MessageQueue select(List<MessageQueue> list, Message message, Object o) {
Integer queueNum = (Integer) o;
return list.get(queueNum);
}
}, 1);
3.1 PushConsumer 消费模式 - 集群模式
适用场景
适用于消费端集群化部署,每条消息只需要被处理一次的场景。此外,由于消费进度在服务端维护,可靠性更高。具体消费示例如下图所示。
注意事项
集群消费模式下,每一条消息都只会被分发到一台机器上处理。如果需要被集群下的每一台机器都处理,请使用广播模式。
集群消费模式下,不保证每一次失败重投的消息路由到同一台机器上。
当我们启动两个消费之 A, B,然后再生产10条消息, 这时我们可以看到A消费了6条,B消费了4条,为啥会这样,不能实现负载均衡吗?
因为消费者通过监听消息对列来实现消息的接受, 加入四个消息对列, 生产者分别向两个消息队列投了三条消息,另外两个投了两条,则会出现这种情况。
21:47:04.981 [main] INFO Customer - [main,31] - {"messageQueue":{"brokerName":"broker-a","queueId":2,"topic":"test"},"msgId":"C0A8006C25B018B4AAC24CC5B3490000","offsetMsgId":"6A355CD000002A9F000000000002045E","queueOffset":40,"regionId":"DefaultRegion","sendStatus":"SEND_OK","traceOn":true}
21:47:05.028 [main] INFO Customer - [main,31] - {"messageQueue":{"brokerName":"broker-a","queueId":3,"topic":"test"},"msgId":"C0A8006C25B018B4AAC24CC5B3950001","offsetMsgId":"6A355CD000002A9F0000000000020515","queueOffset":60,"regionId":"DefaultRegion","sendStatus":"SEND_OK","traceOn":true}
21:47:05.065 [main] INFO Customer - [main,31] - {"messageQueue":{"brokerName":"broker-a","queueId":0,"topic":"test"},"msgId":"C0A8006C25B018B4AAC24CC5B3C40002","offsetMsgId":"6A355CD000002A9F00000000000205CC","queueOffset":39,"regionId":"DefaultRegion","sendStatus":"SEND_OK","traceOn":true}
21:47:05.112 [main] INFO Customer - [main,31] - {"messageQueue":{"brokerName":"broker-a","queueId":1,"topic":"test"},"msgId":"C0A8006C25B018B4AAC24CC5B3E90003","offsetMsgId":"6A355CD000002A9F0000000000020683","queueOffset":64,"regionId":"DefaultRegion","sendStatus":"SEND_OK","traceOn":true}
21:47:05.149 [main] INFO Customer - [main,31] - {"messageQueue":{"brokerName":"broker-a","queueId":2,"topic":"test"},"msgId":"C0A8006C25B018B4AAC24CC5B4180004","offsetMsgId":"6A355CD000002A9F000000000002073A","queueOffset":41,"regionId":"DefaultRegion","sendStatus":"SEND_OK","traceOn":true}
21:47:05.185 [main] INFO Customer - [main,31] - {"messageQueue":{"brokerName":"broker-a","queueId":3,"topic":"test"},"msgId":"C0A8006C25B018B4AAC24CC5B43E0005","offsetMsgId":"6A355CD000002A9F00000000000207F1","queueOffset":61,"regionId":"DefaultRegion","sendStatus":"SEND_OK","traceOn":true}
21:47:05.219 [main] INFO Customer - [main,31] - {"messageQueue":{"brokerName":"broker-a","queueId":0,"topic":"test"},"msgId":"C0A8006C25B018B4AAC24CC5B4610006","offsetMsgId":"6A355CD000002A9F00000000000208A8","queueOffset":40,"regionId":"DefaultRegion","sendStatus":"SEND_OK","traceOn":true}
21:47:05.259 [main] INFO Customer - [main,31] - {"messageQueue":{"brokerName":"broker-a","queueId":1,"topic":"test"},"msgId":"C0A8006C25B018B4AAC24CC5B4830007","offsetMsgId":"6A355CD000002A9F000000000002095F","queueOffset":65,"regionId":"DefaultRegion","sendStatus":"SEND_OK","traceOn":true}
21:47:05.298 [main] INFO Customer - [main,31] - {"messageQueue":{"brokerName":"broker-a","queueId":2,"topic":"test"},"msgId":"C0A8006C25B018B4AAC24CC5B4AB0008","offsetMsgId":"6A355CD000002A9F0000000000020A16","queueOffset":42,"regionId":"DefaultRegion","sendStatus":"SEND_OK","traceOn":true}
21:47:05.338 [main] INFO Customer - [main,31] - {"messageQueue":{"brokerName":"broker-a","queueId":3,"topic":"test"},"msgId":"C0A8006C25B018B4AAC24CC5B4D20009","offsetMsgId":"6A355CD000002A9F0000000000020ACD","queueOffset":62,"regionId":"DefaultRegion","sendStatus":"SEND_OK","traceOn":true}
如果存在这样一个场景, 一个producer 的同一个topic下分别有TagA、TagB、TagC , 这三个tag 分别需要被三个不同的consumer 消费,这样该如何实现?
我们所知的pushConsumer的集群模式不能很好地去实现这种需求,会有其他办法吗?
3.2 PushConsumer 消费模式 - 广播模式
BroadCasting 模式 (广播模式)
同一个ConsumerGroup 里的Consumer 都消费订阅topic 全部信息,也就是一条消息会被每一个Consumer消费
consumer.setMessageModel(MessageModel.BROADCASTING);
方法设置广播模式
广播消费:当使用广播消费模式时,消息队列 MQ 会将每条消息推送给集群内所有注册过的消费者,保证消息至少被每个消费者消费一次。
注意事项
通过上述可以看到广播模式并不推荐使用,我们可以通过集群模式来模拟广播模式
使用集群模式模拟广播
适用场景:
适用于每条消息都需要被多台机器处理,每台机器的逻辑可以相同也可以不一样的场景。具体消费示例如下图所示。
如果业务需要使用广播模式,也可以创建多个 Group ID,用于订阅同一个 Topic。
注意事项
消费进度在服务端维护,可靠性高于广播模式。
对于一个 Group ID 来说,可以部署一个消费者实例,也可以部署多个消费者实例。当部署多个消费者实例时,实例之间又组成了集群模式(共同分担消费消息)。假设 Group ID 1 部署了三个消费者实例 C1、C2、C3,那么这三个实例将共同分担服务器发送给 Group ID 1 的消息。同时,实例之间订阅关系必须保持一致。
3.3 PullConsumer 消息拉取消费模式
pull方式里,取消息的过程需要用户自己写,首先通过打算消费的Topic拿到MessageQueue的集合,遍历MessageQueue集合,然后针对每个MessageQueue批量取消息,一次取完后,记录该队列下一次要取的开始offset,直到取完了,再换另一个MessageQueue。
/**
* 消息拉取模式
**/
public class PullConsumer {
private static final Logger log = LoggerFactory.getLogger("PullConsumer");
//保存上一次消费的消息位置
private static final Map offsetTable = new HashMap();
public static void main(String[] args) throws MQClientException {
//实例化pullConsumer
DefaultMQPullConsumer consumer = new DefaultMQPullConsumer("rocketmq-cluster");
consumer.setNamesrvAddr(MQConstant.Master_Slave);
consumer.start();
//获取topic下所有的队列
Set<MessageQueue> mqs = consumer.fetchSubscribeMessageQueues("test");
//遍历消息队列
for (MessageQueue mq : mqs) {
log.info("消息队列信息: " + mq);
SINGLE_MQ:
while (true) {
try {
//设置上次消费消息下标
PullResult pullResult = consumer.pullBlockIfNotFound(mq, null, getMessageQueueOffset(mq), 32);
// 保存消息下次读取的 offset
putMessageQueueOffset(mq, pullResult.getNextBeginOffset());
switch (pullResult.getPullStatus()) {
//根据结果状态,如果找到消息,批量消费消息
case FOUND:
List<MessageExt> messageExtList = pullResult.getMsgFoundList();
for (MessageExt m : messageExtList) {
log.info("topic:{}; getQueueId:{}; offset:{}; 消息内容:{}",m.getTopic(),m.getQueueId(),m.getQueueOffset(),new String(m.getBody()));
}
break;
case NO_MATCHED_MSG:
log.warn("没有匹配的信息");
break;
case NO_NEW_MSG:
log.warn("没有新的的信息");
break SINGLE_MQ;
case OFFSET_ILLEGAL:
log.warn("OFFSET_ILLEGAL");
break;
default:
break;
}
} catch (Exception e) {
log.error("消息消费出现异常:");
e.printStackTrace();
}
}
}
consumer.shutdown();
}
//保存上次消费的消息下标,这里使用了一个全局HashMap来保存
private static void putMessageQueueOffset(MessageQueue mq, long offset) {
offsetTable.put(mq, offset);
}
//获取上次消费的消息的下表 这里可以保存在硬盘或redis中 ConsumerName-topic-queueId做key
private static long getMessageQueueOffset(MessageQueue mq) {
Long offset = (Long) offsetTable.get(mq);
if (offset != null) {
return offset;
}
return 0;
}
}
4.1 同步刷盘和异步刷盘
RocketMQ的消息是存储到磁盘上的,这样既能保证断电后恢复,又可以让存储的消息量超出内存的限制。
消息在通过Producer写入RocketMQ的时候,有两种写磁盘方式:
异步刷盘方式: 在返回写成功状态时,消息可能只是被写入了内存的PAGECACHE,写操作的返回快,
吞吐量大;当内存里的消息量积累到一定程度时,统一触发写磁盘操作,快速写入
同步刷盘方式: 在返回写成功状态时,消息已经被写入磁盘。具体流程是,消息写入内存的PAGECACHE后,立刻通知刷盘线程刷盘,然后等待刷盘完成,刷盘线程执行完成后唤醒等待的线程,返回消息写成功的状态。
4.12同步双写和异步复制
异步复制和同步双写主要是主和从的关系。消息需要实时消费的,就需要采用主从模式部署
异步复制: 比如这里有一主一从,我们发送一条消息到主节点之后,这样消息就算从producer端发送成功了,然后通过异步复制的方法将数据复制到从节点
同步双写: 比如这里有一主一从,我们发送一条消息到主节点之后,这样消息就并不算从producer端发送成功了,需要通过同步双写的方法将数据同步到从节点后, 才算数据发送成功。