文章目录
学习参考链接 【部分内容为搬运,参照官网】
消息队列的作用
- 应用解耦
AB应用不在互相依赖; 传统项目中A,B应用的消息发送是直连的,并且如果网络数据传递过程中有问题,可能导致某一方数据迟迟获取不到;并且可能存在单点故障问题,此时就需要人工或其他工具进行监控;而使用消息队列后会将消息先存储在中间件中,等合适的时机在进行消费,避免了强耦合。
- 流量削峰
流量达到高峰的时候,通常使用限流算法来控制流量涌入系统,避免系统被击瘫,但是这种方式损失了一部分请求
此时可以使用消息中间件来缓冲大量的请求,匀速消费,当消息队列中堆积消息过多时,我们可以动态上线增加消费端,来保证不丢失重要请求。
- 大数据处理
消息中间件可以把各个模块中产生的管理员操作日志、用户行为、系统状态等数据文件作为消息收集到主题中
数据使用方可以订阅自己感兴趣的数据内容互不影响,进行消费
- 异构系统
跨语言
基本概念
RocketMQ中主要包括消费者,生产者,Broker,Topic;
生产者:产生数据,可以同时存在多个生产者同时发送数据;生产者生产的数据会属于一种topic(场景/主题,一种逻辑概念)
消费者:消费数据,存在消费组的概念,每一个组中可能存在多个消费者,消费者可以从多个队列中进行消费,但是每条消息只能被同一个消费者消费一次(集群消费情况下)
Broker:一般为一个物理节点,也可以理解成是一个服务;每种topic的数据可以跨越多个broker,同时每个broker也可以存储多种topic的数据;节点启动的时候会扫描NameServer,然后注册自己的信息然后定时上报。
Queue:实际存储数据的区域,每种topic中会存在多个queue,并且可以不均匀的分布在不同的broker中
NameServer: 可以类比Zookeeper,起到分布式协调的作用,充当路由的作用,会记录broker的元数据,并为producer/consumer暴露服务,告诉数据属于哪个broker-》queue当中。可以存在多个NameServer做集群。
RocketMQ的消息模型:
topic可以理解成一个大主题,还可以对其中的数据继续划分为不同的tag,可以将整个数据集粒度更细化,方便做索引提高查询效率。
RoketMQ中的两种获取数据方式:
- pull(拉取): 应用通常主动调用Consumer的拉消息方法从Broker服务器拉消息、主动权由应用控制。一旦获取了批量消息,应用就会启动消费过程。
- push(推送):该模式下Broker收到数据后会主动推送给消费端,该消费模式一般实时性较高。但是对于producer/consumer能力不同的时候,生产/消费能力可能不成正比,导致数据的丢失。
Consumer的消费方式:
- 集群消费:集群消费模式下,相同Consumer Group的每个Consumer实例平均分摊消息。当queue:consumer的时候1:1平摊;如果1:N 轮询。
- 广播消费:每次消息的发送,相同Consumer Group的每个Consumer实例都接收全量的消息。
消息的消费顺序:
- 顺序消费:也可以理解是局部有序,消费者通过同一个消息队列( Topic 分区,称作 Message Queue) 收到的消息是有顺序的,不同消息队列收到的消息则可能是无顺序的。

当consumer拉取数据的时候,可能先获取a组或b组,宏观看可能产生 a1 b1 a2 a3 b2…,但是从不同的主题消息看,是局部有序的
- 严格顺序:不存在参杂的情况。可以得到:必须只有一个生产者,一个queue;因为一旦存在多个生产者,总会存在网络上的问题导致进入broker的顺序可能发生改变;而如果在生产数据的时候引入分布式事务,锁等其他技术也会导致整体的性能下降,所以需要更具不同的情况考虑。
所有消息按照严格的先入先出(FIFO)的顺序进行发布和消费。 适用场景:性能要求不高,所有的消息严格按照 FIFO 原则进行消息发布和消费的场景
只有一个queue的说明:顺序消费的原理解析,在默认的情况下消息发送会采取Round Robin轮询方式把消息发送到不同的queue(分区队列);而消费消息的时候从多个queue上拉取消息,这种情况发送和消费是不能保证顺序。但是如果控制发送的顺序消息只依次发送到同一个queue中,消费的时候只从这个queue上依次拉取,则就保证了顺序。当发送和消费参与的queue只有一个,则是全局有序;如果多个queue参与,则为分区有序,即相对每个queue,消息都是有序的。
RocketMQ的可靠性: (来自官方文档)
RocketMQ支持消息的高可靠,影响消息可靠性的几种情况:
- Broker非正常关闭
- Broker异常Crash
- OS Crash
- 机器掉电,但是能立即恢复供电情况
- 机器无法开机(可能是cpu、主板、内存等关键设备损坏)
- 磁盘设备损坏
1)、2)、3)、4) 四种情况都属于硬件资源可立即恢复情况,RocketMQ在这四种情况下能保证消息不丢,或者丢失少量数据(依赖刷盘方式是同步还是异步)。
5)、6)属于单点故障,且无法恢复,一旦发生,在此单点上的消息全部丢失。RocketMQ在这两种情况下,通过异步复制,可保证99%的消息不丢,但是仍然会有极少量的消息可能丢失。通过同步双写技术可以完全避免单点,同步双写势必会影响性能,适合对消息可靠性要求极高的场合,例如与Money相关的应用。注:RocketMQ从3.0版本开始支持同步双写。
Rocket中的broker具有集群-主从模式,对于每个主节点的broker,会将自己的消息同步到备机中;每个生产者只能向主机中写入数据,而消费者可以从主/备获取数据。
Broker配置文件
#所属集群名字
brokerClusterName=rocketmq-cluster
#broker名字,注意此处不同的配置文件填写的不一样
brokerName=broker-a
#0 表示 Master,>0 表示 Slave
brokerId=0
#nameServer地址,分号分割
namesrvAddr=rocketmq-nameserver1:9876;rocketmq-nameserver2:9876
#在发送消息时,自动创建服务器不存在的topic,默认创建的队列数
defaultTopicQueueNums=4
#是否允许 Broker 自动创建Topic,建议线下开启,线上关闭
autoCreateTopicEnable=true
#是否允许 Broker 自动创建订阅组,建议线下开启,线上关闭
autoCreateSubscriptionGroup=true
#Broker 对外服务的监听端口
listenPort=10911
#删除文件时间点,默认凌晨 4点
deleteWhen=04
#文件保留时间,默认 48 小时
fileReservedTime=120
#commitLog每个文件的大小默认1G
mapedFileSizeCommitLog=1073741824
#ConsumeQueue每个文件默认存30W条,根据业务情况调整
mapedFileSizeConsumeQueue=300000
#destroyMapedFileIntervalForcibly=120000
#redeleteHangedFileInterval=120000
#检测物理文件磁盘空间
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
#限制的消息大小
maxMessageSize=65536
#flushCommitLogLeastPages=4
#flushConsumeQueueLeastPages=2
#flushCommitLogThoroughInterval=10000
#flushConsumeQueueThoroughInterval=60000
#Broker 的角色
#- ASYNC_MASTER 异步复制Master
#- SYNC_MASTER 同步双写Master
#- SLAVE
brokerRole=SYNC_MASTER
#刷盘方式
#- ASYNC_FLUSH 异步刷盘
#- SYNC_FLUSH 同步刷盘
flushDiskType=SYNC_FLUSH
#checkTransactionMessageEnable=false
#发消息线程池数量
#sendMessageThreadPoolNums=128
#拉消息线程池数量
#pullMessageThreadPoolNums=128
API的使用
环境准备:两台物理主机节点:192.18.189.130(node1), 192.168.189.131(node02)
分别启动nameserver和broker,并将broker全部指向node01的nameserver



后面会访问nameserver,需要打开6379端口或者关闭防火墙
- 导入rocketmq的依赖
<dependency>
<groupId>org.apache.rocketmq</groupId>
<artifactId>rocketmq-client</artifactId>
<version>4.9.1</version>
</dependency>
Producer端发送同步消息
public void testProducer() throws MQClientException, MQBrokerException, RemotingException, InterruptedException {
DefaultMQProducer producer = new DefaultMQProducer("righteye");
// 指定nameserver
producer.setNamesrvAddr("192.168.189.130:9876");
// 注意需要启动
producer.start();
// 发送消息
for (int i = 0; i < 10; i++) {
Message message = new Message();
message.setTopic("test");
message.setTags("tagA");
message.setBody(("righteye:" + i).getBytes(StandardCharsets.UTF_8));
// 设置等待broker存储完成返回结果
message.setWaitStoreMsgOK(true);
// 发送消息
SendResult send = producer.send(message);
System.out.println(send);
}
默认自动开启自动创建topic,因此可以忽略创建topic的过程
消息随机分发到不同的队列当中,后面的offset是数据的偏移量
Producer发送异步消息
DefaultMQProducer producer = new DefaultMQProducer("righteye");
// 指定nameserver
producer.setNamesrvAddr("192.168.189.130:9876");
// 注意需要启动
producer.start();
// 发送消息
for (int i = 0; i < 10; i++) {
Message message = new Message("test", "tagA", ("righteye:" + i).getBytes(StandardCharsets.UTF_8));
// 设置等待broker存储完成返回结果
message.setWaitStoreMsgOK(true);
// 异步发送消息
producer.send(message, new SendCallback() {
@Override
public void onSuccess(SendResult sendResult) {
// 消息发送成功的时候
System.out.println("success:" + sendResult);
}
@Override
public void onException(Throwable throwable) {
}
});
// 睡眠,等待异步结果返回
Thread.sleep(5000);
}
Producer发送消息到指定queue
DefaultMQProducer producer = new DefaultMQProducer("righteye");
// 指定nameserver
producer.setNamesrvAddr("192.168.189.130:9876");
// 注意需要启动
producer.start();
// 发送消息
for (int i = 0; i < 10; i++) {
Message message = new Message("test", "tagA", ("righteye:" + i).getBytes(StandardCharsets.UTF_8));
// 设置等待broker存储完成返回结果
message.setWaitStoreMsgOK(true);
// topic下可以属于多个broker,并且可以包括多个queue,这里指定为0号队列
MessageQueue queue = new MessageQueue("test", "localhost.localdomain", 0);
SendResult send = producer.send(message, queue);
System.out.println(send);
}
- broker的名字:在启动broker的时候会显示在控制台中
- 运行结果
RocketMQ的监控组件
- 导入依赖
<dependency>
<groupId>org.apache.rocketmq</groupId>
<artifactId>rocketmq-tools</artifactId>
<version>4.9.2</version>
</dependency>
- 实现代码
@Test
public void testAdmin() throws org.apache.rocketmq.remoting.exception.RemotingException, InterruptedException, org.apache.rocketmq.client.exception.MQClientException {
DefaultMQAdminExt adminExt = new DefaultMQAdminExt();
adminExt.setNamesrvAddr("192.168.189.130:9876");
// 启动服务
adminExt.start();
TopicList topicList = adminExt.fetchAllTopicList();
Set<String> topicList1 = topicList.getTopicList();
for (String s : topicList1) {
System.out.println(s);
}
System.out.println("----------topic list------------");
// 获取topic: test下的信息
TopicRouteData res = adminExt.examineTopicRouteInfo("test");
System.out.println(res);
}

推送的方式获取数据
通过订阅相关的topic,当有消息的时候自动推送到消费者供其消费;
@Test
public void testConsumerPush() throws org.apache.rocketmq.client.exception.MQClientException, IOException {
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("consumer_righteye");
consumer.setNamesrvAddr("192.168.189.130:9876");
// 订阅一个或者多个Topic,以及Tag来过滤需要消费的消息
consumer.subscribe("test", "*");
// 绑定监听器
consumer.registerMessageListener(new MessageListenerConcurrently() {
@Override
public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> list, ConsumeConcurrentlyContext consumeConcurrentlyContext) {
for (MessageExt messageExt : list) {
String res = new String(messageExt.getBody());
System.out.println(res);
}
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
}
});
consumer.start();
System.in.read();
}
三、顺序消费
3.1 无序消息
- 模拟生产者
// 生产消息,无序消费
@Test
public void producerInfo() throws Exception {
DefaultMQProducer producer = new DefaultMQProducer("producer-1");
producer.setNamesrvAddr("192.168.189.131:9876");
producer.start();
// 模拟生产10条消息
for (int i = 0; i < 10; i++) {
String body = "Message " + i;
// key 用来做检索,最好全局唯一
Message message = new Message("righteye1", "TagA", "key:" + i, body.getBytes());
// 发送消息
SendResult send = producer.send(message);
System.out.println(send);
}
}

生产10条消息,最后看结果可以看到消息分发到不同的队列当中
- 模拟消费者
@Test
public void consumerInfo() throws Exception {
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("consumer-1");
consumer.setNamesrvAddr("192.168.189.131:9876");
consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_FIRST_OFFSET);
consumer.subscribe("righteye1", "TagA");
// MessageListenerConcurrently 无序
consumer.registerMessageListener(new MessageListenerConcurrently() {
@Override
public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> list, ConsumeConcurrentlyContext consumeConcurrentlyContext) {
Iterator<MessageExt> iterator = list.iterator();
while (iterator.hasNext()) {
try {
MessageExt msg = iterator.next();
String keys = msg.getKeys();
System.out.println(new String(msg.getBody()));
if ("key:1".equals(keys)) {
int a = 1 / 0;
}
} catch (Exception e) {
// 延时重试
return ConsumeConcurrentlyStatus.RECONSUME_LATER;
}
}
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
}
});

- 结果中消息的消费的顺序是乱序的
- 除了10条消息外,Message1会进行重消费,因为key:1的时候自定义发生异常,执行:return ConsumeConcurrentlyStatus.RECONSUME_LATER; 表示延时重试
- 延时重发的消息时间间隔会逐渐变长?
在broker配置文件中存在重试间隔时间:“1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h”; 会根据重试的次数时间增长。- 消费监听器有两种:MessageListenerConcurrently 和 MessageListenerOrderly;
MessageListenerConcurrently: 无序的方式接受消息,返回值除了Success还存在RECONSUME_LATER; 而MessageListenerOrderly表示有序,下面进行演示。
使用admin获取监听的nameserver信息:

在topic下维护了一个重试队列,同时还有死信队列;重试间隔也是用在这里。
3.2 顺序消息生产消费
官网模拟发送了10条数据,通过Id进行分组,相同的id会落于同一个队列中。这里进行模拟
public void producerOrder() throws Exception {
DefaultMQProducer producer = new DefaultMQProducer("producer-2");
producer.setNamesrvAddr("192.168.189.131:9876");
producer.start();
// 模拟生产10条消息,维护消息的有序性
for (int i = 0; i < 10; i++) {
String body = "Message " + i + " type: " + i % 3; // type用来定义不同类别的消息分组
// key 用来做检索,最好全局唯一
Message message = new Message("righteye1", "TagA", "key:" + i, body.getBytes());
// 之前写过:MessageQueue queue = new MessageQueue("test", "localhost.localdomain", 0); 用来指定队列
// 还可以使用一个消息分发器
SendResult send = producer.send(message, new MessageQueueSelector() {
@Override
public MessageQueue select(List<MessageQueue> mqs, Message msg, Object arg) {
Integer idx = (Integer) arg; // 获取消息的分组类别
// 确定分组队列
int index = idx % mqs.size(); // 问题? 这里使用mqs.size(), 如果size是变化的,那么取模运算就不稳定,消息的顺序也就无法保证
// 返回选择的队列
return mqs.get(index);
// 第二种:直接在业务定义的时候将消息指定到固定的队列
// return mqs.get(idx);
}
}, i % 3);
System.out.println(send);
}
producer.start();
}

实验结果:消息通过i % 3发送到3个队列当中,这时要求后续的消费保证从队列中取得的数据是有序的(存在多个队列,所以这里说的是局部有序)
- 消费者模拟
@Test
public void consumerOrder() throws Exception {
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("consumer-2");
consumer.setNamesrvAddr("192.168.189.131:9876");
consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_FIRST_OFFSET);
consumer.subscribe("righteye2", "TagA");
consumer.registerMessageListener(new MessageListenerOrderly() {
@Override
public ConsumeOrderlyStatus consumeMessage(List<MessageExt> msgs, ConsumeOrderlyContext context) {
Iterator<MessageExt> iterator = msgs.iterator();
while (iterator.hasNext()) {
MessageExt msg = iterator.next();
System.out.println(Thread.currentThread().getName() + " send: " + new String(msg.getBody()));
}
// 这里只有SUCCESS,如果有延时重试的概念,数据的顺序无法保证,与目的违背
return ConsumeOrderlyStatus.SUCCESS;
}
});
consumer.start();
System.in.read();
consumer.shutdown();
}
实验结果:

实验结论:
- MessageListenerOrderly 是用来维护消费消息有序性存在的;返回值不同于之前的MessageListenerConcurrently,只有SUCCESS,因为如果存在延时重传,消息的顺序会被打乱。
- 观察结果:type为消息分组,同一type中的消息由相同的线程处理,得到的消息也是顺序递增的,满足有序性。
四、延时队列
消费和生产方式和之前类型,只是在生产消息的时候,消息不会立刻进入目标队列,而是可以自定义消息的延时级别,先进入延时队列,当时间到后在转移到目标队列。
场景:
- 处理订单的时候,为了防止大流量冲击数据库,可以将部分的消息处理过程延长,等到时间到达后再进行处理;
- 此外,对于订单的支付操作,可能存在不能立即支付的情况,如果进行忙等检查订单状态,会有大量无效的请求,此时也可以使用延时队列等待特定时间检查,减少无效的请求数据库
@Test
public void producerDelay() throws Exception {
DefaultMQProducer producer = new DefaultMQProducer("delay_producer");
producer.setNamesrvAddr("192.168.198.131:9876");
producer.start();
for (int i = 0; i < 10; i++) {
Message msga = new Message(
"topic_delay",
"TagD",
("message "+ i).getBytes()
);
//延迟是以消息为级别的
msga.setDelayTimeLevel(i%18);
SendResult send = producer.send(msga); //没有直接进入目标topic的队列,而是进入延迟队列
System.out.println(send);
}
System.in.read();
}
五、分布式事务
微服务环境中,存在两个服务A,B之间的服务调用,通过MQ可以实现A,B的解耦和消息异步,但是在正式通信前,A可能还存在其他的操作,如:数据库持久化,业务逻辑实现,最后是发送消息到MQ,在多步骤的情况下,就需要数据一致性;保证在异常发生,服务宕机的情况下这一批操作也应该要么都发生要么都不发生。
RocketMQ中存在两阶段提交和状态检测;
两阶段提交:A,B服务通信,A会将消息发送到MQ中的状态分为两步,第一阶段为半提交,并由MQ返回一个事务ID,发送方A应该保存这个事务ID表,用于之后能够追溯这条消息;当其他操作全部完成的时候,事务表中的状态更改为可提交,由MQ通过服务端的回调函数不断检测事务表中该ID的状态确定回滚/提交
- MQ的状态检测减少了逻辑层面代码的编写量,由RocketMQ自身维护消息的事务情况(这里说的不是本地事务);当事务表的状态发生变化后,如果为可提交,说明其他的批操作全部完成,即当前这条消息可以被consumer进行消费;否则,消息应该进行回滚,consumer无法消费。
- 具体的实现为RocketMQ的状态检测,通过回调的方式执行producers内部定义的回调方法;该方法已更改是无状态的,这样即使producer宕机,只要同组producer重启后也可以继续执行这个回调方法。
- 事务表应该记录在数据库,不应该内存中存储。因为使用内存存储那么这些数据将是有状态的,当单机故障的时候数据丢失,无法恢复,后续操作无法执行;因此应该使用RocketMQ中内置的变量保证无状态性。
- 这里说的事务提交/回滚说的是MQ的消息是否可以被consumer可见,本地事务的回滚还是需要的单独维护,如数据库。
代码实现
- 模拟消费者
@Test
public void consumerTransaction() throws Exception {
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("consumer-transaction1");
consumer.setNamesrvAddr("192.168.189.130:9876");
consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_FIRST_OFFSET);
consumer.subscribe("topic_transaction1", "TagA");
// MessageListenerConcurrently 无序
consumer.registerMessageListener(new MessageListenerConcurrently() {
@Override
public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> list, ConsumeConcurrentlyContext consumeConcurrentlyContext) {
list.forEach((msg) -> System.out.println(msg));
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
}
});
consumer.start();
System.in.read();
consumer.shutdown();
}
生产者代码说明
- 模拟生产者,事务的实现主要在生产者
这里要明确消费者组名,这样即使producer宕机,只要同组生产者启动,后续的检查也可以执行内部的回调函数
TransactionMQProducer producer = new TransactionMQProducer("producer-transaction1");
事务监听器,里面提供了RocketMQ的需要的回调方法: executeLocalTransaction, checkLocalTransaction
producer.setTransactionListener(new TransactionListener() {...} )
executeLocalTransaction:处理半状态,即第一阶段提交。参数Message为RocketMQ的响应信息,里面包含了事务id,可以维护到数据库用于之后的状态判定。
参数arg为生产者发送消息的时候携带的额外参数
// 发送消息的时候可以附加额外参数,额外参数的策略:
/*
1. 直接附加在body中,随网络进行传送
2. 使用 msg.putUserProperty(k, v), 不污染请求体,减少负荷
3. 通过sendMessageInTransaction 中的第二个参数args,在回调的时候可以得到
*/
代码的实现逻辑:这里进行初始化消息的事务状态;这里定义为:
消息0为其他批操作未完成,当前状态不可确定,需要MQ检测状态进行回调后续查询事务表确认。
消息1为不可被consumer消费的信息,直接回滚丢弃
消息2为正确放入MQ的信息,等待消费。
@Override
public LocalTransactionState executeLocalTransaction(Message msg, Object arg) {
String action = (String) arg; // 拿到额外参数
// msg是MQ的相应消息,不是producer发送的
String transactionId = msg.getTransactionId();
System.out.println("transactionId:" + transactionId);
// 根据状态做出不同的响应
switch (action) {
case "0": {
System.out.println(Thread.currentThread().getName()+" send half:Async api call...action : 0");
// 状态无法确定,可能前置条件如:数据库存储,服务等其他未完成,MQ对consumer消费不可见
// 需要进行检查
return LocalTransactionState.UNKNOW;
}
case "1": {
System.out.println(Thread.currentThread().getName()+" send half:localTransaction faild...action : 1");
// 事务直接回滚
return LocalTransactionState.ROLLBACK_MESSAGE;
}
case "2": {
System.out.println(Thread.currentThread().getName()+" send half:localTransaction ok...action : 2");
// 事务提交
return LocalTransactionState.COMMIT_MESSAGE;
}
}
return null;
}
checkLocalTransaction:回调检查:宕机或者半提交状态的MQ需要进行检查
RocketMQ对于半提交状态的信息会进行回调查阅事务表获取当前信息的事务状态,确定是否可以提交。
每一次的重试由MQ中的重试等级确定,每一次重试的时间呈递增态。如果超过规定次数还无法确认则直接丢弃。
代码实现逻辑
消息0为重检1000ms后确定可以正常提交,修改状态。
消息1,2需要根据事务表具体确认。这里暂定为无法确认UNKNOW
@Override
public LocalTransactionState checkLocalTransaction(MessageExt msg) {
// 获取额外参数
String action = msg.getProperty("action");
switch (action){
case "0" :
System.out.println(Thread.currentThread().getName()+"check:action :0,UNKNOW: ");
// 重传一定时间后表示检查通过,可以提交
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
return LocalTransactionState.COMMIT_MESSAGE;
case "1" :
System.out.println(Thread.currentThread().getName()+"check:action :1 ROLLBACK");
//观察transactinID表,本地事务失败,状态无法确定,继续检查
return LocalTransactionState.UNKNOW;
case "2" :
System.out.println(Thread.currentThread().getName()+"check:action :2 COMMIT");
////观察transactinID表,本地事务失败,状态无法确定,继续检查
return LocalTransactionState.UNKNOW;
}
return null;
}
});
自定义线程处理,可以不配置;当回调函数的时候执行的是这里创建的,可以查看实验结果;在producer.start()的时候内部也会默认创建线程工厂。
// 回调函数需要线程处理,可以自定义一个线程池;当start()的时候也会默认创建一个,因此可以不写
producer.setExecutorService(new ThreadPoolExecutor(
1,
Runtime.getRuntime().availableProcessors(), // 可获得的最大处理器个数
2000,
TimeUnit.SECONDS,
new LinkedBlockingQueue<Runnable>(2000), // 阻塞队列
r -> new Thread(r, "transaction thread") // 线程工厂
));
producer.start();
完整代码
@Test
public void producerTransaction() throws Exception {
TransactionMQProducer producer = new TransactionMQProducer("producer-transaction1");
producer.setNamesrvAddr("192.168.189.130:9876");
producer.setTransactionListener(new TransactionListener() {
// 处理半消息状态; 在第一阶段提交完成的时候会触发这个回调函数
@Override
public LocalTransactionState executeLocalTransaction(Message msg, Object arg) {
String action = (String) arg; // 拿到额外参数
// msg是MQ的相应消息,不是producer发送的
String transactionId = msg.getTransactionId();
System.out.println("transactionId:" + transactionId);
// 根据状态做出不同的响应
switch (action) {
case "0": {
System.out.println(Thread.currentThread().getName()+" send half:Async api call...action : 0");
// 状态无法确定,可能前置条件如:数据库存储,服务等其他未完成,MQ对consumer消费不可见
// 需要进行检查
return LocalTransactionState.UNKNOW;
}
case "1": {
System.out.println(Thread.currentThread().getName()+" send half:localTransaction faild...action : 1");
// 事务直接回滚
return LocalTransactionState.ROLLBACK_MESSAGE;
}
case "2": {
System.out.println(Thread.currentThread().getName()+" send half:localTransaction ok...action : 2");
// 事务提交
return LocalTransactionState.COMMIT_MESSAGE;
}
}
return null;
}
// 回调检查:宕机或者半提交状态的MQ需要进行检查
@Override
public LocalTransactionState checkLocalTransaction(MessageExt msg) {
String action = msg.getProperty("action");
switch (action){
case "0" :
System.out.println(Thread.currentThread().getName()+"check:action :0,UNKNOW: ");
// 重传一定时间后表示检查通过,可以提交
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
return LocalTransactionState.COMMIT_MESSAGE;
case "1" :
System.out.println(Thread.currentThread().getName()+"check:action :1 ROLLBACK");
//观察transactinID表,本地事务失败,状态无法确定,继续检查
return LocalTransactionState.UNKNOW;
case "2" :
System.out.println(Thread.currentThread().getName()+"check:action :2 COMMIT");
////观察transactinID表,本地事务失败,状态无法确定,继续检查
return LocalTransactionState.UNKNOW;
}
return null;
}
});
// 回调函数需要线程处理,可以自定义一个线程池;当start()的时候也会默认创建一个,因此可以不写
producer.setExecutorService(new ThreadPoolExecutor(
1,
Runtime.getRuntime().availableProcessors(), // 可获得的最大处理器个数
2000,
TimeUnit.SECONDS,
new LinkedBlockingQueue<Runnable>(2000), // 阻塞队列
r -> new Thread(r, "transaction thread") // 线程工厂
));
producer.start();
// 生产消息
for (int i = 0; i < 10; i++) {
Message msg = new Message("topic_transaction1",
"TagA",
"key" + i,
("message: " + i).getBytes());
// 发送消息的时候可以附加额外参数:
/*
1. 直接附加在body中,随网络进行传送
2. 使用 msg.putUserProperty(k, v), 不污染请求体,减少负荷
3. 通过sendMessageInTransaction 中的第二个参数args,在回调的时候可以得到
*/
msg.putUserProperty("action", i % 3 + "");
TransactionSendResult res = producer.sendMessageInTransaction(msg, i % 3 + "");
System.out.println(res);
}
System.in.read();
}
运行结果
生产者:

消费者:

- 观察结果:消费者中的额外参数action只有0和2, 2为正确接收的消息,0为重检后的正确发送的消息,不存在消息1,结果正确。
- 另一个实验结果:当producer宕机,只要重新启动(producer-group不变),那么在到达重检时间后MQ可以重新调用回调函数 checkLocalTransaction,然后处理状态。
官网文档说明
- 事务消息不支持延时消息和批量消息。
- 为了避免单个消息被检查太多次而导致半队列消息累积,我们默认将单个消息的检查次数限制为 15 次,但是用户可以通过 Broker 配置文件的
transactionCheckMax参数来修改此限制。如果已经检查某条消息超过 N 次的话( N =transactionCheckMax) 则 Broker 将丢弃此消息,并在默认情况下同时打印错误日志。用户可以通过重写AbstractTransactionalMessageCheckListener类来修改这个行为。 - 事务消息将在 Broker 配置文件中的参数 transactionTimeout 这样的特定时间长度之后被检查。当发送事务消息时,用户还可以通过设置用户属性 CHECK_IMMUNITY_TIME_IN_SECONDS 来改变这个限制,该参数优先于
transactionTimeout参数。 - 事务性消息可能不止一次被检查或消费。
- 提交给用户的目标主题消息可能会失败,目前这依日志的记录而定。它的高可用性通过 RocketMQ 本身的高可用性机制来保证,如果希望确保事务消息不丢失、并且事务完整性得到保证,建议使用同步的双重写入机制。
- 事务消息的生产者 ID 不能与其他类型消息的生产者 ID 共享。与其他类型的消息不同,事务消息允许反向查询、MQ服务器能通过它们的生产者 ID 查询到消费者。

RocketMQ深度解析:消息队列原理与实践




1059

被折叠的 条评论
为什么被折叠?



