前言
- RocketMQ 是阿里云基于 Apache RocketMQ 构建的低延迟、高并发、高可用、高可靠的分布式消息中间件。最初由阿里巴巴自研并捐赠给 Apache 基金会,服务阿里集团13年,覆盖全集团所有业务。作为双十一交易核心链路的官方指定产品,支撑千万级并发、万亿级数据洪峰
- 早期的消息队列产品有ActiveMQ(由java语言编写,并发量比较低)、RabbitMQ(由elang语言开发,社区活跃度比较低,版本更新比较慢)、Kafka(由Scala语言开发,该语言编译后也可以运行在JVM上,单机的并发量能达到百万级别)、RocketMQ(早期的1.0版本是由阿里系对Kafka的复制,后期不断优化,单机的并发量能达到100万)
- 在多主题、多队列的情况下,Kafka的并发量会出现量级别的下降,因为Kafka会为每个主题下的每个队列创建单文件的形式存储数据,在高并发的情况下,会出现大量的IO瓶颈,而RocketMQ则是采用单文件分片的形式存储数据,在高并发的情况下只有一个commitlog文件处于写状态,其它分片则只是提供读,所以在多主题、多队列的情况下RocketMQ的表现会更好
产品架构
Name Server
- 一个几乎无状态节点,可集群部署,在消息队列中提供命名服务,更新和发现Broker服务
Broker
- 负责存储消息,转发消息。分为Master Broker和Slave Broker,一个Master Broker可以对应多个Slave Broker,但是一个Slave Broker只能对应一个Master Broker。Broker启动后需要完成一次将自己注册至Name Server的操作,随后每隔30s定期向Name Server上报Topic路由信息
生产者(Producer)
- 也称为消息发布者,负责生产并发送消息至Topic
- 与Name Server集群中的其中一个节点(随机)建立长连接(Keep-alive),定期从Name Server读取Topic路由信息,并向提供Topic服务的Master Broker建立长连接,且定时向Master Broker发送心跳
消费者(Consumer)
- 也称为消息订阅者,负责从Topic接收并消费消息
- 与Name Server集群中的其中一个节点(随机)建立长连接,定期从Name Server拉取Topic路由信息,并向提供Topic服务的Master Broker、Slave Broker建立长连接,且定时向Master Broker、Slave Broker发送心跳。Consumer既可以从Master Broker订阅消息,也可以从Slave Broker订阅消息,订阅规则由Broker配置决定
基本概念
Message
- 消息队列中信息传递的载体。
- Message ID(消息的全局唯一标识,由消息队列自动生成,唯一标识某条消息)
- Message Key(消息的业务标识,由消息生产者(Producer)设置,唯一标识某个业务逻辑)
Topic
- 消息主题,一级消息类型(用于标识同一类业务逻辑的消息),生产者向其发送消息
Tag
- 消息标签,二级消息类型,用来进一步区分某个Topic下的消息分类。消费者可通过Tag对消息进行过滤
Group
- 一类生产者或消费者,这类生产者或消费者通常生产或消费同一类消息,且消息发布或订阅的逻辑一致
应用场景
场景1:异步解耦
- 采用同步的方式注册全程需要耗时(50 + 50 + 50)ms,而通过消息队列异步解耦后,全程耗时只要50ms,至于邮件和短信则异步的去执行并发送给用户即可
场景2:削峰填谷
- 在秒杀或团队抢购活动中,由于用户请求量暴增,导致应用无法承载,甚至会导致系统崩溃等问题而发生漏通知的情况。为解决这些问题,可在应用和下游通知系统之间加入消息队列:
1、用户发起海量秒杀请求到秒杀业务处理系统
2、秒杀处理系统按照秒杀处理逻辑将满足秒杀条件的请求发送至消息队列
3、下游的通知系统订阅消息队列的秒杀消息,再将秒杀成功的消息发送到相应用户
4、用户收到秒杀成功的通知
3、顺序消息
- 顺序消息分为两种情况:
1、全局顺序:对于指定的一个Topic,所有消息将按照严格的先入先出(FIFO)的顺序,进行顺序发布和顺序消费
2、分区顺序:对于指定的一个Topic,所有消息根据Sharding Key进行区块分区,同一个分区内的消息将按照严格的FIFO的顺序,进行顺序发布和顺序消费,可以保证一个消息被一个进程消费
4、分布式事务的数据一致性
- 普通消息
1、发起注册
1.1 注册失败,流程结束
1.2 注册成功,发送消息给RocketMQ消息队列
1.2.1 发送消息失败,流程结束,导致最终数据状态不一致【1】
1.2.2 发送消息成功,触发邮件系统发送邮件
1.2.2.1 邮件发送失败,流程结束,导致最终数据状态不一致【2】
1.2.2.2 邮件发送成功,流程结束,数据状态最终一致性
- 事物消息
1、发送半事物消息给RocketMQ队列
1.1 发送失败,流程结束
1.2 发送成功,发起用户注册
1.2.1 用户注册失败,回滚半事物消息,流程结束
1.2.2 用户注册成功,提交半事物消息,触发邮件系统发送邮件
1.2.2.1 邮件发送失败,流程结束,导致最终数据状态不一致【1】【需要人为干预进行处理】
1.2.2.2 邮件发送成功,流程结束,数据状态最终一致性
消费模式
1、RocketMQ是基于发布和订阅模式的消息系统。消费者应用一般是分布式系统,以集群方式部署。消费者在订阅Topic时,可根据实际业务选择集群消费或广播消费,集群中的多个消费者将按照实际选择的消费模式消费消息
2、使用相同Group ID的消费者属于同一个集群。同一个集群下的消费者消费逻辑必须完全一致(包括Tag的使用)
1、RocketMQ消息收发过程中,若Consumer消费某条消息失败,则消息队列会在重试间隔时间后,将消息重新投递给Consumer消费,若达到最大重试次数后消息还没有成功被消费,则消息将被投递至死信队列
2、一条消息无论重试多少次,这些重试消息的Message ID都不会改变
3、消息重试只针对集群消费模式生效;广播消费模式不提供失败重试特性,即消费失败后,失败消息不再重试,继续消费新的消息
1、集群消费
- 适用于消费端集群化部署,每条消息只需要被处理一次的场景
2、广播消费
- 适用于消费端集群化部署,每条消息需要被集群下的每个消费者处理的场景
消息类型
1、普通消息
- 消息队列中无特性的消息,区别于有特性的定时和延时消息、顺序消息和事务消息
- 注意:消息队列提供的四种消息类型所对应的Topic不能混用,例如您创建的普通消息的Topic只能用于收发普通消息,不能用于收发其他类型的消息;同理,事务消息的Topic也只能收发事务消息,不能用于收发其他类型的消息
package org.apache.rocketmq.example.quickstart;
import org.apache.rocketmq.client.exception.MQBrokerException;
import org.apache.rocketmq.client.exception.MQClientException;
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;
import org.apache.rocketmq.remoting.common.RemotingHelper;
import org.apache.rocketmq.remoting.exception.RemotingException;
import java.io.UnsupportedEncodingException;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
public class Producer {
public static void main(String[] args) throws MQClientException, InterruptedException, RemotingException, MQBrokerException, UnsupportedEncodingException {
/**
* 发送消息(不关心发送结果)
*/
// DefaultMQProducer onewayProducer = new DefaultMQProducer("ProductGroup");
// onewayProducer.setNamesrvAddr("127.0.0.1:9876");
// onewayProducer.start();
// Message onewayMsg = new Message(
// "onewayMsg-topic",
// "onewayMsg-tag",
// ("onewayMsg-message").getBytes(RemotingHelper.DEFAULT_CHARSET)
// );
// onewayProducer.sendOneway(onewayMsg);
// onewayProducer.shutdown();
/**
* 发送消息(同步)
*/
// DefaultMQProducer synMsgProducer = new DefaultMQProducer("ProductGroup");
// synMsgProducer.setNamesrvAddr("127.0.0.1:9876");
// synMsgProducer.start();
// Message synMsg = new Message(
// "synMsg-topic",
// "synMsg-tag",
// ("synMsg-message").getBytes(RemotingHelper.DEFAULT_CHARSET)
// );
// SendResult sendResult = synMsgProducer.send(synMsg);
// synMsgProducer.shutdown();
/**
* 异步消息
*/
DefaultMQProducer aSynMsgProducer = new DefaultMQProducer("ProductGroup");
aSynMsgProducer.setNamesrvAddr("127.0.0.1:9876");
aSynMsgProducer.start();
// 异步发送流量过大时是否阻止消息
aSynMsgProducer.setEnableBackpressureForAsyncMode(true);
// 异步模式下,失败重试次数
aSynMsgProducer.setRetryTimesWhenSendAsyncFailed(0);
int messageCount = 1;
final CountDownLatch countDownLatch = new CountDownLatch(messageCount);
try {
Message aSynMsg = new Message(
"aSynMsg-topic",
"aSynMsg-tag",
"aSynMsg-message".getBytes(RemotingHelper.DEFAULT_CHARSET));
aSynMsgProducer.send(aSynMsg, new SendCallback() {
@Override
public void onSuccess(SendResult sendResult) {
countDownLatch.countDown();
System.out.printf("%-10d OK %s %n", 0, sendResult.getMsgId());
}
@Override
public void onException(Throwable e) {
countDownLatch.countDown();
System.out.printf("%-10d Exception %s %n", 0, e);
e.printStackTrace();
}
});
} catch (Exception e) {
e.printStackTrace();
}
countDownLatch.await(5, TimeUnit.SECONDS);
aSynMsgProducer.shutdown();
}
}
package org.apache.rocketmq.example.quickstart;
import org.apache.rocketmq.client.consumer.DefaultMQPushConsumer;
import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyStatus;
import org.apache.rocketmq.client.consumer.listener.MessageListenerConcurrently;
import org.apache.rocketmq.client.exception.MQClientException;
import org.apache.rocketmq.common.consumer.ConsumeFromWhere;
public class Consumer {
public static void main(String[] args) throws MQClientException {
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("ConsumerGroup");
consumer.setNamesrvAddr("127.0.0.1:9876");
// Specify where to start in case the specific consumer group is a brand-new one.
consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_FIRST_OFFSET);
// Subscribe one more topic to consume.
consumer.subscribe("aSynMsg-topic", "aSynMsg-tag");
// Register callback to execute on arrival of messages fetched from brokers.
consumer.registerMessageListener((MessageListenerConcurrently) (msg, context) -> {
System.out.printf("%s Receive New Messages: %s %n", Thread.currentThread().getName(), msg);
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
});
consumer.start();
}
}
2、顺序消息
- 默认情况下,消息的发送会采取(Round Robin)轮询方式将消息发送到不同的queue(分区队列)中,消费时从多个queue上拉取,这种情况发送和消费是随机的、没有顺序
- 如果能控制发送的消息只依次发送到同一个queue中,消费的时候只从固定的queue上依次拉取,那么就保证消息的有序性
- 当参与发送和消费的queue只有一个时,则为【全局有序消息】,但并发性能低
- 当参与发送和消费的queue有多个时,则为【分区有序消息】,即相对每个单独的queue,消息都是有序的,并发性能高
package org.apache.rocketmq.example.ordermessage;
import org.apache.rocketmq.client.exception.MQBrokerException;
import org.apache.rocketmq.client.exception.MQClientException;
import org.apache.rocketmq.client.producer.DefaultMQProducer;
import org.apache.rocketmq.client.producer.MessageQueueSelector;
import org.apache.rocketmq.client.producer.SendResult;
import org.apache.rocketmq.common.message.Message;
import org.apache.rocketmq.common.message.MessageQueue;
import org.apache.rocketmq.remoting.common.RemotingHelper;
import org.apache.rocketmq.remoting.exception.RemotingException;
import java.io.UnsupportedEncodingException;
import java.util.List;
public class Producer {
public static void main(String[] args) throws UnsupportedEncodingException {
try {
DefaultMQProducer producer = new DefaultMQProducer("ProductGroup");
producer.setNamesrvAddr("127.0.0.1:9876");
producer.start();
String[] tags = new String[] {"TagA", "TagB", "TagC"};
for (int i = 0; i < 10; i++) {
int orderId = i % 3;
// i 0 1 2 3 4 5 6 7 8 9
// orderId 0 1 2 0 1 2 0 1 2 0
// 0、3、6、9 和 1、4、7 和 2、5、8 分别存放到同一队列
Message msg = new Message(
"orderMsg-topic",
tags[i % tags.length],
"orderMsg-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);
}
producer.shutdown();
} catch (MQClientException | RemotingException | MQBrokerException | InterruptedException e) {
e.printStackTrace();
}
}
}
package org.apache.rocketmq.example.ordermessage;
import java.nio.charset.StandardCharsets;
import java.text.MessageFormat;
import java.util.List;
import org.apache.rocketmq.client.consumer.DefaultMQPushConsumer;
import org.apache.rocketmq.client.consumer.listener.ConsumeOrderlyContext;
import org.apache.rocketmq.client.consumer.listener.ConsumeOrderlyStatus;
import org.apache.rocketmq.client.consumer.listener.MessageListenerOrderly;
import org.apache.rocketmq.client.exception.MQClientException;
import org.apache.rocketmq.common.consumer.ConsumeFromWhere;
import org.apache.rocketmq.common.message.MessageExt;
public class Consumer {
public static void main(String[] args) throws MQClientException {
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("ConsumerGroup");
consumer.setNamesrvAddr("127.0.0.1:9876");
// 设置Consumer第一次启动是从队列头部开始消费还是队列尾部开始消费
// 如果非第一次启动,那么按照上次消费的位置继续消费
consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_FIRST_OFFSET);
consumer.subscribe("orderMsg-topic", "TagA || TagB || TagC");
consumer.registerMessageListener(new MessageListenerOrderly() {
@Override
public ConsumeOrderlyStatus consumeMessage(List<MessageExt> msgs, ConsumeOrderlyContext context) {
context.setAutoCommit(true);
msgs.forEach(item -> {
System.out.println(MessageFormat.format("{0} {1} {2}",
Thread.currentThread().getName(),
item.getQueueId(),
new String(item.getBody(), StandardCharsets.UTF_8)));
});
// 当顺序消息消费失败时,返回SUSPEND_CURRENT_QUEUE_A_MOMENT状态,表示等一会,再继续处理这批消息,
// 不把这批消息放到重试队列里去处理其他消息,不会导致乱序
//return ConsumeOrderlyStatus.SUSPEND_CURRENT_QUEUE_A_MOMENT;
// 消费成功
return ConsumeOrderlyStatus.SUCCESS;
}
});
consumer.start();
}
}
线程:ConsumeMessageThread_ConsumerGroup_1 队列id:0 消息:Hello RocketMQ 0
线程:ConsumeMessageThread_ConsumerGroup_1 队列id:0 消息:Hello RocketMQ 3
线程:ConsumeMessageThread_ConsumerGroup_1 队列id:0 消息:Hello RocketMQ 6
线程:ConsumeMessageThread_ConsumerGroup_1 队列id:0 消息:Hello RocketMQ 9
线程:ConsumeMessageThread_ConsumerGroup_3 队列id:1 消息:Hello RocketMQ 1
线程:ConsumeMessageThread_ConsumerGroup_3 队列id:1 消息:Hello RocketMQ 4
线程:ConsumeMessageThread_ConsumerGroup_3 队列id:1 消息:Hello RocketMQ 7
线程:ConsumeMessageThread_ConsumerGroup_2 队列id:2 消息:Hello RocketMQ 2
线程:ConsumeMessageThread_ConsumerGroup_2 队列id:2 消息:Hello RocketMQ 5
线程:ConsumeMessageThread_ConsumerGroup_2 队列id:2 消息:Hello RocketMQ 8
结论:每个队列固定由同一个线程来消费,实现了每个队列中的消息有序消费
3、【定时消息】和【延时消息】
-
定时消息:Producer将消息发送到消息队列服务端,但并不期望立马投递这条消息,而是推迟到在当前时间点之后的某一个时间投递到Consumer进行消费,该消息即定时消息
-
延时消息:Producer将消息发送到消息队列服务端,但并不期望立马投递这条消息,而是延迟一定时间后才投递到Consumer进行消费,该消息即延时消息
-
适用场景:
1、消息生产和消费有时间窗口要求,例如在电商交易中超时未支付关闭订单的场景,在订单创建时会发送一条延时消息。这条消息将会在30分钟以后投递给消费者,消费者收到此消息后需要判断对应的订单是否已完成支付。如支付未完成,则关闭订单。如已完成支付则忽略。
2、通过消息触发一些定时任务,例如在某一固定时间点向用户发送提醒消息
/** 默认延时配置类 */
org.apache.rocketmq.store.config.MessageStoreConfig:
private String messageDelayLevel = "1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h";
/** 生产消息 */
package org.apache.rocketmq.example.schedule;
import java.nio.charset.StandardCharsets;
import com.alibaba.fastjson.JSONObject;
import org.apache.rocketmq.client.producer.DefaultMQProducer;
import org.apache.rocketmq.client.producer.SendResult;
import org.apache.rocketmq.common.message.Message;
public class ScheduledMessageProducer {
public static void main(String[] args) throws Exception {
DefaultMQProducer producer = new DefaultMQProducer("ProductGroup");
producer.setNamesrvAddr("127.0.0.1:9876");
producer.start();
Message message = new Message(
"ScheduledMsg-topic",
"ScheduledMsg-tag",
("Hello scheduled message").getBytes(StandardCharsets.UTF_8)
);
// 此消息将在10秒后发送给消费者
message.setDelayTimeLevel(3);
SendResult result = producer.send(message);
System.out.println(JSONObject.toJSON(result));
producer.shutdown();
}
}
/** 消费消息 */
package org.apache.rocketmq.example.schedule;
import org.apache.rocketmq.client.consumer.DefaultMQPushConsumer;
import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyStatus;
import org.apache.rocketmq.client.consumer.listener.MessageListenerConcurrently;
import org.apache.rocketmq.common.message.MessageExt;
import java.text.SimpleDateFormat;
import java.util.Date;
public class ScheduledMessageConsumer {
public static void main(String[] args) throws Exception {
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("ConsumerGroup");
consumer.setNamesrvAddr("127.0.0.1:9876");
consumer.subscribe("ScheduledMsg-topic", "ScheduledMsg-tag");
// Register message listener
consumer.registerMessageListener((MessageListenerConcurrently) (messages, context) -> {
for (MessageExt message : messages) {
System.out.println(new SimpleDateFormat("yyyy年MM月dd日 HH时mm分ss秒").format(new Date()));
}
//失败消耗,稍后尝试消耗
//return ConsumeConcurrentlyStatus.RECONSUME_LATER;
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
});
//info:to see the time effect, run the consumer first , it will wait for the msg then start the producer
consumer.start();
}
}
/** 生产消息 */
package org.apache.rocketmq.example.schedule;
import java.nio.charset.StandardCharsets;
import org.apache.rocketmq.client.producer.DefaultMQProducer;
import org.apache.rocketmq.client.producer.SendResult;
import org.apache.rocketmq.common.message.Message;
public class TimerMessageProducer {
public static void main(String[] args) throws Exception {
DefaultMQProducer producer = new DefaultMQProducer("ProductGroup");
producer.setNamesrvAddr("127.0.0.1:9876");
producer.start();
Message message = new Message(
"TimerMsg-topic",
"TimerMsg-tag",
("Hello scheduled message").getBytes(StandardCharsets.UTF_8));
// 此消息将在5秒后发送给消费者
//message.setDelayTimeSec(5);
// 效果同上
//message.setDelayTimeMs(5_000L);
// 设置具体交货时间,效果同上
message.setDeliverTimeMs(System.currentTimeMillis() + 5_000L);
SendResult result = producer.send(message);
System.out.printf(result + "\n");
// Shutdown producer after use.
producer.shutdown();
}
}
/** 消费消息 */
package org.apache.rocketmq.example.schedule;
import org.apache.rocketmq.client.consumer.DefaultMQPushConsumer;
import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyStatus;
import org.apache.rocketmq.client.consumer.listener.MessageListenerConcurrently;
import org.apache.rocketmq.common.message.MessageExt;
import java.text.SimpleDateFormat;
import java.util.Date;
public class TimerMessageConsumer {
public static void main(String[] args) throws Exception {
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("ConsumerGroup");
consumer.setNamesrvAddr("127.0.0.1:9876");
// Subscribe topics
consumer.subscribe("TimerMsg-topic", "TimerMsg-tag");
// Register message listener
consumer.registerMessageListener((MessageListenerConcurrently) (messages, context) -> {
for (MessageExt message : messages) {
System.out.println(new SimpleDateFormat("yyyy年MM月dd日 HH时mm分ss秒").format(new Date()));
}
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
});
//info:to see the time effect, run the consumer first , it will wait for the msg then start the producer
consumer.start();
}
}
4、批量消息
package org.apache.rocketmq.example.batch;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import org.apache.rocketmq.client.producer.DefaultMQProducer;
import org.apache.rocketmq.client.producer.SendResult;
import org.apache.rocketmq.common.message.Message;
/**
* 如果每次发送的消息内容不超过1M,那么建议使用批处理
* 批处理的消息应该具有:相同的主题,相同的waitStoreMsgOK,并且不支持计划
* 消息总大小不应超过4MB
*/
public class SimpleBatchProducer {
public static void main(String[] args) throws Exception {
DefaultMQProducer producer = new DefaultMQProducer("BatchProducerGroupName");
producer.setNamesrvAddr("127.0.0.1:9876");
producer.start();
List<Message> messages = new ArrayList<>();
messages.add(new Message("BatchTOPIC", "BatchTAG", "OrderID001", "Hello world 0".getBytes(StandardCharsets.UTF_8)));
messages.add(new Message("BatchTOPIC", "BatchTAG", "OrderID002", "Hello world 1".getBytes(StandardCharsets.UTF_8)));
messages.add(new Message("BatchTOPIC", "BatchTAG", "OrderID003", "Hello world 2".getBytes(StandardCharsets.UTF_8)));
SendResult sendResult = producer.send(messages);
}
}
package org.apache.rocketmq.example.batch;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import org.apache.rocketmq.client.producer.DefaultMQProducer;
import org.apache.rocketmq.client.producer.SendResult;
import org.apache.rocketmq.common.message.Message;
public class SplitBatchProducer {
public static final int MESSAGE_COUNT = 100 * 1000;
public static void main(String[] args) throws Exception {
DefaultMQProducer producer = new DefaultMQProducer("BatchProducerGroupName");
producer.setNamesrvAddr("127.0.0.1:9876");
producer.start();
// 模拟大批量数据
List<Message> messages = new ArrayList<>(MESSAGE_COUNT);
for (int i = 0; i < MESSAGE_COUNT; i++) {
messages.add(new Message("BatchTOPIC", "BatchTAG", "OrderID" + i, ("Hello world " + i).getBytes(StandardCharsets.UTF_8)));
}
// 对数据经常分割
ListSplitter splitter = new ListSplitter(messages);
while (splitter.hasNext()) {
List<Message> listItem = splitter.next();
SendResult sendResult = producer.send(listItem);
}
}
}
/**
* 按照大小对消息进行分割,分割后批量发送
*/
class ListSplitter implements Iterator<List<Message>> {
private static final int SIZE_LIMIT = 1000 * 1000;
private final List<Message> messages;
private int currIndex;
public ListSplitter(List<Message> messages) {
this.messages = messages;
}
@Override
public boolean hasNext() {
return currIndex < messages.size();
}
@Override
public List<Message> next() {
int nextIndex = currIndex;
int totalSize = 0;
for (; nextIndex < messages.size(); nextIndex++) {
Message message = messages.get(nextIndex);
int tmpSize = message.getTopic().length() + message.getBody().length;
Map<String, String> properties = message.getProperties();
for (Map.Entry<String, String> entry : properties.entrySet()) {
tmpSize += entry.getKey().length() + entry.getValue().length();
}
//for log overhead
tmpSize = tmpSize + 20;
if (tmpSize > SIZE_LIMIT) {
//it is unexpected that single message exceeds the sizeLimit
//here just let it go, otherwise it will block the splitting process
if (nextIndex - currIndex == 0) {
//if the next sublist has no element, add this one and then break, otherwise just break
nextIndex++;
}
break;
}
if (tmpSize + totalSize > SIZE_LIMIT) {
break;
} else {
totalSize += tmpSize;
}
}
List<Message> subList = messages.subList(currIndex, nextIndex);
currIndex = nextIndex;
return subList;
}
@Override
public void remove() {
throw new UnsupportedOperationException("Not allowed to remove");
}
}
5、过滤消息
- 标签过滤
- SQL过滤
6、事务消息
-
RocketMQ提供类似XA或Open XA的分布式事务功能,通过消息队列的事务消息能达到分布式事务的最终一致
-
RocketMQ分布式事务消息不仅可以实现应用之间的解耦,又能保证数据的最终一致性。同时传统的大事务可以被拆分为小事务,不仅能提升效率,还不会因为某一个关联应用的不可用导致整体回滚,从而最大限度保证核心系统的可用性
-
在极端情况下,如果关联的某一个应用始终无法处理成功,也只需对当前应用进行补偿或数据订正处理,而无需对整体业务进行回滚
-
在淘宝购物车下单时,涉及到购物车系统和交易系统,这两个系统之间的数据最终一致性可以通过分布式事务消息的异步处理实现。在这种场景下,交易系统是最为核心的系统,需要最大限度地保证下单成功,而购物车系统只需要订阅消息队列的交易订单消息,做相应的业务处理(人为干预),即可保证最终的数据一致性
1、本地订单持久化之前,先发送一个扣减库存的半事务消息给RocketMQ,RocketMQ收到半事务消息后并做出响应
2、本地事务收到成功响应后开始提交订单:
2.1、本地订单事务执行失败,告知RocketMQ进行Rollback
2.2、本地订单事务执行成功,告知RocketMQ进行Commit,RocketMQ对扣减库存的半事务消息进行投递,触发消费者进行消费
2.3、由于网络等其它原因,RocketMQ一直没有收到本地订单事务提交结果通知,则会主动发起本地订单状态回查(需要开发者提供相关接口),根据回查结果决定是否对扣减库存的半事务消息进行投递
3、如果扣减库存的半事务消息执行失败了,则触发重新投递,超过指定次数后进入死性队列进行周期性存储,针对死性队列的数据需要人为干预
package org.apache.rocketmq.example.transaction;
import org.apache.rocketmq.client.exception.MQClientException;
import org.apache.rocketmq.client.producer.SendResult;
import org.apache.rocketmq.client.producer.TransactionListener;
import org.apache.rocketmq.client.producer.TransactionMQProducer;
import org.apache.rocketmq.common.message.Message;
import org.apache.rocketmq.remoting.common.RemotingHelper;
import java.io.UnsupportedEncodingException;
import java.text.MessageFormat;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
public class TransactionProducer {
public static void main(String[] args) throws MQClientException, InterruptedException {
TransactionMQProducer producer = new TransactionMQProducer("TransactionProducerGroupName");
producer.setNamesrvAddr("127.0.0.1:9876");
TransactionListener transactionListener = new TransactionListenerImpl();
ExecutorService executorService = new ThreadPoolExecutor(
1,
5,
100, TimeUnit.SECONDS,
new ArrayBlockingQueue<>(2000), r -> {
Thread thread = new Thread(r);
thread.setName("TransactionProducer-thread");
return thread;
});
producer.setExecutorService(executorService);
producer.setTransactionListener(transactionListener);
producer.start();
try {
Message msg = new Message(
"TransactionTopic",
"TransactionTag",
"TransactionKey",
("Hello RocketMQ").getBytes(RemotingHelper.DEFAULT_CHARSET));
/** 第一步:发送事务消息 */
SendResult sendResult = producer.sendMessageInTransaction(msg, null);
System.out.println(MessageFormat.format("{0} 发送事务消息成功", new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date())));
} catch (MQClientException | UnsupportedEncodingException e) {
e.printStackTrace();
}
for (int i = 0; i < 100000; i++) {
// 这里通过休眠,让生产者保持start状态,测试使用
Thread.sleep(1000);
}
producer.shutdown();
}
}
package org.apache.rocketmq.example.transaction;
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.text.MessageFormat;
import java.text.SimpleDateFormat;
import java.util.Date;
public class TransactionListenerImpl implements TransactionListener {
/**
* 第二步:执行本地事务并返回相应的状态
*/
@Override
public LocalTransactionState executeLocalTransaction(Message msg, Object arg) {
System.out.println(MessageFormat.format("{0} 执行本地业务逻辑", new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date())));
//return LocalTransactionState.COMMIT_MESSAGE; // RocketMQ服务器提交消息,触发投递
//return LocalTransactionState.ROLLBACK_MESSAGE; // RocketMQ服务器回滚消息
return LocalTransactionState.UNKNOW; // 表示期待下一次回查
}
/**
* 第三步:如果执行本地事务时返回的是UNKNOW,表示期待下一次回查:检查本地事务状态并返回相应的状态
* TODO 如果触发回查,则会出现重复投递,下游需要做好幂等处理
*/
@Override
public LocalTransactionState checkLocalTransaction(MessageExt msg) {
System.out.println(MessageFormat.format("{0} 检查本地事务状态", new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date())));
return LocalTransactionState.COMMIT_MESSAGE; // 回查时成功,RocketMQ服务器提交消息,触发投递
//return LocalTransactionState.ROLLBACK_MESSAGE; // 回查时失败,RocketMQ服务器回滚消息
//return LocalTransactionState.UNKNOW; // 回查时未知,表示期待下一次会查,RocketMQ允许设置消息的回查间隔与回查次数,如果在超过回查次数后依然无法获知消息的事务状态,则回滚消息
}
}
消息的生产与消费
- 消费者获取消息的模式有两种:推模式【PUSH】和拉模式【PULL】,对应的类分别是DefaultMQPushConsumer和DefaultMQPullConsumer**(4.9.0版本中DefaultMQPullConsumer已经被废弃了)**
Pull模式下,由消费者主动从Broker拉取消息,主动权在消费者,消费者可以根据自己的消费能力拉取适量的消息
Push模式下,由Broker接收到消息后主动推送给消费者,实时性较高,但是会增加Broker的压力,如果消费者处理能力跟不上也会被打垮
- 实际上Push模式也是通过Pull的方式实现的,消息统一由消费者主动拉取。
- Consumer和Broker会建立长连接,一旦分配到MessageQueue,就会立马构建PullRequest去拉取消息,在不触发流控的情况下,不管有没有拉取到新的消息,Consumer都会立即再次拉取,这样保证了消息消费的实时性
- Consumer在拉取消息时,会携带参数suspendTimeoutMillis(表示Broker在没有新的消息时,阻塞等待的时间,默认是15秒)。如果没有消息Broker等待15秒再返回结果,避免客户端频繁拉取。如果15秒内有新的消息了,立马返回,保证消息消费的时效性
默认API发送与消费
生产消息(默认集群模式)
详见消息类型
生产消息(广播模式)
package org.apache.rocketmq.example.broadcast;
import org.apache.rocketmq.client.consumer.DefaultMQPushConsumer;
import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyStatus;
import org.apache.rocketmq.client.consumer.listener.MessageListenerConcurrently;
import org.apache.rocketmq.client.exception.MQClientException;
import org.apache.rocketmq.common.consumer.ConsumeFromWhere;
import org.apache.rocketmq.common.protocol.heartbeat.MessageModel;
public class PushConsumer {
public static final String CONSUMER_GROUP = "please_rename_unique_group_name_1";
public static final String DEFAULT_NAMESRVADDR = "127.0.0.1:9876";
public static final String TOPIC = "TopicTest";
public static final String SUB_EXPRESSION = "TagA || TagC || TagD";
public static void main(String[] args) throws InterruptedException, MQClientException {
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer(CONSUMER_GROUP);
consumer.setNamesrvAddr(DEFAULT_NAMESRVADDR);
consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_FIRST_OFFSET);
// 广播模式生产消息
consumer.setMessageModel(MessageModel.BROADCASTING);
consumer.subscribe(TOPIC, SUB_EXPRESSION);
consumer.registerMessageListener((MessageListenerConcurrently) (msgs, context) -> {
System.out.printf("%s Receive New Messages: %s %n", Thread.currentThread().getName(), msgs);
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
});
consumer.start();
System.out.printf("Broadcast Consumer Started.%n");
}
}
主动拉取式消费(自动提交)
package org.apache.rocketmq.example.simple;
import java.nio.charset.StandardCharsets;
import java.util.List;
import org.apache.rocketmq.client.consumer.DefaultLitePullConsumer;
import org.apache.rocketmq.common.consumer.ConsumeFromWhere;
import org.apache.rocketmq.common.message.MessageExt;
public class LitePullConsumerSubscribe {
public static volatile boolean running = true;
public static void main(String[] args) throws Exception {
DefaultLitePullConsumer litePullConsumer = new DefaultLitePullConsumer("LitePullConsumerGroupName");
litePullConsumer.setNamesrvAddr("127.0.0.1:9876");
/**
* CONSUME_FROM_LAST_OFFSET: 默认策略,初次从该队列最尾开始消费,即跳过历史消息,后续再启动接着上次消费的进度开始消费
* CONSUME_FROM_FIRST_OFFSET: 初次从消息队列头部开始消费,即历史消息(还存在broker的),全部消费一遍,后续再启动接着上次消费的进度开始消费
* CONSUME_FROM_TIMESTAMP: 从某个时间点开始消费,默认是半个小时以前,后续再启动着上次消费的进度开始消费
*/
litePullConsumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_FIRST_OFFSET);
// 消费者去broker拉取消息时,每次提取的最大消息数量
litePullConsumer.setPullBatchSize(3);
// 拉取消息后,自动提交偏移的标志(默认)
litePullConsumer.setAutoCommit(true);
litePullConsumer.subscribe("LitePullConsumerTopic", "LitePullConsumerTag");
litePullConsumer.start();
try {
while (running) {
List<MessageExt> messageExts = litePullConsumer.poll();
messageExts.forEach(messageExt -> {
System.out.println(new String(messageExt.getBody(), StandardCharsets.UTF_8));
});
System.out.println("");
}
} finally {
litePullConsumer.shutdown();
}
}
}
整合SpringBoot发送与消费
POM依赖
<dependency>
<groupId>org.apache.rocketmq</groupId>
<artifactId>rocketmq-spring-boot-starter</artifactId>
<version>2.2.0</version>
</dependency>
<dependency>
<groupId>org.apache.rocketmq</groupId>
<artifactId>rocketmq-spring-boot</artifactId>
<version>2.2.0</version>
</dependency>
YAML配置
rocketmq:
# 指定NameServer (生产者和消费者都要配置)
name-server: 192.168.10.12:9876
# 指定配置生产者配置(生产者配置)
producer:
# 组名(由项目名代替)
group: jherp-task-warehouse
# 发送消息超时时间,单位毫秒
send-message-timeout: 10000
# 消息Body超过多大开始压缩(Consumer收到消息会自动解压缩),单位字节
compress-message-body-threshold: 4096
# 客户端限制的消息大小,超过报错,同时服务端也会限制(默认128k)
max-message-size: 131072
生产消息
package com.cocam.warehouse.rocketmq.service;
import org.apache.rocketmq.client.producer.SendResult;
import org.apache.rocketmq.client.producer.TransactionSendResult;
/**
* @description: RocketMq消息服务
* @author: lixing
* @time: 2021/6/8 21:04
*/
public interface RocketMqService<T> {
/**
* @描述 发送普通消息
* @作者 lixing
* @日期 2021/6/8 21:07
* @Param [topic, messageBody]
*/
void sendSimpleMsg(String topic, T messageBody);
/**
* @描述 发送【同步】【无序】消息
* @作者 lixing
* @日期 2021/6/8 21:11
* @Param [topic, messageBody, timeout]
*/
SendResult sendSyncMsg(String topic, T messageBody, long timeout, int delayLevel);
/**
* @描述 发送【同步】【有序】消息
* @作者 lixing
* @日期 2021/6/8 21:11
* @Param [topic, messageBody, timeout]
*/
SendResult sendSyncMsg(String topic, T messageBody, String hashKey, long timeout);
/**
* @描述 发送【异步】【无序】消息
* @作者 lixing
* @日期 2021/6/8 21:19
* @Param [topic, messageBody, sendCallback, timeout, delayLevel]
*/
void sendAsyncMsg(String topic, T messageBody, long timeout, int delayLevel);
/**
* @描述 发送【异步】【有序】消息
* @作者 lixing
* @日期 2021/6/8 21:19
* @Param [topic, messageBody, sendCallback, timeout, delayLevel]
*/
void sendAsyncMsgOrderly(String topic, T messageBody, String hashKey, long timeout);
/**
* @描述 发送事务消息
* @作者 lixing
* @日期 2021/6/8 21:24
* @Param [topic, tag, msg]
* @return TransactionSendResult
*/
TransactionSendResult sendMessageIntransaction(String topic, String tag, String msg);
}
package com.cocam.warehouse.rocketmq.service.impl;
import com.cocam.warehouse.rocketmq.service.RocketMqService;
import org.apache.rocketmq.client.producer.SendCallback;
import org.apache.rocketmq.client.producer.SendResult;
import org.apache.rocketmq.client.producer.TransactionSendResult;
import org.apache.rocketmq.spring.core.RocketMQTemplate;
import org.apache.rocketmq.spring.support.RocketMQHeaders;
import org.springframework.messaging.Message;
import org.springframework.messaging.support.MessageBuilder;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.util.UUID;
/**
* @描述: RocketMq消息服务 TODO 在发送消息的时候,我们只要把tags使用":"添加到topic后面就可以了
* @作者: lixing
* @日期 2021/6/8 21:07
*/
@Service
public class RocketMqServiceImpl<T> implements RocketMqService<T> {
@Resource
private RocketMQTemplate rocketMQTemplate;
/**
* @描述 发送普通消息
* @作者 lixing
* @日期 2021/6/8 21:07
* @Param [topic, messageBody]
*/
@Override
public void sendSimpleMsg(String topic, T messageBody) {
rocketMQTemplate.convertAndSend(topic, messageBody);
}
/**
* @描述 发送【同步】【无序】消息
* @作者 lixing
* @日期 2021/6/8 21:11
* @Param [topic, messageBody, hashKey, timeout]
*/
@Override
public SendResult sendSyncMsg(String topic, T messageBody, long timeout, int delayLevel) {
SendResult sendResult = rocketMQTemplate.syncSend(topic, MessageBuilder.withPayload(messageBody).build(), timeout, delayLevel);
return sendResult;
}
/**
* @描述 发送【同步】【有序】消息
* @作者 lixing
* @日期 2021/6/8 21:11
* @Param [topic, messageBody, timeout]
*/
@Override
public SendResult sendSyncMsg(String topic, T messageBody, String hashKey, long timeout) {
// 第三个参数是hashKey,会根据他的hash值计算发送到哪一个队列,我用的是同一个值,那么他们的hash一样就可以保证发送到同一个队列里
SendResult sendResult = rocketMQTemplate.syncSendOrderly(topic, messageBody, hashKey, timeout);
return sendResult;
}
/**
* @描述 发送【异步】【无序】消息
* @作者 lixing
* @日期 2021/6/8 21:19
* @Param [topic, messageBody, sendCallback, timeout, delayLevel]
*/
@Override
public void sendAsyncMsg(String topic, T messageBody, long timeout, int delayLevel) {
rocketMQTemplate.asyncSend(topic, MessageBuilder.withPayload(messageBody).build(),
new SendCallback() {
@Override
public void onSuccess(SendResult sendResult) { }
@Override
public void onException(Throwable throwable) { }
}, timeout, delayLevel
);
}
/**
* @描述 发送【异步】【有序】消息
* @作者 lixing
* @日期 2021/6/8 21:19
* @Param [topic, messageBody, sendCallback, timeout, delayLevel]
*/
@Override
public void sendAsyncMsgOrderly(String topic, T messageBody, String hashKey, long timeout) {
// 第三个参数是hashKey,会根据他的hash值计算发送到哪一个队列,我用的是同一个值,那么他们的hash一样就可以保证发送到同一个队列里
rocketMQTemplate.asyncSendOrderly(topic, messageBody, hashKey,
new SendCallback() {
@Override
public void onSuccess(SendResult sendResult) { }
@Override
public void onException(Throwable throwable) { }
}, timeout);
}
/**
* @描述 发送事务消息
* @作者 lixing
* @日期 2021/6/8 21:24
* @Param [topic, tag, msg]
* @return TransactionSendResult
*/
@Override
public TransactionSendResult sendMessageIntransaction(String topic, String tag, String msg) {
Message<String> message = MessageBuilder.withPayload(msg)
.setHeader(RocketMQHeaders.TRANSACTION_ID, UUID.randomUUID().toString())
.setHeader(RocketMQHeaders.TAGS, "tag-lss0555")
.setHeader("userid", "lss05555")
.build();
String destination = topic;
TransactionSendResult sendResult = rocketMQTemplate.sendMessageInTransaction(destination, message, destination);
return sendResult;
}
}
消费消息
@RocketMQMessageListener(
topic = "", // topic:和消费者发送的topic相同
consumerGroup = "", // group:不用和生产者group相同
selectorExpression = "*") // tag
public class MqListner implements RocketMQListener<T> {
@Override
public void onMessage(T t) { }
}
RocketMQ Clients(官方工具)