RocketMq基础入门
http://rocketmq.apache.org/dowloading/releases/
docker 搭建rocketmq
暂略
为什么要用消息中间件?
-
应用解耦
-
流量削峰
-
数据分发
通过消息队列可以让数据在多个系统更加之间进行流通。数据的产生方不需要关心谁来使用数据,只需要将数据发送到消息队列,数据使用方直接在消 息队列中直接获取数据即可-
接口调用的弊端,无论是新增系统,还是移除系统,代码改造工作量都很大
-
使用 MQ 做数据分发好处,无论是新增系统,还是移除系统,代码改造工作量较小。
-
所以使用 MQ 做数据的分发,可以提高团队开发的效率。
新增发布端和消费端都不需要修改另一端
-
RocketMQ 产品发展
RocketMQ 版本发展
Metaq1.x 是 RocketMQ 前身的第一个版本,本质上把 Kafka 做了一次 java 版本的重写(Kafka 是 sacla)
Meta2.x,主要是对存储部分进行了优化,因为 kafka 的数据存储,它的 paration 是一个全量的复制,在阿里、在淘宝的这种海量交易。Kafka 这种 机制的横向拓展是非常不好的。2012 年阿里同时把 Meta2.0 从阿里内部开源出来,取名 RocketMQ,同时为了命名上的规范(版本上延续),所以这个就 是 RocketMQ3.0。
现在 RocketMQ 主要维护的是 4.x 的版本,也是大家使用得最多的版本,2017 年从 Apache 顶级项目毕业
展望未来
从阿里负责 RocketMQ 的架构核心人员的信息来看,阿里内部一直全力拓展 RocketMQ。
2017 年 10 月份,OpenMessaging 项目由阿里巴巴发起,与雅虎、滴滴出行、Streamlio 公司共同参与创立, 项目意在创立厂商无关、平台无关的分布 式消息及流处理领域的应用开发标准。同时 OpenMessaging 入驻 Linux 基金会
OpenMessaging 项目已经开始在 Apache RocketMQ 中率先落地,并推广至整个阿里云平台.
另外 RocketMQ5 的版本也在内部推进,主要的方向是 Cloud Native(云原生)
另外还要讲一下 Apache RocketMQ 的商业版本,Aliware MQ 在微服务、流计算、IoT、异步解耦、数据同步等场景有非常广泛的运用

RocketMQ 的物理架构
消息队列 RocketMQ 是阿里巴巴集团基于高可用分布式集群技术,自主研发的云正式商用的专业消息中间件,既可为分布式应用系统提供异步解耦和削峰填谷的能力,同时也具备互联网应用所需的海量消息堆积、高吞吐、可靠重试等特性,是阿里巴巴双 11 使用的核心产品。
RocketMQ 的设计基于主题的发布与订阅模式,其核心功能包括消息发送、消息存储(Broker)、消息消费,整体设计追求简单与性能第一。
核心概念

NameServer
NameServer 是整个 RocketMQ 的“大脑”,它是 RocketMQ 的服务注册中心,所以 RocketMQ 需要先启动 NameServer 再启动 Rocket 中的 Broker。
Broker 在启动时向所有 NameServer 注册(主要是服务器地址等),生产者在发送消息之前先从 NameServer 获取 Broker 服务器地址列表(消费者一样),然后根据负载均衡算法从列表中选择一台服务器进行消息发送。
NameServer 与每台 Broker 服务保持长连接,并间隔 30S 检查 Broker 是否存活,如果检测到 Broker 宕机,则从路由注册表中将其移除。这样就可以实 现 RocketMQ 的高可用。具体细节后续的课程会进行讲解。
生产者(Producer)
生产者:也称为消息发布者,负责生产并发送消息至 RocketMQ。
消费者(Consumer)
消费者:也称为消息订阅者,负责从 RocketMQ 接收并消费消息。
消息(Message)
消息:生产或消费的数据,对于 RocketMQ 来说,消息就是字节数组。
主机(Broker)
RocketMQ 的核心,用于暂存和传输消息。
物理架构中的整体运转
-
NameServer 先启动
-
Broker 启动时向 NameServer 注册
-
生产者在发送某个主题的消息之前先从 NamerServer 获取 Broker 服务器地址列表(有可能是集群),然后根据负载均衡算法从列表中选择一台 Broker 进行消息发送。
-
NameServer 与每台 Broker 服务器保持长连接,并间隔 30S 检测 Broker 是否存活,如果检测到 Broker 宕机(使用心跳机制,如果检测超过120S),则从路由注册表中将其移除。
-
消费者在订阅某个主题的消息之前从 NamerServer 获取 Broker 服务器地址列表(有可能是集群),但是消费者选择从 Broker 中订阅消息,订阅 规则由 Broker 配置决定。
RocketMQ 的概念模型
核心概念
分组(Group)
**生产者:**标识发送同一类消息的 Producer,通常发送逻辑一致。发送普通消息的时候,仅标识使用,并无特别用处。主要作用用于事务消息: (事务消息中如果某条发送某条消息的producer-A宕机,使得事务消息一直处于PREPARED状态并超时,则broker会回查同一个group的其它producer, 确认这条消息应该 commit 还是 rollback)
**消费者:**标识一类 Consumer 的集合名称,这类 Consumer 通常消费一类消息,且消费逻辑一致。同一个 Consumer Group 下的各个实例将共同消费 topic 的消息,起到负载均衡的作用。 消费进度以 Consumer Group 为粒度管理,不同 Consumer Group 之间消费进度彼此不受影响(复制消息),即消息 A 被 Consumer Group1 消费过,也会再给 Consumer Group2 消费。
主题(Topic)
标识一类消息的逻辑名字,消息的逻辑管理单位。无论消息生产还是消费,都需要指定 Topic。
区分消息的种类;一个发送者可以发送消息给一个或者多个 Topic;一个消息的接收者可以订阅一个或者多个 Topic 消息
标签(Tag)
RocketMQ 支持给在发送的时候给 topic 打 tag,同一个 topic 的消息虽然逻辑管理是一样的。但是消费 topic1 的时候,如果你消费订阅的时候指定的 是 tagA,那么 tagB 的消息将不会投递。
消息队列(Message Queue)
简称 Queue 或 Q。消息物理管理单位。一个 Topic 将有若干个 Q。若一个 Topic 创建在不同的 Broker,则不同的 broker 上都有若干 Q,消息将物理地存储落在不同 Broker 结点上,具有水平扩展的能力。
无论生产者还是消费者,实际的生产和消费都是针对 Q 级别。例如 Producer 发送消息的时候,会预先选择(默认轮询)好该 Topic 下面的某一条 Q发送;Consumer 消费的时候也会负载均衡地分配若干个 Q,只拉取对应 Q 的消息。
每一条 message queue 均对应一个文件,这个文件存储了实际消息的索引信息。并且即使文件被删除,也能通过实际纯粹的消息文件(commit log)恢复回来。
偏移量(Offset)
RocketMQ 中,有很多 offset 的概念。一般我们只关心暴露到客户端的 offset。不指定的话,就是指 Message Queue 下面的 offset。
Message queue 是无限长的数组。一条消息进来下标就会涨 1,而这个数组的下标就是 offset,Message queue 中的 max offset 表示消息的最大 offsetConsumer offset 可以理解为标记 Consumer Group 在一条逻辑 Message Queue 上,消息消费到哪里即消费进度。但从源码上看,这个数值是消费过的最新消费的消息 offset+1,即实际上表示的是下次拉取的 offset 位置。
玩转各种消息
普通消息
整体流程如下
-
导入 MQ 客户端依赖
<dependency> <groupId>org.apache.rocketmq</groupId> <artifactId>rocketmq-client</artifactId> <version>4.8.0</version> </dependency> -
消息发送者步骤
-
创建消息生产者 producer,并指定生产者组名
-
指定 Nameserver 地址
-
启动 producer
-
创建消息对象,指定 Topic、Tag 和消息体
-
发送消息
-
关闭生产者 producer
-
-
消息消费者步骤
-
创建消费者 Consumer,指定消费者组名
-
指定 Nameserver 地址
-
订阅主题 Topic 和 Tag
-
设置回调函数,处理消息
-
启动消费者 consumer
-
消息发送
发送同步消息
这种可靠性同步地发送方式使用的比较广泛,比如:重要的消息通知,短信通知
public class SyncProducer {
public static void main(String[] args) throws Exception{
// 实例化消息生产者Producer
DefaultMQProducer producer = new DefaultMQProducer("please_rename_unique_group_name");
// 设置NameServer的地址
producer.setNamesrvAddr("localhost:9876"); // 修改端口需要修改源码
// 启动Producer实例
producer.start();
for (int i = 0; i < 100; i++) {
// 创建消息,并指定Topic,Tag和消息体
Message msg = new Message("TopicTest" /* Topic */,
"TagA" /* Tag */,
("Hello RocketMQ " + i).getBytes(RemotingHelper.DEFAULT_CHARSET) /* Message body */
);
// 发送消息到一个Broker
SendResult sendResult = producer.send(msg);
// 通过sendResult返回消息是否成功送达
System.out.printf("%s%n", sendResult);
}
// 如果不再发送消息,关闭Producer实例。
producer.shutdown();
}
}
Message ID
消息的全局唯一标识(内部机制的 ID 生成是使用机器 IP 和消息偏移量的组成,所以有可能重复,如果是幂等性还是最好考虑 Key),由消息队列 MQ系统自动生成,唯一标识某条消息。
SendStatus
发送的标识。成功,失败等
Queue
相当于是 Topic 的分区;用于并行发送和接收消息
发送异步消息
异步消息通常用在对响应时间敏感的业务场景,即发送端不能容忍长时间地等待 Broker 的响应。
public class AsyncProducer {
public static void main(String[] args) throws Exception{
// 实例化消息生产者Producer
DefaultMQProducer producer = new DefaultMQProducer("AsyncProducer");
// 设置NameServer的地址
producer.setNamesrvAddr("localhost:9876");
// 启动Producer实例
producer.start();
producer.setRetryTimesWhenSendAsyncFailed(0); // 多了一个重试次数
//启用Broker故障延迟机制
producer.setSendLatencyFaultEnable(true);
for (int i = 0; i < 100; i++) {
final int index = i;
// 创建消息,并指定Topic,Tag和消息体
Message msg = new Message("TopicTest", "TagA", "OrderID888",
"Hello world".getBytes(RemotingHelper.DEFAULT_CHARSET));
// SendCallback接收异步返回结果的回调
producer.send(msg, new SendCallback() { // 提供一个失败回调的方法
@Override
public void onSuccess(SendResult sendResult) {
System.out.printf("%-10d OK %s %n", index, sendResult.getMsgId()); // 不是顺序的编号,印证了是异步的
}
@Override
public void onException(Throwable e) {
System.out.printf("%-10d Exception %s %n", index, e);e.printStackTrace();
}
});
}
Thread.sleep(10000);
// 如果不再发送消息,关闭Producer实例。
producer.shutdown();
}
}
单向发送
这种方式主要用在不特别关心发送结果的场景,例如日志发送。
public class OnewayProducer {
public static void main(String[] args) throws Exception{
// 实例化消息生产者Producer
DefaultMQProducer producer = new DefaultMQProducer("please_rename_unique_group_name");
// 设置NameServer的地址
producer.setNamesrvAddr("localhost:9876");
// 启动Producer实例
producer.start();
for (int i = 0; i < 100; i++) {
// 创建消息,并指定Topic,Tag和消息体
Message msg = new Message("TopicTest" /* Topic */,
"TagA" /* Tag */,
("Hello RocketMQ " + i).getBytes(RemotingHelper.DEFAULT_CHARSET) /* Message body */
);
// 发送单向消息,没有任何返回结果
producer.sendOneway(msg);
}
// 如果不再发送消息,关闭Producer实例。
producer.shutdown();
}
}
单向(Oneway)发送特点为发送方只负责发送消息,不等待服务器回应且没有回调函数触发,即只发送请求不等待应答。此方式发送消息的过程耗
时非常短,一般在微秒级别
消息发送的权衡

消息消费
集群消费
public class BalanceComuser {
public static void main(String[] args) throws Exception {
// 实例化消息生产者,指定组名
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("group1");
// 指定Namesrv地址信息.
consumer.setNamesrvAddr("localhost:9876");
// 订阅Topic
consumer.setMaxReconsumeTimes(1);
consumer.subscribe("TopicTest", "*");
//负载均衡模式消费
consumer.setMessageModel(MessageModel.CLUSTERING); // 设置消费模式
// 注册回调函数,处理消息
consumer.registerMessageListener(new MessageListenerConcurrently() {
@Override
public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs,
ConsumeConcurrentlyContext context) {
System.out.printf("%s Receive New Messages: %s %n",
Thread.currentThread().getName(), msgs);
return ConsumeConcurrentlyStatus.RECONSUME_LATER;
// return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
}
});
//启动消息者
consumer.start();
System.out.printf("Consumer Started.%n");
}
}
广播消费
public class BroadcastComuser {
public static void main(String[] args) throws Exception {
// 实例化消息生产者,指定组名
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("group1");
// 指定Namesrv地址信息.
consumer.setNamesrvAddr("localhost:9876");
// 订阅Topic
consumer.subscribe("TopicTest", "*");
//广播模式消费
consumer.setMessageModel(MessageModel.BROADCASTING);
// 注册回调函数,处理消息
consumer.registerMessageListener(new MessageListenerConcurrently() {
@Override
public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs,
ConsumeConcurrentlyContext context) {
System.out.printf("%s Receive New Messages: %s %n",
Thread.currentThread().getName(), msgs);
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
}
});
//启动消息者
consumer.start();
System.out.printf("Consumer Started.%n");
}
}
消息消费时的权衡
集群模式:适用场景&注意事项
集群消费模式下,每一条消息都只会被分发到一台机器上处理。如果需要被集群下的每一台机器都处理,请使用广播模式。
集群消费模式下,不保证每一次失败重投的消息路由到同一台机器上,因此处理消息时不应该做任何确定性假设
广播模式:适用场景&注意事项
广播消费模式下不支持顺序消息。
广播消费模式下不支持重置消费位点。
每条消息都需要被相同逻辑的多台机器处理。
消费进度在客户端维护,出现重复的概率稍大于集群模式。
广播模式下,消息队列 RocketMQ 保证每条消息至少被每台客户端消费一次,但是并不会对消费失败的消息进行失败重投,因此业务方需要关注消
费失败的情况。
广播模式下,客户端每一次重启都会从最新消息消费。客户端在被停止期间发送至服务端的消息将会被自动跳过,请谨慎选择。
广播模式下,每条消息都会被大量的客户端重复处理,因此推荐尽可能使用集群模式。
目前仅 Java 客户端支持广播模式
广播模式下服务端不维护消费进度,所以消息队列 RocketMQ 控制台不支持消息堆积查询、消息堆积报警和订阅关系查询功能。
顺序消息
消息有序指的是可以按照消息的发送顺序来消费(FIFO)。RocketMQ 可以严格的保证消息有序,可以分为分区有序或者全局有序。
顺序消费的原理解析,在默认的情况下消息发送会采取 Round Robin 轮询方式把消息发送到不同的 queue(分区队列);而消费消息的时候从多个 queue 上拉取消息,这种情况发送和消费是不能保证顺序。但是如果控制发送的顺序消息只依次发送到同一个 queue 中,消费的时候只从这个 queue 上依次拉 取,则就保证了顺序**。当发送和消费参与的 queue 只有一个,则是全局有序;如果多个 queue 参与,则为分区有序**,即相对每个 queue,消息都是有序的。
- 全局顺序消息

- 部分顺序消息

顺序消息生产
一个订单的顺序流程是:创建、付款、推送、完成。订单号相同的消息会被先后发送到同一个队列中,下面是订单进行分区有序的示例代码。
public class ProducerInOrder {
public static void main(String[] args) throws Exception {
DefaultMQProducer producer = new DefaultMQProducer("OrderProducer");
producer.setNamesrvAddr("localhost:9876");
producer.start();
String[] tags = new String[]{"TagA", "TagC", "TagD"};
// 订单列表
List<Order> orderList = new ProducerInOrder().buildOrders();
Date date = new Date();
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
String dateStr = sdf.format(date);
for (int i = 0; i < orderList.size(); i++) {
// 加个时间前缀
String body = dateStr + " Order:" + orderList.get(i);
Message msg = new Message("PartOrder", tags[i % tags.length], "KEY" + i, body.getBytes());
SendResult sendResult = producer.send(msg, new MessageQueueSelector() { // 选择分区
@Override
public MessageQueue select(List<MessageQueue> mqs, Message msg, Object arg) {
Long id = (Long) arg; //根据订单id选择发送queue
long index = id % mqs.size();
return mqs.get((int) index);
}
}, orderList.get(i).getOrderId());//订单id
System.out.println(String.format("SendResult status:%s, queueId:%d, body:%s",
sendResult.getSendStatus(),
sendResult.getMessageQueue().getQueueId(),
body));
}
producer.shutdown();
}
/**
* 订单
*/
private static class Order {
private long orderId;
private String desc;
public long getOrderId() {
return orderId;
}
public void setOrderId(long orderId) {
this.orderId = orderId;
}
public String getDesc() {
return desc;
}
public void setDesc(String desc) {
this.desc = desc;
}
@Override
public String toString() {
return "Order{" +
"orderId=" + orderId +
", desc='" + desc + '\'' +
'}';
}
}
}
顺序消息消费
消费时,同一个 OrderId 获取到的肯定是同一个队列。从而确保一个订单中处理的顺序。
public class ConsumerInOrder {
public static void main(String[] args) throws Exception {
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("OrderConsumer");
consumer.setNamesrvAddr("127.0.0.1:9876");
/**
* 设置Consumer第一次启动是从队列头部开始消费还是队列尾部开始消费<br>
* 如果非第一次启动,那么按照上次消费的位置继续消费
*/
consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_FIRST_OFFSET);
consumer.subscribe("PartOrder", "TagA || TagC || TagD");
consumer.registerMessageListener(new MessageListenerOrderly() { // 顺序消费
Random random = new Random();
@Override
public ConsumeOrderlyStatus consumeMessage(List<MessageExt> msgs, ConsumeOrderlyContext context) {
context.setAutoCommit(true);
for (MessageExt msg : msgs) {
// 可以看到每个queue有唯一的consume线程来消费, 订单对每个queue(分区)有序: 一个queue一个consume消费是保证有序的前提
System.out.println("consumeThread=" + Thread.currentThread().getName() + "queueId=" + msg.getQueueId() + ", content:" + new String(msg.getBody()));
}
try {
//模拟业务逻辑处理中...
TimeUnit.MILLISECONDS.sleep(random.nextInt(300));
} catch (Exception e) {
e.printStackTrace();
}
return ConsumeOrderlyStatus.SUCCESS;
}
});
consumer.start();
System.out.println("Consumer Started.");
}
}
消息发送时的重要方法/属性
属性
//todo producerGroup:生产者所属组(针对 事务消息 高可用)
DefaultMQProducer producer = new DefaultMQProducer("produce_details");
//todo 默认主题在每一个Broker队列数量(对于新创建主题有效)
producer.setDefaultTopicQueueNums(8);
//todo 发送消息默认超时时间,默认3s (3000ms)
producer.setSendMsgTimeout(1000*3);
//todo 消息体超过该值则启用压缩,默认4k
producer.setCompressMsgBodyOverHowmuch(1024 * 4);
//todo 同步方式发送消息重试次数,默认为2,总共执行3次
producer.setRetryTimesWhenSendFailed(2);
//todo 异步方式发送消息重试次数,默认为2,总共执行3次
producer.setRetryTimesWhenSendAsyncFailed(2);
//todo 消息重试时选择另外一个Broker时(消息没有存储成功是否发送到另外一个broker),默认为false
producer.setRetryAnotherBrokerWhenNotStoreOK(false);
//todo 允许发送的最大消息长度,默认为4M
producer.setMaxMessageSize(1024 * 1024 * 4);
// 设置NameServer的地址
producer.setNamesrvAddr("localhost:9876");//localhost
方法
单向发送
void sendOneway(final Message msg) throws MQClientException, RemotingException, InterruptedException;
选择指定队列单向发送消息
void sendOneway(final Message msg, final MessageQueue mq) throws MQClientException, RemotingException, InterruptedException;
同步发送
// 同步发送消息
SendResult send(final Message msg) throws MQClientException, RemotingException, MQBrokerException, InterruptedException;
// 同步超时发送消
SendResult send(final Message msg, final long timeout) throws MQClientException, RemotingException, MQBrokerException, InterruptedException;
//选择指定队列同步发送消息
SendResult send(final Message msg, final MessageQueue mq) throws MQClientException, RemotingException, MQBrokerException, InterruptedException;
异步发送
//异步发送消息: 异步都有callback
void send(final Message msg, final SendCallback sendCallback) throws MQClientException, RemotingException, InterruptedException;
//异步超时发送消息
void send(final Message msg, final SendCallback sendCallback, final long timeout) throws MQClientException, RemotingException, InterruptedException;
//选择指定队列异步发送消息
void send(final Message msg, final MessageQueue mq, final SendCallback sendCallback) throws MQClientException, RemotingException, InterruptedException;
消息消费时的重要方法/属性
属性
//todo 属性
//todo consumerGroup:消费者组
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("xhgy");
//todo 指定Namesrv地址信息.
consumer.setNamesrvAddr("localhost:9876");
//todo 消息消费模式(默认集群消费)
consumer.setMessageModel(MessageModel.CLUSTERING);
//todo 指定消费开始偏移量(上次消费偏移量、最大偏移量、最小偏移量、启动时间戳)开始消费
consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_LAST_OFFSET);
//todo 消费者最小线程数量(默认20)
consumer.setConsumeThreadMin(20);
//todo 消费者最大线程数量(默认20)
consumer.setConsumeThreadMax(20);
//todo 推模式下任务间隔时间(推模式也是基于不断的轮训拉取的封装)
consumer.setPullInterval(0);
//todo 推模式下任务拉取的条数,默认32条(一批批拉)
consumer.setPullBatchSize(32);
//todo 消息重试次数,-1代表16次 (超过 次数成为死信消息)-1,0,1,2,...,15
consumer.setMaxReconsumeTimes(-1);
//todo 消息消费超时时间(消息可能阻塞正在使用的线程的最大时间:以分钟为单位)
consumer.setConsumeTimeout(15); // 分钟
方法
void subscribe(final String topic, final MessageSelector selector) // 订阅消息,并指定队列选择器
void unsubscribe(final String topic) // :取消消息订阅
Set<MessageQueue> fetchSubscribeMessageQueues(final String topic) // :获取消费者对主题分配了那些消息队列
void registerMessageListener(final MessageListenerConcurrently messageListener) //:注册并发事件监听器
void registerMessageListener(final MessageListenerOrderly messageListener)//:注册顺序消息事件监听器1
消费确认(ACK)
业务实现消费回调的时候,当且仅当此回调函数返回 ConsumeConcurrentlyStatus.CONSUME_SUCCESS,RocketMQ 才会认为这批消息(默认是 1 条)是消费完成的, 中途断电,抛出异常等都不会认为成功——即都会重新投递。
返回 ConsumeConcurrentlyStatus.RECONSUME_LATER,RocketMQ 就会认为这批消息消费失败了。
如果业务的回调没有处理好而抛出异常,会认为是消费失败 ConsumeConcurrentlyStatus.RECONSUME_LATER 处理
为了保证消息是肯定被至少消费成功一次,RocketMQ 会把这批消息重发回 Broker(topic 不是原 topic 而是这个消费组的 RETRY topic),在延迟的某个时间点(默认是 10 秒,业务可设置)后,再次投递到这个 ConsumerGroup。而如果一直这样重复消费都持续失败到一定次数(默认 16 次),就会投递到 DLQ 死信队列。应用可以监控死信队列来做人工干预。
另外如果使用顺序消费的回调 MessageListenerOrderly 时,由于顺序消费是要前者消费成功才能继续消费,所以没有 RECONSUME_LATER 的这个状态,只有 SUSPEND_CURRENT_QUEUE_A_MOMENT 来暂停队列的其余消费,直到原消息不断重试成功为止才能继续消费
延时消息
概念介绍
延时消息:Producer 将消息发送到消息队列 RocketMQ 服务端,但并不期望这条消息立马投递***,而是延迟一定时间后才投递***到 Consumer 进行消费, 该消息即延时消息。
适用场景
消息生产和消费有时间窗口要求:比如在电商交易中超时未支付关闭订单的场景,在订单创建时会发送一条延时消息。这条消息将会在 30 分钟以 后投递给消费者,消费者收到此消息后需要判断对应的订单是否已完成支付。 如支付未完成,则关闭订单。如已完成支付则忽略
使用方式
Apache RocketMQ 目前只支持固定精度的定时消息,因为如果要支持任意的时间精度,在 Broker 层面,必须要做消息排序,如果再涉及到持久化, 那么消息排序要不可避免的产生巨大性能开销。(阿里云 RocketMQ 提供了任意时刻的定时消息功能,Apache 的 RocketMQ 并没有,阿里并没有开源)
发送延时消息时需要设定一个延时时间长度,消息将从当前发送时间点开始延迟固定时间之后才开始投递。
延迟消息是根据延迟队列的 level 来的,延迟队列默认是msg.setDelayTimeLevel(3)代表延迟 10 秒
“1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h”
**源码中:**org/apache/rocketmq/store/config/MessageStoreConfig.java // 129
代码演示
生产者
public class ScheduledMessageProducer {
public static void main(String[] args) throws Exception {
// 实例化一个生产者来产生延时消息
DefaultMQProducer producer = new DefaultMQProducer("ScheduledProducer");
// 设置NameServer的地址
producer.setNamesrvAddr("localhost:9876");
// 启动Producer实例
producer.start();
int totalMessagesToSend = 10;
for (int i = 0; i < totalMessagesToSend; i++) {
Message message = new Message("ScheduledTopic", ("Hello scheduled message " + i).getBytes());
// 设置延时等级3,这个消息将在10s之后投递给消费者(详看delayTimeLevel)
// delayTimeLevel:(1~18个等级)"1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h"
message.setDelayTimeLevel(4); // 设置延时等级
// 发送消息
producer.send(message);
}
// 关闭生产者
producer.shutdown();
}
}
消费者
public class ScheduledMessageConsumer {
public static void main(String[] args) throws Exception {
// 实例化消费者
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("ScheduledConsumer");
// 指定Namesrv地址信息.
consumer.setNamesrvAddr("localhost:9876");
// 订阅Topics
consumer.subscribe("ScheduledTopic", "*");
// 注册消息监听者
consumer.registerMessageListener(new MessageListenerConcurrently() {
@Override
public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> messages, ConsumeConcurrentlyContext context) {
for (MessageExt message : messages) {
// Print approximate delay time period
System.out.println("Receive message[msgId=" + message.getMsgId() + "] "
+ (message.getStoreTimestamp()-message.getBornTimestamp()) + "ms later");
}
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
}
});
// 启动消费者
consumer.start();
}
}
批量消息
批量发送消息能显著提高传递小消息的性能。限制是这些批量消息应该有相同的 topic,相同的 waitStoreMsgOK(集群时会细讲),而且不能是延 时消息。此外,这一批消息的总大小不应超过 4MB。
代码演示
// 生产者
public class BatchProducer {
public static void main(String[] args) throws Exception {
// 实例化消息生产者Producer
DefaultMQProducer producer = new DefaultMQProducer("BatchProducer");
// 设置NameServer的地址
producer.setNamesrvAddr("localhost:9876");
// 启动Producer实例
producer.start();
String topic = "BatchTest";
List<Message> messages = new ArrayList<>();
messages.add(new Message(topic, "Tag", "OrderID001", "Hello world 0".getBytes()));
messages.add(new Message(topic, "Tag", "OrderID002", "Hello world 1".getBytes()));
messages.add(new Message(topic, "Tag", "OrderID003", "Hello world 2".getBytes()));
try {
producer.send(messages);
} catch (Exception e) {
producer.shutdown();
e.printStackTrace();
}
// 如果不再发送消息,关闭Producer实例。
producer.shutdown();
}
}
// **消费者**
public class BatchComuser {
public static void main(String[] args) throws Exception {
// 实例化消息生产者,指定组名
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("BatchComsuer");
// 指定Namesrv地址信息.
consumer.setNamesrvAddr("localhost:9876");
// 订阅Topic
consumer.subscribe("BatchTest", "*");
//负载均衡模式消费
consumer.setMessageModel(MessageModel.CLUSTERING);
// 注册回调函数,处理消息
consumer.registerMessageListener(new MessageListenerConcurrently() {
@Override
public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs,
ConsumeConcurrentlyContext context) {
System.out.printf("%s Receive New Messages: %s %n",
Thread.currentThread().getName(), msgs);
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
}
});
//启动消息者
consumer.start();
System.out.printf("Consumer Started.%n");
}
}
批量切分
如果消息的总长度可能大于 4MB 时,这时候最好把消息进行分割
代码演示
我们需要发送 10 万元素的数组,这个量很大,怎么快速发送完。同时每一次批量发送的消息大小不能超过 4M 具体见代码
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.rocketmq.example.batch;
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.common.message.Message;
/**
* @author 【享学课堂】 King老师
* 批量消息-超过4m-生产者
*/
public class SplitBatchProducer {
public static void main(String[] args) throws Exception {
// 实例化消息生产者Producer
DefaultMQProducer producer = new DefaultMQProducer("BatchProducer");
// 设置NameServer的地址
producer.setNamesrvAddr("localhost:9876");
// 启动Producer实例
producer.start();
//large batch
String topic = "BatchTest";
List<Message> messages = new ArrayList<>(100 * 1000);
//10万元素的数组
for (int i = 0; i < 100 * 1000; i++) {
messages.add(new Message(topic, "Tag", "OrderID" + i, ("Hello world " + i).getBytes()));
}
//把大的消息分裂成若干个小的消息(1M左右)
ListSplitter splitter = new ListSplitter(messages);
while (splitter.hasNext()) {
List<Message> listItem = splitter.next();
producer.send(listItem);
Thread.sleep(100);
}
// 如果不再发送消息,关闭Producer实例。
producer.shutdown();
System.out.printf("Consumer Started.%n");
}
}
class ListSplitter implements Iterator<List<Message>> {
private int sizeLimit = 1000 * 1000;//1M
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();
}
tmpSize = tmpSize + 20; // 增加日志的开销20字节
if (tmpSize > sizeLimit) {
//单个消息超过了最大的限制(1M)
//忽略,否则会阻塞分裂的进程
if (nextIndex - currIndex == 0) {
//假如下一个子列表没有元素,则添加这个子列表然后退出循环,否则只是退出循环
nextIndex++;
}
break;
}
if (tmpSize + totalSize > sizeLimit) {
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");
}
}
// 核心思想就是自己去定义自己的迭代器, 这是一个很好的思路, 我们做切分也可以自己定义迭代器, 使用方不用关心细节
过滤消息
Tag 过滤
在大多数情况下,TAG 是一个简单而有用的设计,其可以来选择您想要的消息。
// 生产
public class TagFilterProducer {
public static void main(String[] args) throws Exception {
DefaultMQProducer producer = new DefaultMQProducer("TagFilterProducer");
//todo 指定Namesrv地址信息.
producer.setNamesrvAddr("localhost:9876");
producer.start();
String[] tags = new String[] {"TagA", "TagB", "TagC"};
for (int i = 0; i < 60; i++) {
Message msg = new Message("TagFilterTest",
tags[i % tags.length],
"Hello world".getBytes(RemotingHelper.DEFAULT_CHARSET));
SendResult sendResult = producer.send(msg);
System.out.printf("%s%n", sendResult);
}
producer.shutdown();
}
}
// 消费
public class TagFilterConsumer {
public static void main(String[] args) throws InterruptedException, MQClientException, IOException {
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("TagFilterComsumer");
//todo 指定Namesrv地址信息.
consumer.setNamesrvAddr("localhost:9876");
consumer.subscribe("TagFilterTest", "TagA || TAGB || TAGC"); // 区分大小写
consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_LAST_OFFSET);
consumer.registerMessageListener(new MessageListenerConcurrently() {
@Override
public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs,
ConsumeConcurrentlyContext context) {
try {
for(MessageExt msg : msgs) {
String topic = msg.getTopic();
String msgBody = new String(msg.getBody(), "utf-8");
String msgPro = msg.getProperty("a");
String tags = msg.getTags();
System.out.println("收到消息:" + " topic :" + topic + " ,tags : " + tags + " ,a : " + msgPro +" ,msg : " + msgBody);
}
} catch (Exception e) {
e.printStackTrace();
return ConsumeConcurrentlyStatus.RECONSUME_LATER;
}
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
}
});
consumer.start();
System.out.printf("Consumer Started.%n");
}
}
消费者将接收包含 TagA (区分大小写)或 TAGB 或 TAGC 的消息。但是限制是一个消息只能有一个标签,这对于复杂的场景可能不起作用。在这种情况下,可以使用 SQL 表达式筛选消息。SQL 特性可以通过发送消息时的属性来进行计算。
Sql 过滤
SQL 基本语法
RocketMQ 定义了一些基本语法来支持这个特性。你也可以很容易地扩展它。
只有使用 push 模式的消费者才能用使用 SQL92 标准的 sql 语句,常用的语句如下:
**数值比较:**比如:>,>=,<,<=,BETWEEN,=;
**字符比较:**比如:=,<>,IN; IS NULL 或者 IS NOT NULL;
**逻辑符号:**AND,OR,NOT;
常量支持类型为:
数值,比如:123,3.1415;
字符,比如:‘abc’,必须用单引号包裹起来;
NULL,特殊的常量
布尔值,TRUE 或 FALSE
消息生产者(加入消息属性)
发送消息时,你能通过 putUserProperty 来设置消息的属性
// msg.putUserProperty("a", String.valueOf(i));
public class SqlFilterProducer {
public static void main(String[] args) throws Exception {
DefaultMQProducer producer = new DefaultMQProducer("SqlFilterProducer");
//todo 指定Namesrv地址信息.
producer.setNamesrvAddr("localhost:9876");
producer.start();
String[] tags = new String[] {"TagA", "TagB", "TagC"};
for (int i = 0; i < 10; i++) {
Message msg = new Message("SqlFilterTest",
tags[i % tags.length],
("Hello RocketMQ " + i).getBytes(RemotingHelper.DEFAULT_CHARSET)
);
// 设置一些属性
msg.putUserProperty("a", String.valueOf(i));
SendResult sendResult = producer.send(msg);
System.out.printf("%s%n", sendResult);
}
producer.shutdown();
}
}
消息消费者(使用 SQL 筛选)
用 MessageSelector.bySql 来使用 sql 筛选消息
public class SqlFilterConsumer {
public static void main(String[] args) throws Exception {
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("SqlFilterConsumer");
//todo 指定Namesrv地址信息.
consumer.setNamesrvAddr("localhost:9876");
// Don't forget to set enablePropertyFilter=true in broker
consumer.subscribe("SqlFilterTest",
MessageSelector.bySql("(TAGS is not null and TAGS in ('TagA', 'TagB'))" +
"and (a is not null and a between 0 and 3)"));
consumer.registerMessageListener(new MessageListenerConcurrently() {
@Override
public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs,
ConsumeConcurrentlyContext context) {
try {
for(MessageExt msg : msgs) {
String topic = msg.getTopic();
String msgBody = new String(msg.getBody(), "utf-8");
String msgPro = msg.getProperty("a");
String tags = msg.getTags();
System.out.println("收到消息:" + " topic :" + topic + " ,tags : " + tags + " ,a : " + msgPro +" ,msg : " + msgBody);
}
} catch (Exception e) {
e.printStackTrace();
return ConsumeConcurrentlyStatus.RECONSUME_LATER;
}
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
}
});
consumer.start();
System.out.printf("Consumer Started.%n");
}
}
如果这个地方抛出错误:说明 Sql92 功能没有开启

需要修改 Broker.conf 配置文件。
加入 enablePropertyFilter=true 然后重启 Broker 服务
事务消息

其中分为两个流程:正常事务消息的发送及提交、事务消息的补偿流程。
正常事务流程
(1) 发送消息(half 消息):图中步骤 1。
(2) 服务端响应消息写入结果:图中步骤 2。
(3) 根据发送结果执行本地事务(如果写入失败,此时 half 消息对业务不可见,本地逻辑不执行):图中步骤 3。
(4) 根据本地事务状态执行 Commit 或者 Rollback(Commit 操作生成消息索引,消息对消费者可见):图中步骤 4
事务补偿流程
(1) 对没有 Commit/Rollback 的事务消息(pending 状态的消息),从服务端发起一次“回查”:图中步骤 5。
(2) Producer 收到回查消息,检查回查消息对应的本地事务的状态:图中步骤 6。
(3) 根据本地事务状态,重新 Commit 或者 Rollback::图中步骤 6。
其中,补偿阶段用于解决消息 Commit 或者 Rollback 发生超时或者失败的情况。
事务消息状态
事务消息共有三种状态,提交状态、回滚状态、中间状态:
-
TransactionStatus.CommitTransaction: 提交状态,它允许消费者消费此消息(完成图中了 1,2,3,4 步,第 4 步是 Commit)。
-
TransactionStatus.RollbackTransaction: 回滚状态,它代表该消息将被删除,不允许被消费(完成图中了 1,2,3,4 步, 第 4 步是 Rollback)。
-
TransactionStatus.Unknown: 中间状态,它代表需要检查消息队列来确定状态(完成图中了 1,2,3 步, 但是没有 4 或者没有 7,无法 Commit 或 Rollback)。
代码演示
创建事务性生产者
使用 TransactionMQProducer 类创建生产者,并指定唯一的 ProducerGroup,就可以设置自定义线程池来处理这些检查请求。执行本地事务后、需 要根据执行结果对消息队列进行回复。
public class TransactionProducer {
public static void main(String[] args) throws MQClientException, InterruptedException {
//创建事务监听器
TransactionListener transactionListener = new TransactionListenerImpl();
//创建消息生产者
TransactionMQProducer producer = new TransactionMQProducer("TransactionProducer");
// 设置NameServer的地址
producer.setNamesrvAddr("localhost:9876");
//创建线程池
ExecutorService executorService = new ThreadPoolExecutor(2, 5, 100, TimeUnit.SECONDS, new ArrayBlockingQueue<Runnable>(2000), new ThreadFactory() {
@Override
public Thread newThread(Runnable r) {
Thread thread = new Thread(r);
thread.setName("client-transaction-msg-check-thread");
return thread;
}
});
//设置生产者回查线程池
producer.setExecutorService(executorService);
//生产者设置监听器
producer.setTransactionListener(transactionListener);
//启动消息生产者
producer.start();
//todo 1、开启事务
//todo @Transaction
System.out.println("开启事务@Transaction");
try {
Message msg =
new Message("TransactionTopic", null, ("A向B系统转100块钱 ").getBytes(RemotingHelper.DEFAULT_CHARSET));
//todo 半事务的发送
SendResult sendResult = producer.sendMessageInTransaction(msg, null);
sendResult.getMsgId();//MQ生成的
} catch (MQClientException | UnsupportedEncodingException e) {
//todo 回滚rollback
e.printStackTrace();
}
//等待,因为要等输入密码,确认等操作,因为要事务回查,
for (int i = 0; i < 1000; i++) {
Thread.sleep(1000);
}
producer.shutdown();
}
}
实现事务的监听接口
当发送半消息成功时,我们使用 executeLocalTransaction 方法来执行本地事务(步骤 3)。它返回前一节中提到的三个事务状态之一。
checkLocalTranscation 方法用于检查本地事务状态(步骤 5),并回应消息队列的检查请求。它也是返回前一节中提到的三个事务状态之一。
public class TransactionListenerImpl implements TransactionListener {
//执行本地事务
@Override
public LocalTransactionState executeLocalTransaction(Message msg, Object arg) {
//todo 执行本地事务 update A...
System.out.println("update A ... where transactionId:"+msg.getTransactionId());
//todo 情况1:本地事务成功
// System.out.println("commit:"+msg.getTransactionId());
// System.out.println("执行本地事务成功,确认消息");
// return LocalTransactionState.COMMIT_MESSAGE;
//todo 情况2:本地事务失败
// System.out.println("rollback:"+msg.getTransactionId());
// System.out.println("执行本地事务失败,删除消息");
// return LocalTransactionState.ROLLBACK_MESSAGE;
//todo 情况3:业务复杂,还处于中间过程或者依赖其他操作的返回结果,就是unknow
System.out.println("业务比较长,还没有处理完,不知道是成功还是失败!");
return LocalTransactionState.UNKNOW;
}
//事务回查 默认是60s,一分钟检查一次
@Override
public LocalTransactionState checkLocalTransaction(MessageExt msg) {
//打印每次回查的时间
SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");//设置日期格式
System.out.println("checkLocalTransaction:"+df.format(new Date()));// new Date()为获取当前系统时间
//todo 情况3.1:业务回查成功!
// System.out.println("业务回查:执行本地事务成功,确认消息");
// return LocalTransactionState.COMMIT_MESSAGE;
//todo 情况3.2:业务回查回滚!
// System.out.println("业务回查:执行本地事务失败,删除消息");
// return LocalTransactionState.ROLLBACK_MESSAGE;
//todo 情况3.3:业务回查还是UNKNOW!
System.out.println("业务比较长,还没有处理完,不知道是成功还是失败!");
return LocalTransactionState.UNKNOW;
}
}
// BrokerConfig.java
@ImportantField
private long transactionCheckInterval = 60 * 1000; // 178
使用场景
用户提交订单后,扣减库存成功、扣减优惠券成功、使用余额成功,但是在确认订单操作失败,需要对库存、库存、余额进行回退。如何保证数据的完整性?
可以使用 RocketMQ 的分布式事务保证在下单失败后系统数据的完整性(注意是确定本地事务和提交到mq的一致性)
使用限制
-
事务消息不支持延时消息和批量消息。
-
**事务回查的间隔时间:**BrokerConfig. transactionCheckInterval 通过 Broker 的配置文件设置好
-
为了避免单个消息被检查太多次而导致半队列消息累积,我们默认将单个消息的检查次数限制为 15 次,但是用户可以通过 Broker 配置文件的 transactionCheckMax 参数来修改此限制。如果已经检查某条消息超过 N 次的话( N = transactionCheckMax ) 则 Broker 将丢弃此消息,并在默认情况下同时打印错误日志。用户可以通过重写 AbstractTransactionCheckListener 类来修改这个行为。
-
事务消息将在 Broker 配置文件中的参数 transactionMsgTimeout 这样的特定时间长度之后被检查。当发送事务消息时,用户还可以通过设置用户属性 CHECK_IMMUNITY_TIME_IN_SECONDS 来改变这个限制,该参数优先于 transactionMsgTimeout 参数。
-
事务性消息可能不止一次被检查或消费。
-
事务性消息中用到了生产者群组,这种就是一种高可用机制,用来确保事务消息的可靠性。
-
提交给用户的目标主题消息可能会失败,目前这依日志的记录而定。它的高可用性通过 RocketMQ 本身的高可用性机制来保证,如果希望确保事务消息不丢失、并且事务完整性得到保证,建议使用同步的双重写入机制。
-
事务消息的生产者 ID 不能与其他类型消息的生产者 ID 共享。与其他类型的消息不同,事务消息允许反向查询、MQ 服务器能通过它们的生产者ID 查询到消费者。
分布式事务
分布式事务就是两个系统之间交互,都有可能失败,但是当一个系统的提交了,另一个系统不能很好的让他回滚,所以很多做法就是发送一个补充的提交,让数据保持一致,RocketMq的思想就是,MQ妥协,当数据到达队列暂存,只有你确定了你成功了我才发送,其实这里有个假设,只有我发送数据(重消息)发送可能失败,我其他的确认消息一定不会失败,因为假设我们过程中长时间断网也是没辙的(这种可能性几乎为零,所以我们也认为这种方案的可靠的,因为这种情况那个系统都无能为力)
3375

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



