RocketMQ 学习随笔(1)
文章目录
阿里文档
核心概念
- Topic:消息主题,一级消息类型,生产者向其发送消息。
- 生产者:也称为消息发布者,负责生产并发送消息至Topic。
- 消费者:也称为消息订阅者,负责从Topic接收并消费消息。
- 消息:生产者向Topic发送并最终传送给消费者的数据和(可选)属性的组合。
- 消息属性:生产者可以为消息定义的属性,包含Message Key和Tag。
- Group:一类生产者或消费者,这类生产者或消费者通常生产或消费同一类消息,且消息发布或订阅的逻辑一致。
消息类型
消息队列提供的四种消息类型不能混用.创建的普通消息的Topic只能用来收发普通消息,不能用来收发其他类型的消息;同理,事务消息的Topic也只能收发事务消息,不能用来收发其他类型的消息.
.普通消息
. 普通消息是指消息队列RocketMQ版中无特性的消息,区别于有特性的定 时和延时消息、顺序消息和事务消息。
.定时和延时消息
- 定时消息:Producer将消息发送到消息队列RocketMQ版服务端,但并不期望立马投递这条消息,而是推迟到在当前时间点之后的某一个时间投递到Consumer进行消费,该消息即定时消息。
- 延时消息:Producer将消息发送到消息队列RocketMQ版服务端,但并不期望立马投递这条消息,而是延迟一定时间后才投递到Consumer进行消费,该消息即延时消息。
.顺序消息
顺序消息(FIFO消息)是消息队列RocketMQ版提供的一种严格按照顺 序来发布和消费的消息。顺序发布和顺序消费是指对于指定的一个 Topic,生产者按照一定的先后顺序发布消息;消费者按照既定的先后 顺序订阅消息,即先发布的消息一定会先被客户端接收到。
.事务消息
- 事务消息:消息队列RocketMQ版提供类似X或Open XA的分布式事务功能,通过消息队列RocketMQ版事务消息能达到分布式事务的最终一致。
- 半事务消息:暂不能投递的消息,发送方已经成功地将消息发送到了消息队列RocketMQ版服务端,但是服务端未收到生产者对该消息的二次确认,此时该消息被标记成“暂不能投递”状态,处于该种状态下的消息即半事务消息。
- 消息回查:由于网络闪断、生产者应用重启等原因,导致某条事务消息的二次确认丢失,消息队列RocketMQ版服务端通过扫描发现某条消息长期处于“半事务消息”时,需要主动向消息生产者询问该消息的最终状态(Commit或是Rollback),该询问过程即消息回查。
消息重试
顺序消息的重试
对于顺序消息,当消费者消费消息失败后,消息队列RocketMQ版会自 动不断地进行消息重试(每次间隔时间为1秒),这时,应用会出现消 息消费被阻塞的情况。因此,建议您使用顺序消息时,务必保证应用能 够及时监控并处理消费失败的情况,避免阻塞现象的发生。
无序消息的重试
对于无序消息(普通、定时、延时、事务消息),当消费者消费消息失 败时,您可以通过设置返回状态达到消息重试的结果。
无序消息的重试只针对集群消费方式生效;广播方式不提供失败重试特 性,即消费失败后,失败消息不再重试,继续消费新的消息。
消息过滤
Tag,即消息标签,用于对某个Topic下的消息进行分类。消息队列 RocketMQ版的生产者在发送消息时,已经指定消息的Tag,消费者需根 据已经指定的Tag来进行订阅。
Exactly-Once投递语义
Exactly-Once是指发送到消息系统的消息只能被消费端处理且仅处理一次,即使生产端重试消息发送导致某消息重复投递,该消息在消费端也只被消费一次。
批量消费
消息队列RocketMQ版接收到生产者发送地消息后,不需要分开一条一条的推送给消费者,可以先将消息进行缓存,等攒够指定数量的消息或等待指定的时长后统一将缓存的这些消息推送给消费者进行批量消费。
1.MQ介绍
1.为什么要用MQ?
.解耦:从RPC调用变为异步调用
.异步
.削峰:应对突发大流量问题
.数据分发
2.MQ的缺点
.系统复杂性提高:事务消息,重试机制
.系统可用性降低
.系统一致性问题:分布式消息
2.RocketMQ快速入门
快速开始
https://yuya008.gitbooks.io/apache-rocketmq/content/kuai-su-kai-shi.html
namesrv
启动命令:nohup sh mqnamesrv &
关闭命令:sh bin/mqshutdown namesrv
broker
启动命令:nohup sh mqbroker -n localhost:9876 &
关闭命令:sh bin/mqshutdown broker
消息的发送以及接收
在发送和接受消息之前,我们必须告诉客户端name server的位置。RocketMQ提供多种方式获得,简单的办法就是使用环境变量NAMESRV_ADDR
export NAMESRV_ADDR=localhost:9876
sh bin/tools.sh org.apache.rocketmq.example.quickstart.Producer
sh bin/tools.sh org.apache.rocketmq.example.quickstart.Consumer
各角色介绍:
-
Producer:消息发送者
-
Consumer:消息接收者
-
Broker:暂存和传输消息
主从模式(Master-Slave),主节点处理写操作,从节点处理读操作.
通过BrokerName分组,通过Broker id确定主从. -
NameServer:管理Broker
无状态节点,集群中所有节点保存着一样的信息 -
Topic:区分消息的种类
-
Message Queue:相当于Topic的分区,用于并行发送和接收消息
集群模式
单Master模式,多Master模式,多Master多Slave模式.
工作流程
- 启动NameServer,NameServer起来后监听端口,等待Broker,Producer,Consumer连上来,相当于一个控制中心.
- Broker启动,跟所有NameServer保持长连接,定时发送心跳包.心跳包中包含者当前Broker信息(IP+端口号等)以及存储的所有Topic信息.注册成功后,NameServer集群中就有了所有的Broker跟Topic的映射关系.
- 收发消息前,先创建Topic,创建Topic时,需要指定当前Topic要存储在哪些Broker上,也可以在消息发送时自动创建Topic.
- Producer发送消息,启动时先跟NameServer集群中的一台创建一个长连接,并从NameServer中获取当前发送的Topic需要存储在哪些Broker上,轮询从队列列表中选出一条队列,然后与队列列表所在的Broker建立长连接从而向Broker发送消息.
- Consumer跟Producer类似,跟其中一台NameServer建立长连接,获取当前订阅Topic存在哪些Broker上,然后直接跟Broker建立连接通道,开始消费消息.
集群搭建学习
此处作为了解,非重点学习部分.
broker.conf 配置文件解析
#所属集群名字
brokerClusterName = DefaultCluster
#broker名字,注意此处不同的配置文件填写的不一样
brokerName = broker-a
#0代表master,>0代表slave
brokerId = 0
#nameServer地址,分号分隔
namesrvAddr=rocketmq-namesrv1:9876;rocketmq-namesrv2:9876
#在发送消息时,自动创建服务器不存在的Topic,默认创建队列数
defaultTopicQueueNums=4
#是否允许broker自动创建Topic,建议线下开启,线上关闭
autoCreateTopicEnable=true
#是否允许broker自动创建订阅组,建议线下开启,线上关闭
autoCreateSubscriptionGroup-true
#broker 对外提供服务的监听端口
listenPort=10911
#删除文件时间点,默认凌晨四点
deleteWhen = 04
#文件保留时间,默认48小时
fileReservedTime = 48
# commitLog每个文件的大小默认1G
mapedFileSizeCommitLog=1073741824
# ConsumeQueue每个文件默认存30w条, 根据业务情况调整
mapedFileSizeConsumeQueue=30000
# destroyMapedFileIntervalForcibly=12000
# redeleteHangedFileInterval=12000
# 检测物理文件磁盘空间
diskMaxUsedSpaceRatio=88
# 存储路径
storePathRootDir=/usr/local/server/mq/rocketmq/store
# commitLog存储路径
storePathCommitLog=/usr/local/server/mq/rocketmq/store/commitlog
# 消息队列储存路径
storePathConsumeQueue=/usr/local/server/mq/rocketmq/store/consumequeue
# 消息索引粗存路径
storePathIndex=/usr/local/server/mq/rocketmq/store/index
# checkpoint 文件储存路径
storeCheckpoint=/usr/local/server/mq/rocketmq/store/checkpoint
# abort 文件存储路径
abortFile=/usr/local/server/mq/rocketmq/store/abort
# 限制的消息大小
maxMessageSize=65536
# flushCommitLogLeastPages=4
# flushConsumeQueueLeastPages=2
# flushCommitLogThoroughInterval=10000
# flushConsumeQueueThoroughInterval=60000
# Broker的角色
# -ASYNC_MASTER 异步复制Master
# -SYNC_MASTER 同步双写Master
# -SLAVE
brokerRole=ASYNC_MASTER
# 刷盘方式
# - ASYNC_FLUSH 异步刷盘
# - SYNC_FLUSH 同步刷盘
flushDiskType=ASYNC_FLUSH
# checkTransactionMessageEnable=false
# 发消息线程池数量
# sendMessageTreadPoolNums=128
# 拉消息线程池数量
# pullMessageTreadPoolNums=128lushDiskType=ASYNC_FLUSHH
mqadmin管理工具
位置:bin目录下, 命令./mqadmin {command} {args}
具体命令介绍:
https://blog.csdn.net/m0_46201444/article/details/107514689
图形化管理工具
RocketMQ消息发送与接收
-
消息发送者步骤分析
- 创建消息生产者Producer,并指定生产者组名
- 指定NameServer地址
- 启动Producer
- 创建消息对象,指定主题Topic,Tag和消息体
- 发送消息
- 关闭生产者Producer
-
消息消费者步骤分析
- 创建消费者Consumer,指定消费者组名
- 指定NameServer地址
- 订阅主题Topic和Tag
- 设置回调函数,处理消息
- 启动消费者Consumer
1.添加依赖
<dependency>
<groupId>org.apache.rocketmq</groupId>
<artifactId>rocketmq-client</artifactId>
<version>4.8.0</version>
</dependency>
2.同步发送消息
同步消息在发送后需要等待返回发送结果;
public class SyncProducer {
public static void main(String[] args) throws MQClientException, RemotingException, InterruptedException, MQBrokerException {
//- 创建消息生产者Producer,并指定生产者组名
DefaultMQProducer defaultMQProducer = new DefaultMQProducer("group1");
//- 指定NameServer地址
defaultMQProducer.setNamesrvAddr("127.0.0.1:9876");
//- 启动Producer
defaultMQProducer.start();
defaultMQProducer.setSendMsgTimeout(6000);
defaultMQProducer.setVipChannelEnabled(false);
//- 创建消息对象,指定主题Topic,Tag和消息体
for (int i = 0; i < 10; i++) {
Message message = new Message();
message.setTopic("topic");
message.setTags("tag1");
String body = "body" + ++i;
message.setBody(body.getBytes());
//- 发送消息
SendResult result = defaultMQProducer.send(message);
//消息发送状态
SendStatus sendStatus = result.getSendStatus();
//消息id
String msgId = result.getMsgId();
//消息发送队列id
int queueId = result.getMessageQueue().getQueueId();
System.out.println("消息发送状态:" + sendStatus + "\n消息id:" + msgId + "\n消息发送队列id:" + queueId);
TimeUnit.SECONDS.sleep(1);
}
//- 关闭生产者Producer
defaultMQProducer.shutdown();
}
}
问题总结:由于RocketMQ搭建在云服务器上,服务器中使用内网IP无影响,但是在远程连接服务器时会造成消息发送超时的现象.因此在配置Broker.conf文件时需要指定brokerIP=公网ip;
可以通过mqadmin提供的工具,查看broker的注册情况等.
启动Broker时可以通过-c参数指定配置文件.
3.发送异步消息
异步消息实现回调接口,发送后无需等待发送结果.
//异步发送消息
public class AsyncProducer {
public static void main(String[] args) throws MQClientException, RemotingException, InterruptedException {
DefaultMQProducer producer = new DefaultMQProducer("group1");
producer.setVipChannelEnabled(false);
producer.setSendMsgTimeout(6000);
producer.setNamesrvAddr("127.0.0.1:9876");
producer.start();
for (int i = 0; i < 10; i++) {
Message message = new Message();
message.setTopic("topic");
message.setTags("tag2");
String body = "body" + ++i;
message.setBody(body.getBytes());
producer.send(message, new SendCallback() {
@Override
public void onSuccess(SendResult sendResult) {
System.out.println("发送成功:" + sendResult);
}
@Override
public void onException(Throwable throwable) {
System.out.println("发送异常");
}
});
TimeUnit.SECONDS.sleep(1);
}
producer.shutdown();
}
}
4.单向传输
单向传输应用于日志的收集等只需要发送,不需要返回的业务场景.
public class OnewayProducer {
public static void main(String[] args) throws RemotingException, MQClientException, InterruptedException {
//- 创建消息生产者Producer,并指定生产者组名
DefaultMQProducer defaultMQProducer = new DefaultMQProducer("group1");
//- 指定NameServer地址
defaultMQProducer.setNamesrvAddr("127.0.0.1:9876");
//- 启动Producer
defaultMQProducer.start();
defaultMQProducer.setSendMsgTimeout(6000);
defaultMQProducer.setVipChannelEnabled(false);
//- 创建消息对象,指定主题Topic,Tag和消息体
for (int i = 0; i < 10; i++) {
Message message = new Message();
message.setTopic("topic");
message.setTags("tag3");
String body = "body" + ++i;
message.setBody(body.getBytes());
//- 发送消息
defaultMQProducer.sendOneway(message);
TimeUnit.SECONDS.sleep(1);
}
//- 关闭生产者Producer
defaultMQProducer.shutdown();
}
}
5.消息的消费
public class Consumer {
public static void main(String[] args) throws MQClientException {
// - 创建消费者Consumer,指定消费者组名
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("group1");
// - 指定NameServer地址
consumer.setNamesrvAddr("127.0.0.1:9876");
// - 订阅主题Topic和Tag
consumer.subscribe("topic", "tag3");
// - 设置回调函数,处理消息
consumer.registerMessageListener(new MessageListenerConcurrently() {
@Override
public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> list, ConsumeConcurrentlyContext consumeConcurrentlyContext) {
for (MessageExt messageExt : list) {
byte[] body = messageExt.getBody();
System.out.println("msgBody:" + new String(body));
}
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
}
});
// - 启动消费者Consumer
consumer.start();
}
}
6.顺序消息
- 全局消息顺序
- 局部消息顺序(将有先后顺序的消息发送到同一个队列中)
适用于一些有严格先后顺序的业务场景.如:下单->付款->发货等
public class OrderedProducer {
public static void main(String[] args) throws MQClientException, UnsupportedEncodingException, RemotingException, InterruptedException, MQBrokerException {
//Instantiate with a producer group name.
MQProducer producer = new DefaultMQProducer("example_group_name");
//Launch the instance.
producer.start();
String[] tags = new String[] {"TagA", "TagB", "TagC", "TagD", "TagE"};
for (int i = 0; i < 100; i++) {
int orderId = i % 10;
//Create a message instance, specifying topic, tag and message body.
Message msg = new Message("TopicTestjjj", tags[i % tags.length], "KEY" + i,
("Hello RocketMQ " + i).getBytes(RemotingHelper.DEFAULT_CHARSET));
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);
System.out.printf("%s%n", sendResult);
}
//server shutdown
producer.shutdown();
}
}
public class OrderedConsumer {
public static void main(String[] args) throws Exception {
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("example_group_name");
consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_FIRST_OFFSET);
consumer.subscribe("TopicTest", "TagA || TagC || TagD");
consumer.registerMessageListener(new MessageListenerOrderly() {
AtomicLong consumeTimes = new AtomicLong(0);
@Override
public ConsumeOrderlyStatus consumeMessage(List<MessageExt> msgs,
ConsumeOrderlyContext context) {
context.setAutoCommit(false);
System.out.printf(Thread.currentThread().getName() + " Receive New Messages: " + msgs + "%n");
this.consumeTimes.incrementAndGet();
if ((this.consumeTimes.get() % 2) == 0) {
return ConsumeOrderlyStatus.SUCCESS;
} else if ((this.consumeTimes.get() % 3) == 0) {
return ConsumeOrderlyStatus.ROLLBACK;
} else if ((this.consumeTimes.get() % 4) == 0) {
return ConsumeOrderlyStatus.COMMIT;
} else if ((this.consumeTimes.get() % 5) == 0) {
context.setSuspendCurrentQueueTimeMillis(3000);
return ConsumeOrderlyStatus.SUSPEND_CURRENT_QUEUE_A_MOMENT;
}
return ConsumeOrderlyStatus.SUCCESS;
}
});
consumer.start();
System.out.printf("Consumer Started.%n");
}
}
7.批量消息
public static void main(String[] args) throws MQClientException, RemotingException, InterruptedException, MQBrokerException {
//- 创建消息生产者Producer,并指定生产者组名
DefaultMQProducer defaultMQProducer = new DefaultMQProducer("group1");
//- 指定NameServer地址
defaultMQProducer.setNamesrvAddr("127.0.0.1:9876");
//- 启动Producer
defaultMQProducer.start();
defaultMQProducer.setSendMsgTimeout(6000);
defaultMQProducer.setVipChannelEnabled(false);
List<Message> msgs = new ArrayList<>();
//- 创建消息对象,指定主题Topic,Tag和消息体
for (int i = 0; i < 10; i++) {
Message message = new Message();
message.setTopic("batchTopic");
message.setTags("tag1");
String body = "body" + ++i;
message.setBody(body.getBytes());
msgs.add(message);
}
//发送批量消息
SendResult send = defaultMQProducer.send(msgs);
System.out.println(send);
//- 关闭生产者Producer
defaultMQProducer.shutdown();
}
批量消息大小不能超过4M,超过之后需要对消息进行分割.
批量消息不支持广播模式.
8.消息过滤
大多数情况下可以通过标签做过滤.
使用者将收到包含TAGA或TAGB或TAGC的消息。但是限制是,一条消息只能有一个标签,这可能不适用于复杂的情况。 应对复杂情况,可以使用SQL表达式来过滤消息.
使用SQL92需要在broker配置文件中添加:enablePropertyFilter=true
public static void main(String[] args) throws InterruptedException, RemotingException, MQClientException, MQBrokerException {
//- 创建消息生产者Producer,并指定生产者组名
DefaultMQProducer defaultMQProducer = new DefaultMQProducer("group1");
//- 指定NameServer地址
defaultMQProducer.setNamesrvAddr("127.0.0.1:9876");
//- 启动Producer
defaultMQProducer.start();
defaultMQProducer.setSendMsgTimeout(6000);
defaultMQProducer.setVipChannelEnabled(false);
//- 创建消息对象,指定主题Topic,Tag和消息体
for (int i = 0; i < 10; i++) {
Message message = new Message();
message.setTopic("filterTopic");
message.setTags("tag1");
String body = "body" + ++i;
message.setBody(body.getBytes());
message.putUserProperty("i", String.valueOf(i));
//- 发送消息
SendResult result = defaultMQProducer.send(message);
//消息发送状态
SendStatus sendStatus = result.getSendStatus();
//消息id
String msgId = result.getMsgId();
//消息发送队列id
int queueId = result.getMessageQueue().getQueueId();
System.out.println("消息发送状态:" + sendStatus + "\n消息id:" + msgId + "\n消息发送队列id:" + queueId);
TimeUnit.SECONDS.sleep(1);
}
//- 关闭生产者Producer
defaultMQProducer.shutdown();
}
public static void main(String[] args) throws MQClientException {
// - 创建消费者Consumer,指定消费者组名
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("group1");
// - 指定NameServer地址
consumer.setNamesrvAddr("127.0.0.1:9876");
// - 订阅主题Topic和Tag
consumer.subscribe("filterTopic", MessageSelector.bySql("i>5"));
//设置消息消费模式 负载均衡||广播
consumer.setMessageModel(MessageModel.BROADCASTING);
// - 设置回调函数,处理消息
consumer.registerMessageListener(new MessageListenerConcurrently() {
@Override
public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> list, ConsumeConcurrentlyContext consumeConcurrentlyContext) {
for (MessageExt messageExt : list) {
byte[] body = messageExt.getBody();
System.out.println("msgBody:" + new String(body));
}
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
}
});
// - 启动消费者Consumer
consumer.start();
System.out.println("消费者启动");
}
9.事务消息
1.分布式事务-两阶段提交协议
两阶段提交协议(Two-phase Commit,2PC)经常被用来实现分布式事务。一般分为协调器TC和若干事务执行者两种角色,这里的事务执行者就是具体的数据库,协调器可以和事务执行器在一台机器上。
-
第一阶段简述
执行者执行本机事务,不进行commit
-
第二阶段简述
所有执行者都执行成功,协调器发起commit,否则全部回滚
缺点:
- 1.两阶段提交都涉及多次节点间的网络通信,通信时间长
- 2.单点故障.一旦协调者故障,则执行者会一直阻塞.
- 3.数据不一致.由于网络等原因会出现部分参与者未commit而造成数据不一致的情况.
2.使用消息队列来避免分布式事务
事务消息发送步骤如下:
- 发送方将半事务消息发送至消息队列RocketMQ版服务端。
- 消息队列RocketMQ版服务端将消息持久化成功之后,向发送方返回Ack确认消息已经发送成功,此时消息为半事务消息。
- 发送方开始执行本地事务逻辑。
- 发送方根据本地事务执行结果向服务端提交二次确认(Commit或是Rollback),服务端收到Commit状态则将半事务消息标记为可投递,订阅方最终将收到该消息;服务端收到Rollback状态则删除半事务消息,订阅方将不会接受该消息。
事务消息回查步骤如下:
- 在断网或者是应用重启的特殊情况下,上述步骤4提交的二次确认最终未到达服务端,经过固定时间后服务端将对该消息发起消息回查。
- 发送方收到消息回查后,需要检查对应消息的本地事务执行的最终结果。
- 发送方根据检查得到的本地事务的最终状态再次提交二次确认,服务端仍按照步骤4对半事务消息进行操作。
注意事项
-
事务消息的Group ID不能与其他类型消息的Group ID共用。与其他类型的消息不同,事务消息有回查机制,回查时消息队列RocketMQ版服务端会根据Group ID去查询客户端。
-
通过
ONSFactory.createTransactionProducer
创建事务消息的Producer时必须指定LocalTransactionChecker
的实现类,处理异常情况下事务消息的回查。 -
事务消息发送完成本地事务后,可在
execute
方法中返回以下三种状态:TransactionStatus.CommitTransaction
:提交事务,允许订阅方消费该消息。TransactionStatus.RollbackTransaction
:回滚事务,消息将被丢弃不允许消费。TransactionStatus.Unknow
:暂时无法判断状态,等待固定时间以后消息队列RocketMQ版服务端向发送方进行消息回查。
-
可通过以下方式给每条消息设定第一次消息回查的最快时间:
Message message = new Message(); // 在消息属性中添加第一次消息回查的最快时间,单位秒。例如,以下设置实际第一次回查时间为120秒~125秒之间message.putUserProperties(PropertyKeyConst.CheckImmunityTimeInSeconds,"120"); // 以上方式只确定事务消息的第一次回查的最快时间,实际回查时间向后浮动0秒~5秒;如第一次回查后事务仍未提交,后续每隔5秒回查一次