RocketMQ概念与使用
一、概述
MetaQ是一款分布式、队列模型的消息中间件,基于发布订阅模式,支持Topic与Queue两种模式,有Push和Pull两种消费方式,支持严格的消息顺序,亿级别的消息堆积能力,支持消息回溯和多个维度的消息查询。
MetaQ和RocketMQ区别:两者等价,在阿里内部称为MetaQ 3.0,对外称为RocketMQ 3.0。MetaQ基于RocketMQ内核采用拉模型,主要解决顺序消息和海量堆积问题。
1.1 集群架构
- NameServer:
• 专为 RocketMQ 设计的轻量级域名服务器,具有简单、可集群横向扩展、无状态、节点之间互不通信等特点。
• 由于ZooKeeper功能过重,RocketMQ(即MetaQ 3.x)去掉了对ZooKeeper依赖,采用自己的NameServer。
• 只需要保持最终一致性,即为AP模式。
• Broker集群、Producer集群、Consumer集群都需要与NameServer集群进行通信。 - Broker:
• 消息中转角色,负责接收消息,存储消息,转发消息。
• 一个Broker集群由多组Master/Slave组成,BrokerId为0表示Master,非0表示Slave。
• 每个Broker节点在启动时,都会遍历轮询 NameServer列表,与每个NameServer建立长连接,注册自己的信息,之后定时上报。 - Consumer:
• 通过NameServer集群获得Topic的路由信息,连接到对应的Broker上,分为 Push Consumer / Pull Consumer,定时向Master、Slave发送心跳。
• 前者向Consumer对象注册一个Listener接口,收到消息后回调Listener接口方法,采用long-polling长轮询实现push。
• 后者由Consumer主动拉取信息,同Kafka。 - Producer:
• 消息生产者,通过NameServer集群获得Topic的路由信息,包括Topic下面有哪些Queue,这些Queue分布在哪些Broker上等。
• Producer只会将消息发送到Master节点上,因此只需与Master节点建立长连接,定时向Master发送心跳。
1.2 消息领域模型
- Message:单位消息,消息队列中信息传递的载体;
- Topic:软分区,对应相同的topic时,生产者对应消费者的分区标识。消息主题,一级消息类型,通过 Topic 对消息进行分类;
- Tag:消息在topic基础上的二级分类;
- Message Queue:硬分区,物理上区分topic,一个topic对应多个message queue,消息是发送到多个broker上的多个队列上;
- Consumer Group(ID):一类 Consumer 的集合名称,这类 Consumer 通常消费一类消息,且消费逻辑一致;
- Producer Group(ID):一类 Producer 的集合名称,这类 Producer 通常发送一类消息,且发送逻辑一致;
- Offset:绝对偏移值,message queue中有两类offset(commitOffset和offset),前者存储在OffsetStore中表示消费到的位置,后者是在PullRequest中为拉取消息位置;
- 广播消费:Producer 向一些队列轮流发送消息,队列集合称为 Topic,每一个 consumer 实例都消费这个 Topic 对应的所有队列中的消息;
- 集群消费:多个 Consumer 实例平均消费(Rebalance) 这个 topic 对应的队列集合中的消息。《Kafka Rebalance机制分析》。
- Producer 实例:Producer 的一个对象实例,不同的 Producer 实例可以运行在不同进程内或者不同机器上。Producer 实例线程安全,可在同一进程内多线程之间共享。
- Consumer 实例:Consumer 的一个对象实例,不同的 Consumer 实例可以运行在不同进程内或者不同机器上。一个 Consumer 实例内配置线程池消费消息。
1.3 注意事项
1.3.1 发布订阅
- 对于重要消息,业务方需要重发补偿的机制;
- 对于大Message,配置压缩与解压缩,推荐进行拆分:
• MetaQ采用RPC方式,可能会导致网络层的Buffer异常;
• 服务器存储是LRU CACHE系统,过大的消息会占用较多Cache; - 设置Message Key属性进行唯一标识,如各类id;
- 发送消息时,自由设置Tag,完成订阅方过滤需求;
- 订阅Topic时,可以使用表达式方式过滤消息(Message Tag);
- 非顺序消息消费,耗时时间不做限制,顺序消息消费,耗时时间有限制。
1.3.2 消息重复
- MetaQ不能保证消息不重复:分布式环境下的超时问题,Rebalance短暂不一致,订阅者意外宕机;
- 通过DB等去重,或者通过主动拉的方式,可保证拉消息绝对不重复。
1.3.3 消息重试
- 非顺序消息消费失败重试,可以指定下次到达Consumer的时间,消费失败重试次数有限制,超过重试次数,消息进入死信队列。
- 顺序消息消费失败重试,某个队列正在消费的消息消费失败,会将当前队列挂起。
1.3.4 消费幂等
- 发送时消息重复【消息 Message ID 不同】:MQ Producer 发送消息场景下,消息已成功发送到服务端并完成持久化,此时网络闪断或者客户端宕机导致服务端应答给客户端失败。如果此时 MQ Producer 意识到消息发送失败并尝试再次发送消息,MQ 消费者后续会收到两条内容相同但是 Message ID 不同的消息。
- 投递时消息重复【消息 Message ID 相同】:MQ Consumer 消费消息场景下,消息已投递到消费者并完成业务处理,当客户端给服务端反馈应答的时候网络闪断。为了保证消息至少被消费一次,MQ 服务端将在网络恢复后再次尝试投递之前已被处理过的消息,MQ 消费者后续会收到两条内容相同并且 Message ID 也相同的消息。
真正安全的幂等处理,不建议以 Message ID 作为处理依据,最好的方式是以业务唯一标识作为幂等处理的关键依据,而业务的唯一标识可以通过消息 Key 进行设置。
Message message = new Message();
message.setKey("ORDERID_10");
SendResult sendResult = producer.send(message);
1.3.5 消息堆积
真实堆积:
- 消费正常,发送方量级增大超出消费上限,出现堆积;
- 消费变慢,出现堆积,通过单机视角jstack、arthas、排查RT是否增长;
- 顺序消费,某个队列某条数据消费失败,导致当前队列block,堆积增大,需要解决消费失败问题。
1.4 集群
双主双从:
工作流程:
- 启动NameServer,NameServer起来后监听端口,等待Broker、Producer、Consumer连上来。
- Broker启动,跟所有的NameServer保持长连接,定时发送心跳包。心跳包中包含当前Broker信息(IP+端口等)以及所有的Topic信息,注册成功后,NameServer集群中就有Topic跟Broker的映射关系。
- 收发消息前先创建Topic,创建Topic时需指定该Topic要存储在哪些Broker上,也可以在发送消息时自动创建Topic。
- Producer发送消息,启动时先跟NameServer集群中的其中一台建立长连接,并从NameServer中获取当前发送的Topic存在哪些Broker上,轮询从队列列表中选择一个队列,然后与队列所在的Broker建立长连接从而向Broker发消息。
- Consumer跟其中一台NameServer建立长连接,获取当前订阅Topic存在哪些Broker上,然后直接跟Broker建立连接通道,开始消费消息。
1.5 mqadmin集群管理工具
略。。。
二、基本使用
- RocketMQ客户端依赖导入
<dependency>
<groupId>org.apache.rocketmq</groupId>
<artifactId>rocketmq-client</artifactId>
<version>4.4.0</version>
</dependency>
2.1 普通消息发送
一般步骤:
- 创建消息生产者Producer,并制定生产者组名;
- 指定Nameserver地址;
- 启动Producer;
- 创建消息对象,指定主题Topic、Tag和消息体;
- 发送消息;
- 关闭生产者Producer。
注意:
- 一个应用创建一个Producer,ProducerGroupName需由应用来保证唯一(全局对象或者单例)。
- 默认会在topic不存在时自动创建,并且为topic生成4个MessageQueue。
- Producer对象在使用之前必须调用start()初始化一次(注意不是每次发消息时)。
- 一个Producer对象可以同步发送(send(message))多个topic、多个tag的消息。
- 应用退出时,shutdown一下。
- 发送同步消息
public class SyncProducer {
public static void main(String[] args) throws Exception {
// 1.创建消息生产者 Producer,并制定生产者组名
DefaultMQProducer producer = new DefaultMQProducer("group1");
// 2.指定 Nameserver 地址:集群配置(用;隔离)
producer.setNamesrvAddr("一号服务器公网IP:9876;二号服务器公网IP:9876");
// 3.启动 Producer
producer.start();
for (int i = 0; i < 10; i++) {
// 4.创建消息对象,指定主题 Topic、Tag 和消息体
/**
* 参数一:消息主题 Topic
* 参数二:消息 Tag
* 参数三:消息内容:字节数组
*/
Message msg = new Message("base", "Tag1", ("Hello World" + i).getBytes());
// 5.发送同步消息
// 阻塞等待回传
SendResult result = producer.send(msg);
// 获取发送状态
SendStatus status = result.getSendStatus();
// 消息 ID 和接收队列 ID
String msgId = result.getMsgId();
int queueId = result.getMessageQueue().getQueueId();
System.out.println("发送结果:" + result + msgId + queueId);
// 线程睡 1 秒
TimeUnit.SECONDS.sleep(1);
}
// 6.关闭生产者 Producer
producer.shutdown();
}
}
- 发送异步消息
在send()方法中实现 SendCallback() 回调接口。
异步消息通常用在对响应时间敏感的业务场景,即发送端不能容忍长时间地等待Broker的响应。
public class AsyncProducer {
public static void main(String[] args) throws Exception {
// 1.创建消息生产者 Producer,并制定生产者组名
DefaultMQProducer producer = new DefaultMQProducer("group1");
// 2.指定 Nameserver 地址
producer.setNamesrvAddr("一号服务器公网IP:9876;二号服务器公网IP:9876");
// 3.启动 Producer
producer.start();
for (int i = 0; i < 10; i++) {
// 4.创建消息对象,指定主题 Topic、Tag 和消息体
Message msg = new Message("base", "Tag2", ("Hello World" + i).getBytes());
// 5.发送异步消息
producer.send(msg, new SendCallback() {
/**
* 发送成功的回调函数
*/
public void onSuccess(SendResult sendResult) {
System.out.println("发送结果:" + sendResult);
}
/**
* 发送失败的回调函数
*/
public void onException(Throwable e) {
System.out.println("发送异常:" + e);
}
});
// 线程睡 1 秒
TimeUnit.SECONDS.sleep(1);
}
// 6.关闭生产者 Producer
producer.shutdown();
}
- 发送单向消息
- 这种方式主要用在不特别关心发送结果的场景,例如日志发送。
- 利用sendOneway(msg) 发送消息,不返回结果。
/**
* 发送单向消息
*/
public class OneWayProducer {
public static void main(String[] args) throws Exception, MQBrokerException {
// 1.创建消息生产者 Producer,并制定生产者组名
DefaultMQProducer producer = new DefaultMQProducer("group1");
// 2.指定 Nameserver 地址
producer.setNamesrvAddr("一号服务器公网IP:9876;二号服务器公网IP:9876");
// 3.启动 Producer
producer.start();
for (int i = 0; i < 3; i++) {
// 4.创建消息对象,指定主题 Topic、Tag 和消息体
Message msg = new Message("base", "Tag3", ("Hello World,单向消息" + i).getBytes());
// 5.发送单向消息
producer.sendOneway(msg);
// 线程睡 1 秒
TimeUnit.SECONDS.sleep(5);
}
// 6.关闭生产者 Producer
producer.shutdown();
}
}
2.2 普通消息消费
一般步骤:
- 创建消费者Consumer,制定消费者组名;
- 指定Nameserver地址;
- 订阅主题Topic和Tag;
- 设置回调函数,处理消息;
- 启动消费者Consumer。
注意:
- 一个应用创建一个Consumer,ConsumerGroupName需由应用来保证唯一,(全局对象或者单例)。
- setConsumeFromWhere设置从哪里开始消费,subscribe订阅指定topic下的Tags(可以通配符)。
- setConsumeMessageBatchMaxSize()设置批量消费。
- 如果为批量消费,要么都成功,要么都失败。
- 注册监听器,自定义实现回调方法,在消息到达时执行,此方法由RabbitMQ客户端多线程回调,需要应用来处理并发安全问题。
- 消息消费不成功有重试机制,达到阈值后则丢弃。
- 调用start()初始化。
- 默认负载均衡模式——集群消费
- 消费者采用负载均衡方式消费消息,多个消费者共同消费队列消息,每个消费者处理的消息不同。
- 当前示例为PushConsumer,感官上是从服务器推到了客户端,实际上Consumer内部使用长轮询Pull方式从RabbitMQ服务器拉消息,然后再回调Listener方法。
public class Consumer {
public static void main(String[] args) throws Exception {
// 1.创建消费者 Consumer,制定消费者组名
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("group1");
// 2.指定 Nameserver 地址
consumer.setNamesrvAddr("39.96.86.4:9876;59.110.158.93:9876");
// 3.订阅主题 Topic 和 Tag
consumer.subscribe("base", "Tag1")
consumer.registerMessageListener(new MessageListenerConcurrently() {
@Override
public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> list, ConsumeConcurrentlyContext consumeConcurrentlyContext) {
System.out.println(list);
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
}
});
//5.启动消费者consumer
consumer.start();
}
}
- 广播模式消费消息
通过setMessageModel() 进行模式指定。
public class Consumer {
public static void main(String[] args) throws Exception {
// 1.创建消费者 Consumer,制定消费者组名
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("group1");
// 2.指定 Nameserver 地址
consumer.setNamesrvAddr("39.96.86.4:9876;59.110.158.93:9876");
// 3.订阅主题 Topic 和 Tag
consumer.subscribe("base", "Tag2");
// 指定消费模式为广播模式
consumer.setMessageModel(MessageModel.BROADCASTING);
consumer.registerMessageListener(new MessageListenerConcurrently() {
@Override
public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> list, ConsumeConcurrentlyContext consumeConcurrentlyContext) {
System.out.println(list);
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
}
});
// 5.启动消费者 Consumer
consumer.start();
}
}
2.3 顺序(queue)消息
消息顺序问题:
- RocketMQ可以严格地保证消息有序,可以分为分区(queue)有序和全局有序(只有一个队列的情况)。
- Broker存储消息有序的前提是Producer发送消息是有序的:
• 需要同一线程发送一组消息,而消息不能异步发送,同步发送时才能保证Broker收到的是有序的。
• 同步保证前一个消息发送成功并返回响应后,再发送下一个。
• 每次发送选择的是同一个MessageQueue。 - Consumer消费也要是顺序的,需要保证一个queue只在一个线程内被消费。
- 例:一个订单的顺序流程是创建、付款、推送、完成,订单号相同的消息会被先后发送到同一个队列中,消费时,同一个OrderId获取到的肯定是同一个队列。
2.3.1 顺序发送
- 在send()中实现一个MessageQueueSelector,重写选择MessageQueue的select()方法。
- 自定义选择MessageQueue的话,就可以控制将某一类消息发送到对应的MessageQueue中了。
- send()的最后一个参数,会作为参数被传给select方法中的arg。
public class Producer {
public static void main(String[] args) throws Exception {
// 1.创建消息生产者 Producer,并制定生产者组名
DefaultMQProducer producer = new DefaultMQProducer("group1");
// 2.指定 Nameserver 地址
producer.setNamesrvAddr("一号服务器公网IP:9876;二号服务器公网IP:9876");
// 3.启动 Producer
producer.start();
// 构建消息(订单)集合
List<OrderStep> orderSteps = OrderStep.buildOrders();
// 发送消息
for (int i = 0; i < orderSteps.size(); i++) {
String body = orderSteps.get(i) + "";
Message message = new Message("OrderTopic", "Order", "i" + i, body.getBytes());
/**
* 参数一:消息对象
* 参数二:消息队列的选择器
* 参数三:选择队列的业务标识(订单 ID)
*/
SendResult sendResult = producer.send(message, new MessageQueueSelector() {
/**
*
* @param mqs:队列集合
* @param msg:消息对象
* @param arg:业务标识的参数
*/
@Override
public MessageQueue select(List<MessageQueue> mqs, Message msg, Object arg) {
long orderId = (long) arg;
long index = orderId % mqs.size();
return mqs.get((int) index);
}
}, orderSteps.get(i).getOrderId());
System.out.println("发送结果:" + sendResult);
}
producer.shutdown();
}
}
- 对于官方Demo,可以看出,orderId为6的订单被顺序放入queueId为6的队列,且queueOffset同时在顺序增长。
2.3.2 顺序消费
- 注册的监听器中需要实现MessageListenerOrderly接口。
- 在集群模式下,利用分布式锁,保证了一个queue只会被一个消费者锁定和消费。
- 加锁保证只有一个线程可以消费,从ProcessQueue中获取出来的消息是有序的,Consumer保证了消费的有序性。
public class Consumer {
public static void main(String[] args) throws MQClientException {
// 1.创建消费者 Consumer,制定消费者组名
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("group1");
// 2.指定 Nameserver 地址
consumer.setNamesrvAddr("一号服务器公网IP:9876;二号服务器公网IP:9876");
// 3.订阅主题 Topic 和 Tag
consumer.subscribe("OrderTopic", "*");
// 4.注册消息监听器
consumer.registerMessageListener(new MessageListenerOrderly() {
@Override
public ConsumeOrderlyStatus consumeMessage(List<MessageExt> msgs, ConsumeOrderlyContext context) {
for (MessageExt msg : msgs) {
System.out.println("线程名称:【" + Thread.currentThread().getName() + "】:" + new String(msg.getBody()));
}
return ConsumeOrderlyStatus.SUCCESS;
}
});
// 5.启动消费者
consumer.start();
System.out.println("消费者启动");
}
}
顺序消费结果:
2.4 延迟消息
- 比如电商里,提交了一个订单就可以发送一个延时消息,1h后去检查这个订单的状态,如果还是未付款就取消订单释放库存。
- 该逻辑可以类比RabbitMQ中用死信队列构造延时队列,来实现分布式事务。
- RocketMQ并不支持任意时间的延时,需要设置几个固定的延时等级,从1s到2h分别对应着等级1到18。
// MessageStoreConfig.java
private String messageDelayLevel = "1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h";
- 发送端:直接在Message中设置延迟时间——setDelayTimeLevel(2)
public class Producer {
public static void main(String[] args) throws InterruptedException, RemotingException, MQClientException, MQBrokerException {
// 1.创建消息生产者 Producer,并制定生产者组名
DefaultMQProducer producer = new DefaultMQProducer("group1");
// 2.指定 Nameserver 地址
producer.setNamesrvAddr("一号服务器公网IP:9876;二号服务器公网IP:9876");
// 3.启动 Producer
producer.start();
for (int i = 0; i < 10; i++) {
// 4.创建消息对象,指定主题 Topic、Tag 和消息体
Message msg = new Message("DelayTopic", "Tag1", ("Hello World" + i).getBytes());
// 设定延迟时间
msg.setDelayTimeLevel(2);
// 5.发送消息
SendResult result = producer.send(msg);
SendStatus status = result.getSendStatus();
System.out.println("发送结果:" + result);
TimeUnit.SECONDS.sleep(1);
}
// 6.关闭生产者 Producer
producer.shutdown();
}
}
- 消费端
public class Consumer {
public static void main(String[] args) throws Exception {
// 1.创建消费者 Consumer,制定消费者组名
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("group1");
// 2.指定 Nameserver 地址
consumer.setNamesrvAddr("一号服务器公网IP:9876;二号服务器公网IP:9876");
// 3.订阅主题 Topic 和 Tag
consumer.subscribe("DelayTopic", "*");
// 4.设置回调函数,处理消息
consumer.registerMessageListener(new MessageListenerConcurrently() {
//接受消息内容
@Override
public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs, ConsumeConcurrentlyContext context) {
for (MessageExt msg : msgs) {
System.out.println("消息ID:【" + msg.getMsgId() + "】,延迟时间:" + (System.currentTimeMillis() - msg.getStoreTimestamp()));
}
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
}
});
// 5.启动消费者 Consumer
consumer.start();
System.out.println("消费者启动");
}
}
2.5 批量消息发送
批量发送消息能显著提高传递小消息的性能,批量消息应该有相同的Topic和waitStoreMsgOK,而且不能是延时消息。批量消息的单次发送不应超过4MB,大于4MB时最好把消息进行分割。
- 批量发送与消息分割
public class ListSplitter implements Iterator<List<Message>> {
private final int SIZE_LIMIT = 1024 * 1024 * 4;
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 > SIZE_LIMIT) {
// 单个消息超过了最大的限制
// 忽略,否则会阻塞分裂的进程
if (nextIndex - currIndex == 0) {
// 假如下一个子列表没有元素,则添加这个子列表,然后退出循环,否则只是退出循环
nextIndex++;
}
break;
}
if (tmpSize + totalSize > SIZE_LIMIT) {
break;
} else {
totalSize += tmpSize;
}
}
List<Message> subList = messages.subList(currIndex, nextIndex);
currIndex = nextIndex;
return subList;
}
}
// 把大的消息分裂成若干个小的消息
ListSplitter splitter = new ListSplitter(messages);
while (splitter.hasNext()) {
try {
List<Message> listItem = splitter.next();
producer.send(listItem);
} catch (Exception e) {
e.printStackTrace();
//处理error
}
}
2.6 过滤消息
2.6.1 Tag过滤
消费者将接收包含TAGA或TAGB或TAGC的消息,但是限制是一个消息只能有一个Tag标签,这对于复杂的场景可能不起作用。如需订阅某 Topic 下所有类型的消息,Tag 用符号 * 表示。
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("CID_EXAMPLE");
consumer.subscribe("TOPIC", "TAGA || TAGB || TAGC");
2.6.2 SQL过滤
可以使用SQL表达式筛选消息,SQL特性可以通过发送消息时的属性来进行计算,在RocketMQ定义的语法下,可以实现一些简单的逻辑:
- 数值比较,比如:>,>=,<,<=,BETWEEN,=;
- 字符比较,比如:=,<>,IN;
- IS NULL 或者 IS NOT NULL;
- 逻辑符号 AND,OR,NOT;
支持类型:
- 数值:123,3.1415;
- 字符:‘abc’,必须用单引号
- NULL,特殊的常量
- 布尔值:TRUE 或 FALSE
- Produce
Message中调用putUserProperty() 传入一些SQL要用的参数。
public class Producer {
public static void main(String[] args) throws InterruptedException, RemotingException, MQClientException, MQBrokerException {
// 1.创建消息生产者 Producer,并制定生产者组名
DefaultMQProducer producer = new DefaultMQProducer("group1");
// 2.指定 Nameserver 地址
producer.setNamesrvAddr("一号服务器公网IP:9876;二号服务器公网IP:9876");
// 3.启动 Producer
producer.start();
for (int i = 0; i < 10; i++) {
// 4.创建消息对象,指定主题 Topic、Tag 和消息体
Message msg = new Message("FilterSQLTopic", "Tag1", ("Hello World" + i).getBytes());
// 设置一些属性
msg.putUserProperty("i", String.valueOf(i));
// 5.发送消息
SendResult result = producer.send(msg);
SendStatus status = result.getSendStatus();
System.out.println("发送结果:" + result);
TimeUnit.SECONDS.sleep(1);
}
// 6.关闭生产者 Producer
producer.shutdown();
}
}
- Consumer
subscribe()传入选择器MessageSelector,调用bySql() 进行SQL过滤语句编写。
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("please_rename_unique_group_name_4");
// 只有订阅的消息有属性 i:i >= 0 and i <= 3
consumer.subscribe("FilterSQLTopic", MessageSelector.bySql("a between 0 and 3");
consumer.registerMessageListener(new MessageListenerConcurrently() {
@Override
public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs, ConsumeConcurrentlyContext context) {
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
}
});
consumer.start();
2.7 事务消息——解决分布式事务问题
发送事务消息是一个二阶段过程,类似于数据库的二阶段提交,包括两个过程:
- 发送Half消息:应用发送Half消息到Broker,消费方此时不能拉取到消息,需要等待后续Half消息状态。
- 发送事务状态:应用根据自身的事务执行情况,对Half消息发送事务状态:Commit、Rollback、UNKNOW(当前无法判决事务状态,如事务无法及时完成)。
2.7.1 基本流程
事务消息共有三种状态,提交状态、回滚状态、中间状态:
- TransactionStatus.CommitTransaction:提交事务,它允许消费者消费此消息。
- TransactionStatus.RollbackTransaction:回滚事务,它代表该消息将被删除,不允许被消费。
- TransactionStatus.Unknown:中间状态,它代表需要检查消息队列来确定状态。
事务流程:
- 事务消息的发送及提交
- 发送消息(half消息);
- 服务端响应消息写入结果;
- 根据发送结果执行本地事务,如果写入失败,此时half消息对业务不可见,本地逻辑不执行;
- 根据本地事务状态执行Commit或者Rollback,Commit操作生成消息索引,消息对消费者可见。
- 事务补偿
- 对没有Commit/Rollback的事务消息(pending状态的消息),从服务端发起一次回查;
- Producer收到回查消息,检查回查消息对应的本地事务的状态;
- 根据本地事务状态,重新Commit或者Rollback;
- 补偿阶段用于解决消息Commit或者Rollback发生超时或者失败的情况。
2.7.2 基本使用
- Producer
使用 TransactionMQProducer类创建生产者,并指定唯一的 ProducerGroup,就可以设置自定义线程池来处理这些检查请求,执行本地事务后、需要根据执行结果对消息队列进行回复。
- setTransactionListener() 添加事务监听器作为执行本地事务的入口。
当发送HALF消息成功时,使用 executeLocalTransaction() 方法来执行本地事务,它返回三个事务状态之一。checkLocalTranscation() 方法用于检查本地事务状态,并回应消息队列的检查请求。
public class txProducer {
public static void main(String[] args) throws Exception {
// 1.创建消息生产者 Producer,并制定生产者组名
TransactionMQProducer producer = new TransactionMQProducer("group5");
// 2.指定 Nameserver 地址
producer.setNamesrvAddr("39.96.86.4:9876;59.110.158.93:9876");
// 添加事务监听器
producer.setTransactionListener(new TransactionListener() {
/**
* 在该方法中执行本地事务
*/
@Override
public LocalTransactionState executeLocalTransaction(Message msg, Object arg) {
if (StringUtils.equals("TAGA", msg.getTags())) {
return LocalTransactionState.COMMIT_MESSAGE;
} else if (StringUtils.equals("TAGB", msg.getTags())) {
return LocalTransactionState.ROLLBACK_MESSAGE;
} else if (StringUtils.equals("TAGC", msg.getTags())) {
return LocalTransactionState.UNKNOW;
}
return LocalTransactionState.UNKNOW;
}
/**
* 该方法是 MQ 进行消息事务状态回查的逻辑
*/
@Override
public LocalTransactionState checkLocalTransaction(MessageExt msg) {
System.out.println("消息的Tag:" + msg.getTags());
return LocalTransactionState.COMMIT_MESSAGE;
}
});
// 3.启动 Producer
producer.start();
String[] tags = {"TAGA", "TAGB", "TAGC"};
for (int i = 0; i < 3; i++) {
// 4.创建消息对象,指定主题 Topic、Tag 和消息体
Message msg = new Message("TransactionTopic", tags[i], ("Hello World" + i).getBytes());
// 5.发送消息
// 传入 null 对所有消息进行事务控制
SendResult result = producer.sendMessageInTransaction(msg, null);
// 发送状态
SendStatus status = result.getSendStatus();
System.out.println("发送结果:" + result);
TimeUnit.SECONDS.sleep(2);
}
// 6.关闭生产者 Producer
//producer.shutdown();
}
}
- 生产者2
- LocalTransactionExecuter接口:发送 Half 消息成功后,会立即调用这里判断当前的事务状态,可以写业务操作事务的代码。
- TransactionCheckListener接口:进行消息事务状态回查。
- 必须要有一致的业务逻辑。
public class txProducer2 {
public static void main(String[] args) throws MQClientException, InterruptedException {
// 监听器
TransactionCheckListener transactionCheckListener = new TransactionCheckListenerImpl();
TransactionMQProducer producer = new TransactionMQProducer("rename_your_producerGroup");
// 本地 check 线程池的 corePoolSize
producer.setCheckThreadPoolMinSize(2);
// 本地 check 线程池的 maximumPoolSize
producer.setCheckThreadPoolMaxSize(2);
// 本地 check 线程池的队列长度
producer.setCheckRequestHoldMax(2000);
// 设置 checkListener,Broker 定期 check 的时候被调用
producer.setTransactionCheckListener(transactionCheckListener);
producer.start();
String[] tags = new String[] {"TagA", "TagB", "TagC", "TagD", "TagE"};
//
TransactionExecuterImpl tranExecuter = new TransactionExecuterImpl();
for (int i = 0; i < 100; i++) {
try {
Message msg =
new Message("TopicTest", tags[i % tags.length], "KEY" + i,
("Hello RocketMQ " + i).getBytes(RemotingHelper.DEFAULT_CHARSET));
// 过程 1、发送 Half 消息
TransactionSendResult result = producer.sendMessageInTransaction(msg, tranExecuter, null);
System.out.printf("msgId:%s, transaction result:%s %n", result.getMsgId(), result.getLocalTransactionState());
if (result.getLocalTransactionState() != LocalTransactionState.COMMIT_MESSAGE) {
System.out.printf("halfMsg sendResult%s, transaction result:%s, errorMsg:%s %n", result.getSendStatus(), result.getLocalTransactionState(), result.getErrorMessage());
}
Thread.sleep(10);
} catch (MQClientException | UnsupportedEncodingException e) {
e.printStackTrace();
}
}
for (int i = 0; i < 100000; i++) {
Thread.sleep(1000);
}
producer.shutdown();
}
public static class TransactionExecuterImpl implements LocalTransactionExecuter {
private AtomicInteger transactionIndex = new AtomicInteger(1);
@Override
public LocalTransactionState executeLocalTransactionBranch(final Message msg, final Object arg) {
// 过程 2、发送事务状态
// 发送 Half 消息成功后,会立即调用这里判断当前的事务状态,这里可以写业务操作事务的代码。
// 如果应用还没执行完成宕机,或者暂时无法判断事务状态,返回 UNKNOW 状态
// Broker 定期发送消息来 Check 这个 Half 消息,会调用下面的 TransactionCheckListener 接口。
// 如果这里返回 null 或者抛出异常,同样设置为 UNKNOW 状态
int value = transactionIndex.getAndIncrement();
if (value == 0) {
//注意:如果抛出异常,等于设置 UNKNOW
throw new RuntimeException("Could not find db");
}
else if ((value % 5) == 0) {
return LocalTransactionState.ROLLBACK_MESSAGE;
}
else if ((value % 4) == 0) {
return LocalTransactionState.COMMIT_MESSAGE;
}
return LocalTransactionState.UNKNOW;
}
}
public static class TransactionCheckListenerImpl implements TransactionCheckListener {
private AtomicInteger transactionIndex = new AtomicInteger(0);
@Override
public LocalTransactionState checkLocalTransactionState(MessageExt msg) {
// 过程 2 发送 UNKNOWN 状态或者 Broker 没能在限定时间内接收状态,Broker 会定期 check 状态,则调用此接口
// 如果这里返回 null 或者抛出异常,同样设置为 UNKNOW 状态
System.out.printf("server checking TrMsg %s%n", msg);
int value = transactionIndex.getAndIncrement();
if ((value % 6) == 0) {
// 注意:如果抛出异常,等于设置 UNKNOW
throw new RuntimeException("Could not find db");
} else if ((value % 5) == 0) {
return LocalTransactionState.ROLLBACK_MESSAGE;
} else if ((value % 4) == 0) {
return LocalTransactionState.COMMIT_MESSAGE;
}
return LocalTransactionState.UNKNOW;
}
}
}
- 总结
- 事务消息不支持延时消息和批量消息。
- 为了避免单个消息被检查多次导致半队列消息累积,默认将单个消息的检查次数限制为 15 次,可以通过 Broker 配置文件的 transactionCheckMax参数来修改此限制。
- 如果已经检查某条消息超过 N 次的话,Broker 将丢弃此消息,并在默认情况下同时打印错误日志,可以通过重写 AbstractTransactionCheckListener 类来修改这个行为。
- 事务消息将在 Broker 配置文件中的参数 transactionMsgTimeout 配置的特定时长后被检查,当发送事务消息时,可以通过设置CHECK_IMMUNITY_TIME_IN_SECONDS 来改变这个限制,该参数优先于 transactionMsgTimeout 参数。
- 事务性消息可能不止一次被检查或消费。
- 提交给用户的目标主题消息可能会失败,它的高可用性通过 RocketMQ 本身的高可用性机制来保证,如果希望确保事务消息不丢失,并且事务完整性得到保证,可使用同步的双重写入机制。
- 事务消息的生产者 ID 不能与其它类型消息的生产者 ID 共享,事务消息允许反向查询,MQ服务器能通过生产者 ID 查询到消费者。
2.8 消费者主动Pull
《RocketMq : 消费消息的两种方式 pull 和 push》
- 示例
public class PullConsumer {
private static final Map<MessageQueue, Long> offseTable = new HashMap<MessageQueue, Long>();
public static void main(String[] args) throws MQClientException {
DefaultPullConsumer consumer = new DefaultPullConsumer("please_rename_unique_group_name_5");
consumer.start();
Set<MessageQueue> mqs = consumer.fetchSubscribeMessageQueues("TopicTest");
for (MessageQueue mq : mqs) {
System.out.println("Consume from the queue: " + mq);
PullResult pullResult;
try {
pullResult = consumer.pullBlockIfNotFound(mq, null, getMessageQueueOffset(mq), 32);
System.out.println(pullResult);
putMessageQueueOffset(mq, pullResult.getNextBeginOffset());
switch (pullResult.getPullStatus()) {
case FOUND:
// TODO
break;
case NO_MATCHED_MSG:
break;
case NO_NEW_MSG:
break;
case OFFSET_ILLEGAL:
break;
default:
break;
}
} catch (Exception e) {
e.printStackTrace();
}
}
consumer.shutdown();
}
private static long getMessageQueueOffset(MessageQueue mq) {
Long offset = offseTable.get(mq);
if (offset != null)
return offset;
return 0;
}
private static void putMessageQueueOffset(MessageQueue mq, long offset) {
offseTable.put(mq, offset);
}
}
三、RocketMQ补充
3.1 消息的存储 + 高可用性机制 + 负载均衡
- ACK机制保证持久化存储
- 消息存储为顺序写
- mmap零拷贝机制,提高消息存盘和网络发送的速度
3.2 消息重试
- 广播模式下,消费抛出异常,不会重试。
- 顺序消息消费,RocketMQ会自动不断进行消息重试,直到返回成功才停止重试。此时消息消费被阻塞,因此耗时时间有限制。
- 要保证消费的顺序性,所以一旦某个队列的某条消息消费失败,那么这个队列暂停消费,其它队列不受影响:
public ConsumeOrderlyStatus consumeMessage(List<MessageExt> msgs, ConsumeOrderlyContext context) {
// 设置挂起当前 Queue 的消费,挂起时间 5000ms
context.setSuspendCurrentQueueTimeMillis(5000);
return ConsumeOrderlyStatus.SUSPEND_CURRENT_QUEUE_A_MOMENT;
}
- 非顺序消息消费,耗时时间不做限制,默认自动重试16次,只对集群消费有效,超过最大重试次数后会被放入死信队列。
- 重试次数:
- 每次消费失败后,应用可以设置消息下次间隔多长时间到达Consumer,正常情况下,间隔时间会比较准确:
public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs,
ConsumeConcurrentlyContext context) {
MessageExt msg = msgs.get(0);
// 获取这条消息是第几次重试消费
long value = msg.getReconsumeTimes();
// 设置定时 Level 为 5,一分钟后重试消费这条消息
context.setDelayLevelWhenNextConsume(5);
return ConsumeConcurrentlyStatus.RECONSUME_LATER;
}
- 重试配置方式
- MessageListener可以传入consumer.subscribe() 中。
- 重试次数配置
consumer.getMetaPushConsumerImpl().setMaxReconsumeTimes();
3.3 死信队列
特性: