目录
1. 简介
Apache RocketMQ是一个采用Java语言开发的分布式的消息系统,由阿里巴巴团队开发,与2016年底贡献给
Apache,成为了Apache的一个顶级项目。
在阿里内部,RocketMQ 很好地服务了 集 团大大小小上千个应用,在每年的双十一当天,更有不可思议的万亿级
消息通过 RocketMQ 流转(在 2017 年的双十一当天,整个阿里巴巴集团通过 RocketMQ 流转的线上消息达到了 万
亿级,峰值 TPS 达到 5600 万),在阿里大中台策略上发挥着举足轻重的作用 。
地址:http://rocketmq.apache.org/
2. 核心概念说明
Producer 消息生产者,负责产生消息、一般由业务系统负责产生消息
ProducerGroup:一类Producer集合的名称、这类Producer通常发送一类消息、且发送逻辑一致。
Consumer 消息消费者,负责消费消息,一般由后台系统负责异步消费
PushConsumer:服务端向消费端推送消息
PullConsumer:消费端向服务端定时拉取消息
ConsumerGroup:一类Consumer集合的名称、这类Consumer通常消费一类消息、且消费逻辑一致
NameServer
1. 集群架构中的组织协调员
2. 收集broker的工作情况
3. 不负责消息的处理
Broker
1. 是RockerMQ的核心、负责消息的发送、接收、高可用等(真正干活的)
2. 需要定时发送自身情况给NameServer、默认10秒发送一次、超过2分钟则认为该broker失效
Topic
1. 不同类型的消息以不同的Topic名称进行区分、比如User、Order
2. 是逻辑概念
MessageQueue: 消息队列、用于存储消息
3. 部署及安装
下载地址:https://www.apache.org/dyn/closer.cgi?path=rocketmq/4.3.2/rocketmq-all-4.3.2-bin-release.zip
3.1 配置环境变量 ROCKETMQ_HOME
3.2 启动NameServer
执行bin里面的 mqnamesrv.cmd
看到图中的文字表示启动成功
3.3 启动Broker
-n 指定nameserver地址和端口
执行 mqbroker.cmd -n 127.0.0.1:9876
看到图中的文字表示启动成功
4. Java 操作 RocketMQ
4.1 同步producer
package com.sun.producer;
import org.apache.rocketmq.client.producer.DefaultMQProducer;
import org.apache.rocketmq.client.producer.SendResult;
import org.apache.rocketmq.common.message.Message;
/**
* @author sun
* @from 宇宙最强 CTO 因太过牛逼 请跪着膜拜好吗
*/
public class SyncProducer {
public static void main(String[] args) throws Exception {
DefaultMQProducer producer = new DefaultMQProducer("test-group");
producer.setNamesrvAddr("127.0.0.1:9876");
producer.start();
Message msg = new Message("TopicTest11", "tag", "你干啥啊".getBytes("UTF-8"));
SendResult sendResult = producer.send(msg);
System.out.println("sendResult: " + sendResult);
// create topic
//producer.createTopic("broker_name", "topic_name", 8);
producer.shutdown();
}
}
4.2 接收消息
package com.sun.producer;
import org.apache.rocketmq.client.consumer.DefaultMQPushConsumer;
import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyStatus;
import org.apache.rocketmq.client.consumer.listener.MessageListenerConcurrently;
import java.util.List;
import java.util.stream.Collectors;
/**
* @author sun
* @from 宇宙最强 CTO 因太过牛逼 请跪着膜拜好吗
*/
public class ConsumerDemo {
public static void main(String[] args) throws Exception {
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("consumer-group-1");
consumer.setNamesrvAddr("127.0.0.1:9876");
// 订阅topic 并监听所有消息
consumer.subscribe("TopicTest11", "*");
// 其他订阅方式 “SEND_MSG” (完全匹配) 、 “SEND_MSG1||SEND_MSG2”(或匹配)
// 监听消息
consumer.registerMessageListener((MessageListenerConcurrently) (msgs, context) -> {
// 转换成 List<String> String = new String(MessageExt.getBody());
List<String> result = msgs.stream()
.map((messageExt) -> new String(messageExt.getBody()))
.collect(Collectors.toList());
System.out.println("接收到消息: " + result);
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
});
consumer.start();
}
}
4.3 异步 发送消息
package com.sun.producer;
import org.apache.rocketmq.client.producer.DefaultMQProducer;
import org.apache.rocketmq.client.producer.SendCallback;
import org.apache.rocketmq.client.producer.SendResult;
import org.apache.rocketmq.common.message.Message;
/**
* @author sun
* @from 宇宙最强 CTO 因太过牛逼 请跪着膜拜好吗
*
* 异步发送数据
*/
public class AsyncProducer {
public static void main(String[] args) throws Exception {
DefaultMQProducer producer = new DefaultMQProducer("producer1");
producer.setNamesrvAddr("127.0.0.1:9876");
// 发送失败的重试次数
producer.setRetryTimesWhenSendAsyncFailed(0);
producer.start();
String msgStr = "我要做CTO、架构师已经无法满足我了";
Message msg = new Message("TopicTest11", "send_msg", msgStr.getBytes("UTF-8"));
// 异步发送数据
producer.send(msg, new SendCallback() {
public void onSuccess(SendResult sendResult) {
System.out.println("消息状态:" + sendResult.getSendStatus());
System.out.println("消息id:" + sendResult.getMsgId());
System.out.println("消息queue:" + sendResult.getMessageQueue());
System.out.println("消息offset:" + sendResult.getQueueOffset());
}
public void onException(Throwable e) {
System.out.println("发送失败: " + e);
}
});
// 要注释掉 因为是异步发送 所以可能出现还未发送就已关闭
// producer.shutdown();
}
}
4.4 消息过滤
发送消息时
Message msg = new Message("TopicTest11", "SEND_MSG",
msgStr.getBytes(RemotingHelper.DEFAULT_CHARSET));
msg.putUserProperty("age", "18");
msg.putUserProperty("sex", "女");
接收消息时
consumer.subscribe("TopicTest11", MessageSelector.bySql("age>=20 AND sex='女'"));
记得开启过滤功能 在broker的配置文件
enablePropertyFilter=true
5. 消息的顺序 TODO
如何保证消息的顺序呢??? 2019 / 06 / 24 16.30
6. 消息的事务
6.1 名词介绍
半消息(HalfMessage):指的是发送方已经将消息发送给MQ服务器,但是服务器端未收到生产者对该消息的二次确认,此时消息就会被标记成 "暂不能投递状态" ,处于该状态的消息即 半消息
消息回查(MessageStatusCheck):由于网络闪断,生产者应用重启等原因,导致某条事务消息的二次确认丢失。MQ服务器通过扫描发现某条消息长时间处于 "半消息" 时,需要主动向消息生产者询问该消息的最终状态(Commit还是Rollback),该过程就是消息回查。
6.2 执行过程
1. 发送方向 MQ 服务端发送消息。
2. MQ Server 将消息持久化成功之后,向发送方 ACK 确认消息已经发送成功,此时消息为半消息。
3. 发送方开始执行本地事务逻辑。
4. 发送方根据本地事务执行结果向 MQ Server 提交二次确认(Commit 或是 Rollback),MQ Server 收到
Commit 状态则将半消息标记为可投递,订阅方最终将收到该消息;MQ Server 收到 Rollback 状态则删除半
消息,订阅方将不会接受该消息。
5. 在断网或者是应用重启的特殊情况下,上述步骤4提交的二次确认最终未到达 MQ Server,经过固定时间后
MQ Server 将对该消息发起消息回查。
6. 发送方收到消息回查后,需要检查对应消息的本地事务执行的最终结果。
7. 发送方根据检查得到的本地事务的最终状态再次提交二次确认,MQ Server 仍按照步骤4对半消息进行操作。
6.3 代码
生产者
import org.apache.rocketmq.client.producer.TransactionMQProducer;
import org.apache.rocketmq.common.message.Message;
public class TransactionProducer {
public static void main(String[] args) throws Exception {
TransactionMQProducer producer = new
TransactionMQProducer("transaction_producer");
producer.setNamesrvAddr("172.16.55.185:9876");
// 设置事务监听器
producer.setTransactionListener(new TransactionListenerImpl());
producer.start();
// 发送消息
Message message = new Message("pay_topic", "用户A给用户B转账500
元".getBytes("UTF-8"));
producer.sendMessageInTransaction(message, null);
Thread.sleep(999999);
producer.shutdown();
}
}
注意:发送消息使用的是TransactionMQProducer
本地事务处理
import org.apache.rocketmq.client.producer.LocalTransactionState;
import org.apache.rocketmq.client.producer.TransactionListener;
import org.apache.rocketmq.common.message.Message;
import org.apache.rocketmq.common.message.MessageExt;
import java.util.HashMap;
import java.util.Map;
public class TransactionListenerImpl implements TransactionListener {
private static Map<String, LocalTransactionState> STATE_MAP = new HashMap<>();
/**
* 执行具体的业务逻辑
*
* @param msg 发送的消息对象
* @param arg
* @return
*/
@Override
public LocalTransactionState executeLocalTransaction(Message msg, Object arg) {
try {
System.out.println("用户A账户减500元.");
Thread.sleep(500); //模拟调用服务
// System.out.println(1/0);
System.out.println("用户B账户加500元.");
Thread.sleep(800);
STATE_MAP.put(msg.getTransactionId(),
LocalTransactionState.COMMIT_MESSAGE);
// 二次提交确认
return LocalTransactionState.COMMIT_MESSAGE;
} catch (Exception e) {
e.printStackTrace();
}
STATE_MAP.put(msg.getTransactionId(),
LocalTransactionState.ROLLBACK_MESSAGE);
// 回滚
return LocalTransactionState.ROLLBACK_MESSAGE;
}
/**
* 消息回查
*
* @param msg
* @return
*/
@Override
public LocalTransactionState checkLocalTransaction(MessageExt msg) {
return STATE_MAP.get(msg.getTransactionId());
}
}
7. consumer详解
7.1 push和pull模式
在RocketMQ中,消费者有两种模式,一种是push模式,另一种是pull模式。
push模式:客户端与服务端建立连接后,当服务端有消息时,将消息推送到客户端。
pull模式:客户端不断的轮询请求服务端,来获取新的消息。
但在具体实现时,Push和Pull模式都是采用消费端主动拉取的方式,即consumer轮询从broker拉取消息。
区别:
Push方式里,consumer把轮询过程封装了,并注册MessageListener监听器,取到消息后,唤醒
MessageListener的consumeMessage()来消费,对用户而言,感觉消息是被推送过来的。
Pull方式里,取消息的过程需要用户自己写,首先通过打算消费的Topic拿到MessageQueue的集合,遍历
MessageQueue集合,然后针对每个MessageQueue批量取消息,一次取完后,记录该队列下一次要取的开
始offset,直到取完了,再换另一个MessageQueue。
疑问:既然是采用pull方式实现,RocketMQ如何保证消息的实时性呢?
7.2 长轮询
长轮询即是在请求的过程中,若是服务器端数据并没有更新,那么则将这个连接挂起,直到服务器推送新的
数据,再返回,然后进入循环周期。
客户端像传统轮询一样从服务端请求数据,服务端会阻塞请求不会立刻返回,直到有数据或超时才返回给客
户端,然后关闭连接,客户端处理完响应信息后再向服务器发送新的请求
7.3 消息模式
DefaultMQPushConsumer实现了自动保存offset值以及实现多个consumer的负载均衡。
//设置组名
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("HAOKE_IM");
通过groupname将多个consumer组合在一起,那么就会存在一个问题,消息发送到这个组后,消息怎么分配呢?
这个时候,就需要指定消息模式,分别有集群和广播模式。
- 集群模式
同一个 ConsumerGroup(GroupName相同) 里的每 个 Consumer 只消费所订阅消息的一部分内容, 同
一个 ConsumerGroup 里所有的 Consumer消费的内容合起来才是所订阅 Topic 内容的整体, 从而达到
负载均衡的目的 。
- 广播模式
同一个 ConsumerGroup里的每个 Consumer都 能消费到所订阅 Topic 的全部消息,也就是一个消息会
被多次分发,被多个 Consumer消费。
// 集群模式
consumer.setMessageModel(MessageModel.CLUSTERING);
// 广播模式
consumer.setMessageModel(MessageModel.BROADCASTING);
7.4 重复消息的解决方案
造成消息重复的根本原因是:网络不可达。只要通过网络交换数据,就无法避免这个问题。所以解决这个问题的办
法就是绕过这个问题。那么问题就变成了:如果消费端收到两条一样的消息,应该怎样处理?
1. 消费端处理消息的业务逻辑保持幂等性
2. 保证每条消息都有唯一编号且保证消息处理成功与去重表的日志同时出现
第1条很好理解,只要保持幂等性,不管来多少条重复消息,最后处理的结果都一样。第2条原理就是利用一张日志
表来记录已经处理成功的消息的ID,如果新到的消息ID已经在日志表中,那么就不再处理这条消息。
第1条解决方案,很明显应该在消费端实现,不属于消息系统要实现的功能。第2条可以消息系统实现,也可以业务
端实现。正常情况下出现重复消息的概率其实很小,如果由消息系统来实现的话,肯定会对消息系统的吞吐量和高
可用有影响,所以最好还是由业务端自己处理消息重复的问题,这也是RocketMQ不解决消息重复的问题的原因。
RocketMQ不保证消息不重复,如果你的业务需要保证严格的不重复消息,需要你自己在业务端去重
8. RocketMQ的存储
RocketMQ中的消息数据存储,采用了零拷贝技术(使用 mmap + write 方式),文件系统采用 Linux Ext4 文件系
统进行存储。
8.1 消息数据的存储
在RocketMQ中,消息数据是保存在磁盘文件中,为了保证写入的性能,RocketMQ尽可能保证顺序写入,顺序写
入的效率比随机写入的效率高很多。
RocketMQ消息的存储是由ConsumeQueue和CommitLog配合完成的,CommitLog是真正存储数据的文件,
ConsumeQueue是索引文件,存储数据指向到物理文件的配置。