RocketMQ从实战到源码:客户端消息机制
😄生命不息,写作不止
🔥 继续踏上学习之路,学之分享笔记
👊 总有一天我也能像各位大佬一样
🏆 博客首页 @怒放吧德德 To记录领地 @一个有梦有戏的人
🌝分享学习心得,欢迎指正,大家一起学习成长!
转发请携带作者信息 @怒放吧德德(掘金) @一个有梦有戏的人(CSDN)
前言
前面一篇文章,我们简单学习了 RockerMQ 的安装以及简单 demo,现在我们就进一步学习 RockerMQ 的消息机制。本篇篇幅较大,内容较多,后续会将每个消息机制进行抽取。
1 基础使用
通过 rocket 的代码中的 example 案例,里面有简单的例子。(包:org.apache.rocketmq.example.quickstart)
生产者代码:
public class Producer {
/**
* The number of produced messages.
*/
public static final int MESSAGE_COUNT = 1000;
public static final String PRODUCER_GROUP = "Study1";
public static final String DEFAULT_NAMESRVADDR = "192.168.109.134:9876";
public static final String TOPIC = "TopicTest";
public static final String TAG = "TagA";
public static void main(String[] args) throws MQClientException, InterruptedException {
// 创建生产者,指定生产组
DefaultMQProducer producer = new DefaultMQProducer(PRODUCER_GROUP);
// 设置Nameserver地址
producer.setNamesrvAddr(DEFAULT_NAMESRVADDR);
// 启动生产者
producer.start();
for (int i = 0; i < MESSAGE_COUNT; i++) {
try {
// 创建消息体:订阅主题、Tag、消息体
Message msg = new Message(TOPIC,
TAG,
("Hello RocketMQ " + i).getBytes(RemotingHelper.DEFAULT_CHARSET)
);
// 发送消息
SendResult sendResult = producer.send(msg, 20 * 1000);
System.out.printf("%s%n", sendResult);
} catch (Exception e) {
e.printStackTrace();
Thread.sleep(1000);
}
}
// 关闭生产者,释放资源
producer.shutdown();
}
}
消费这代码:
public class Consumer {
public static final String CONSUMER_GROUP = "Study1";
public static final String DEFAULT_NAMESRVADDR = "192.168.109.134:9876";
public static final String TOPIC = "TopicTest";
public static void main(String[] args) throws MQClientException {
// 创建消费者,指定消费组
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer(CONSUMER_GROUP);
// 设置Nameserver地址
consumer.setNamesrvAddr(DEFAULT_NAMESRVADDR);
// 如果特定的消费群体是全新的,请指定从哪里开始
consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_FIRST_OFFSET);
// 配置订阅主题Topic、Tag
consumer.subscribe(TOPIC, "*");
// 设置回调函数,处理消息
consumer.registerMessageListener((MessageListenerConcurrently) (msg, context) -> {
System.out.printf("%s Receive New Messages: %s %n", Thread.currentThread().getName(), msg);
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
});
// 启动消费者,启动后会一直挂着,持续处理消息
consumer.start();
System.out.printf("Consumer Started.%n");
}
}
客户端编程逻辑顺序比较固定。
① 生产者固定步骤
- 创建生产者,指定生产组
- 设置Nameserver地址
- 启动生产者
- 创建消息体:订阅主题、Tag、消息体
- 发送消息
- 关闭生产者,释放资源
② 消费者固定步骤
- 创建消费者,指定消费组
- 设置Nameserver地址
- 如果特定的消费群体是全新的,请指定从哪里开始
- 配置订阅主题Topic、Tag
- 设置回调函数,处理消息
- 启动消费者,启动后会一直挂着,持续处理消息
2 确认消息机制
消息是必先要保证安全性,而这包含了两种,一是要确保消息发送到 Broker 上,二是要确保消费者能够正常从 Broker 消费。RocketMQ 提供了完善的消息确认机制,确保消息的可靠传递。主要分为生产者发送确认和消费者消费确认两大部分。
2.1 生产者端
生产者通过多次重试的机制保证消息正常发送到 Broker。生产者发送消息后,需要确认 Broker 是否成功接收并持久化消息,核心依赖 Broker 的响应结果,不同发送模式的确认方式不同。消息生产者的 3 种消息发送方法:同步发送、异步发送、单向发送(3 种简单代码上文均有提及)。
2.1.1 发送模式与确认逻辑
| 方式 | 确认方式 | 场景 |
|---|---|---|
| 同步发送 | 生产者阻塞等待 Broker 返回SendResult,通过SendStatus.SEND_OK判断是否成功 | 核心业务(如交易、支付),必须确保消息投递成功 |
| 异步发送 | 通过SendCallback回调函数接收结果:onSuccess表示成功,onException表示失败 | 高并发场景,无需阻塞等待结果 |
| 单向发送 | 只发送消息,不等待 Broker 响应,无确认机制 | 非核心业务(如日志采集),允许少量丢失 |
Broker 端的刷盘 / 复制策略会影响确认的可靠性:
- 刷盘策略:同步刷盘(消息写入后立即刷磁盘,确认慢但可靠)、异步刷盘(默认,写入内存后立即返回确认,性能高);
- 主从复制:同步复制(主 Broker 需等待从 Broker 同步完成再确认)、异步复制(默认,主 Broker 写入后直接确认)。
2.1.2 单向发送
单向发送的情况下,生产者只管往 Broker 发送消息,并不关心 Broker 端是否有接收到。
// 单向发送(无确认)
producer.sendOneway(message);
sendOneway方式发送消息有个好处就是效率高,但是如果 Broker 没收到的时候是无法补救的,适用日志采集这种形式。
2.1.3 同步发送
同步发送之后会阻塞当前线程,等待 Broker 响应,直到得到响应,开发可以通过获取的结果去做其他处理,如果发送失败可以进行补救,比如重发或者告警之类。
// 同步发送(有确认)
SendResult sendResult = producer.send(message);
System.out.println("发送状态:" + sendResult.getSendStatus()); // SEND_OK
SendResult 是 Broker 返回的信息,可以查看状态是否成功。里面有 SendStatus,用来表示发送状态。
public enum SendStatus {
SEND_OK,
FLUSH_DISK_TIMEOUT,
FLUSH_SLAVE_TIMEOUT,
SLAVE_NOT_AVAILABLE;
private SendStatus() {
}
}
这个枚举的说明如下:
- SEND_OK: 发送成功,消息已持久化到Broker
- FLUSH_DISK_TIMEOUT: 刷盘超时(同步刷盘模式)
- FLUSH_SLAVE_TIMEOUT: 同步到Slave超时(主从模式)
- SLAVE_NOT_AVAILABLE: Slave不可用
但是此时要注意,如果Broker端返回的SendStatus不是SEND_OK,也并不表示消息就⼀定不会推送给下游的消费者。仅仅只是表示Broker端并没有完全正确的处理这些消息。因此,如果要重新发送消息,最好要带上唯⼀的系统标识,这样在消费者端,才能⾃⾏做幂等判断。也就是⽤具有业务含义的OrderID这样的字段来判断消息有没有被重复处理。
同步的可能会导致发送耗时比较长,影响后续逻辑。
2.1.4 异步发送
异步发送机制下,⽣产者在向Broker发送消息时,会同时注册⼀个回调函数。接下来⽣产者并不等待Broker的响应。当Broker端有响应数据过来时,⾃动触发回调函数进⾏对应的处理。
// 异步发送(回调确认)
producer.send(message, new SendCallback() {
@Override
public void onSuccess(SendResult sendResult) {
// 发送成功回调
}
@Override
public void onException(Throwable e) {
// 发送失败回调
}
});
这个回调函数是用来处理成功与失败的逻辑,如果失败了,就会执行 onException代码逻辑,可以在这里定义对应的补救措施。
注:触发了SendCallback的onException⽅法同样并不⼀定就表示消息不会向消费者推
送。如果Broker端返回响应信息太慢,超过了超时时间,也会触发 onException 方法。
超时默认是 3 秒,通过 producer.setSendMsgTimeout 可以修改。
2.2 消费者端状态确认机制
消费者端采用状态确认机制保证消费者⼀定能正常处理对应的消息。消费者的确认机制核心是 消费偏移量(Offset)的提交 ——Offset 表示消费者消费到队列的哪个位置,Broker 通过 Offset 判断消息是否已被消费。也就是消费者会返回结果给 Broker,Broker等待消费者返回消息处理状态。
2.2.1 自动确认
原理:消费线程处理消息时如果没有抛出异常,客户端会自动向 Broker 提交 Offset,Broker 标记消息已消费;如果抛出异常,不提交 Offset,Broker 会重试该消息。
注意:批量消费时,只要有一条消息消费失败,整个批次的 Offset 都不会提交,会重试整个批次。
代码:
// PushConsumer - 自动确认(默认)
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("group");
consumer.registerMessageListener((MessageListenerConcurrently) (msgs, context) -> {
// 业务处理
for (MessageExt msg : msgs) {
System.out.println(new String(msg.getBody()));
}
// 自动返回CONSUME_SUCCESS
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
});
2.2.2 手动确认
适用于需要精准控制确认时机的场景(如消息消费后需写入数据库,需确保数据库写入成功再确认)。
// 手动确认示例
consumer.registerMessageListener(new MessageListenerConcurrently() {
@Override
public ConsumeConcurrentlyStatus consumeMessage(
List<MessageExt> msgs,
ConsumeConcurrentlyContext context
) {
try {
// 业务处理
processMessages(msgs);
// 手动确认成功
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
} catch (Exception e) {
// 返回失败,稍后重试
return ConsumeConcurrentlyStatus.RECONSUME_LATER;
}
}
});
2.2.3 确认消息的关键
这个返回值是⼀个枚举值,有两个选项 CONSUME_SUCCESS 和 RECONSUME_LATER。如果消费者返回 CONSUME_SUCCESS,那么消息自然就处理结束了。但是如果消费者没有处理成功,返回的是 RECONSUME_LATER,Broker 就会过⼀段时间再发起消息重试。
消费状态如下:
- CONSUME_SUCCESS: 消费成功,消息将被删除
- RECONSUME_LATER: 消费失败,稍后重试
总的来说就是如下几点
- 重试机制:未确认的消息会进入重试队列(
%RETRY%+消费组名),默认重试 16 次,超过次数进入死信队列(%DLQ%+消费组名); - Offset 存储:Broker 将消费组 - 队列的 Offset 存储在内置主题
consumerOffse中,即使消费者重启也能恢复消费进度; - 顺序消费确认:顺序消费默认是 “手动确认变种”,通过加锁机制保证消费顺序,只有当前消息确认后才会消费下一条。
2.3 消费者端自行指定消费点
Broker 端通过 Consumer 返回的状态来推进所属消费者组对应的 Offset。RocketMQ 可以让消费者自行定义,通过设置 ConsumeFromWhere 与 ConsumeTimestamp 来设置消费起始位置。
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("group");
// 方法1:从指定时间点开始消费
consumer.setConsumeTimestamp("20260101000000"); // 格式:yyyyMMddHHmmss
// 方法2:指定消费起始位置策略
consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_FIRST_OFFSET); // 从最早开始
consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_LAST_OFFSET); // 从最新开始
consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_TIMESTAMP); // 从指定时间开始
ConsumeFromWhere:消费位置策略枚举
public enum ConsumeFromWhere {
CONSUME_FROM_LAST_OFFSET, // 从队列的第⼀条消息开始重新消费
CONSUME_FROM_FIRST_OFFSET, // 从上次消费到的地⽅开始继续消费
CONSUME_FROM_TIMESTAMP // 从某⼀个时间点开始重新消费
}
如果通过时间点来指定消费,需要指定时间点,这个时间点的格式:yyyyMMddHHmmss。
3 广播消息
在 rocketmq 中,主要有两种消息的消费模式,一种是集群消费,另一种是广播消费。
- 集群消费(Clustering):默认模式。同一条消息只会被同一个消费者组(Consumer Group) 中的一个消费者消费。其设计目标是实现负载均衡和高可用,多个消费者共同分担消息处理任务。
- 广播消费(Broadcasting):同一条消息会被同一个消费者组中的每一个消费者消费一次。其设计目标是让多个订阅者都接收到完整的消息流。
集群模式下,⼀个消息,只会被⼀个消费者组中的多个消费者实例共同处理⼀次。⼴播模式下,⼀个消息,则会推送给所有消费者实例处理,不再关⼼消费者组。
3.1 简单代码
JavaAPI 代码
// !!!关键设置:将消费模式设置为广播模式 !!!
consumer.setMessageModel(MessageModel.BROADCASTING);
消息模型枚举有两种,分别是集群和广播
public enum MessageModel {
BROADCASTING("BROADCASTING"),
CLUSTERING("CLUSTERING");
// ....
}
实现思路
默认模式(也就是集群模式)下,Broker端会给每个ConsumerGroup维护⼀个统⼀的Offset,这样,当Consumer来拉取消息时,就可以通过Offset保证⼀个消息,在同⼀个ConsumerGroup内只会被消费⼀次。⽽⼴播模式的本质,是将Offset转移到Consumer端⾃⾏保管,包括Offset的记录以及更新,全都放到客户端。这样Broker推送消息时,就不再管ConsumerGroup,只要Consumer来拉取消息,就返回对应的消息。
3.2 工作原理
假设你有一个消费者组 MyConsumerGroup,它下面有三个消费者实例 C1, C2, C3,它们都订阅了主题 MyTopic。
- 在集群模式下:主题
MyTopic下的10条消息会以负载均衡的方式(默认为轮询)平均分配给C1,C2,C3消费。每个消息只会被其中一个实例消费。 - 在广播模式下:主题
MyTopic下的每一条消息,都会完整地发送给C1,C2,C3各一份。每个消费者实例都会独立消费这10条消息。
集群模式 (负载均衡) 广播模式 (全量复制)
Msg1 -> [C1] Msg1 -> [C1]
Msg2 -> [C2] Msg2 -> [C1]
Msg3 -> [C3] VS Msg3 -> [C1]
Msg4 -> [C1] ... (以此类推,每条消息)
Msg1 -> [C2]
Msg2 -> [C2]
...
Msg1 -> [C3]
Msg2 -> [C3]
...
3.3 注意事项
- 消费进度(Offset)管理:
- 集群模式:消费进度由Broker端集中存储和管理(新版),所有消费者实例共享同一份进度。如果
C1消费了Msg1,整个组就认为Msg1已被消费。 - 广播模式:消费进度存储在消费者实例本地(例如本地文件系统)。每个实例独立维护自己的消费进度,互不影响。
C1消费到哪里,与C2、C3完全无关。这意味着如果C1实例重启,它会从自己本地的记录开始消费,而不会从其他实例的进度继续。
- 集群模式:消费进度由Broker端集中存储和管理(新版),所有消费者实例共享同一份进度。如果
- 资源消耗与性能:
- 广播模式下,网络流量和Broker的负载会成倍增加(与消费者实例数量成正比)。假设有N个实例,每条消息理论上会被传输N次。
- 每个消费者实例都要处理全量消息,对下游业务系统(如数据库)可能造成重复写入压力,需要业务方自己处理幂等性。
- 适用场景:
- 缓存刷新/同步:多个应用服务器实例需要同时刷新本地缓存(如商品信息变更、配置更新)。
- 事件通知:一个事件需要触发所有微服务实例的某个动作(如日志清理、连接重置)。
- 数据镜像:多个异地系统需要维护一份相同的数据副本。
- 不适用场景:
- 需要负载分担处理大量消息的场景(如订单处理、日志收集)。此时应使用集群模式。
- 对消息处理有严格的“只处理一次”要求,且不希望业务层做复杂幂等处理的场景。
4 过滤消息
RocketMQ 的过滤消息功能允许消费者只接收其感兴趣的消息,而不是订阅主题下的所有消息。这在多种消费者对同一主题有不同关注点的场景下非常有用。
当我们相同的模块的消息都放到同一个 Topic 中,但是不同的业务需要根据不同的需求去获取不同的消息。例如仓储系统,入库出库消息都是在仓储的 Topic 中,仓储系统需要记账,就根据所需的消息去做进账与出账业务。
RocketMQ 的消息过滤是在 Broker 端执行的核心机制,核心是通过Tag 标签过滤和SQL92 属性过滤两种方式,让消费者只接收符合条件的消息,减少无效网络传输与消费端压力,过滤在服务端完成,遵循 “匹配即投递” 原则。
4.1 Tag 过滤
基于消息的 Tag 标签进行过滤,Tag 是消息的子分类标识,每个消息只能有一个 Tag。
语法规则
- 单 Tag 匹配:如 "CREATE",只接收标签为 CREATE 的消息。
- 多 Tag 匹配:多个 Tag 间用
||分隔(逻辑或),如 "CREATE||PAY||CANCEL"。(不支持模糊匹配) - 全匹配:用
*表示接收该主题下所有消息。
实现原理
- 生产者发送消息时,Tag 会被计算哈希值存入 ConsumeQueue(消息消费逻辑队列)。
- Broker 过滤时先通过 ConsumeQueue 中的 Tag 哈希值快速匹配,命中后再校验消息原始 Tag 字符串,避免哈希冲突导致误匹配。
- 消费端拉取到消息后,也会二次校验 Tag 原始值,确保过滤准确性。
代码案例
生产者,向 Topic 是 TopicTest 发送 TagA、TagB、TagC 三种标签的消息。
public class TagFilterProducer {
public static final int MESSAGE_COUNT = 20;
public static final String PRODUCER_GROUP = "Study1";
public static final String DEFAULT_NAMESRVADDR = "192.168.109.134:9876";
public static final String TOPIC = "TopicTest";
public static final List<String> TAGS = List.of("TagA", "TagB", "TagC");
public static void main(String[] args) throws MQClientException, InterruptedException {
// 创建消费者,指定消费组
DefaultMQProducer producer = new DefaultMQProducer(PRODUCER_GROUP);
// 设置Nameserver地址
producer.setNamesrvAddr(DEFAULT_NAMESRVADDR);
// 启动生产者
producer.start();
for (int i = 0; i < MESSAGE_COUNT; i++) {
try {
// 创建消息体:订阅主题、Tag、消息体
Message msg = new Message(TOPIC,
TAGS.get(i % TAGS.size()),
("Hello RocketMQ " + i).getBytes(RemotingHelper.DEFAULT_CHARSET)
);
// 发送消息
SendResult sendResult = producer.send(msg, 20 * 1000);
System.out.printf("%s%n", sendResult);
} catch (Exception e) {
e.printStackTrace();
Thread.sleep(1000);
}
}
// 关闭生产者,释放资源
producer.shutdown();
}
}
过滤消费者
通过指定 Tag 规则进行过滤。
public class TagFilterConsumer {
public static final String CONSUMER_GROUP = "Study1";
public static final String DEFAULT_NAMESRVADDR = "192.168.109.134:9876";
public static final String TOPIC = "TopicTest";
public static void main(String[] args) throws MQClientException {
// 创建消费者,指定消费组
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer(CONSUMER_GROUP);
// 设置Nameserver地址
consumer.setNamesrvAddr(DEFAULT_NAMESRVADDR);
// 如果特定的消费群体是全新的,请指定从哪里开始
consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_FIRST_OFFSET);
// 配置订阅主题Topic、Tag
// consumer.subscribe(TOPIC, "*");
// consumer.subscribe(TOPIC, "TagA");
consumer.subscribe(TOPIC, "TagA || TagB");
// 设置回调函数,处理消息
consumer.registerMessageListener((MessageListenerConcurrently) (msg, context) -> {
System.out.printf("%s Receive New Messages: %s %n", Thread.currentThread().getName(), msg);
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
});
// 启动消费者,启动后会一直挂着,持续处理消息
consumer.start();
System.out.printf("Consumer Started.%n");
}
}
过滤可以获取全部的消息、指定单个的 Tag 或者指定多个 Tag。
// 通配
consumer.subscribe(TOPIC, "*");
// 单个
consumer.subscribe(TOPIC, "TagA");
// 多个
consumer.subscribe(TOPIC, "TagA || TagB");
适用场景:简单的消息分类过滤,如订单创建、支付、取消等不同类型消息的分离,适合高吞吐量场景,性能损耗低。
4.2 SQL 过滤
SQL 过滤是基于消息的自定义属性(key-value)进行过滤,支持 SQL92 语法的子集,可实现复杂条件匹配,每个消息可设置多个属性。
支持语法元素
- 比较运算符:
=、<>、>、>=、<、<=。 - 逻辑运算符:
AND、OR、NOT。 - 常量:字符串(用单引号)、数字、
NULL。 - 函数:
IN、LIKE、BETWEEN等。 - 模糊匹配:
%匹配任意 0 个或多个字符,_匹配任意单个字符 。
实现原理:依赖 rocketmq-filter 模块构建并执行 SQL 表达式,过滤时需读取消息的完整属性进行计算,性能略低于 Tag 过滤,但灵活性更高。
案例代码
生产者还是指定标签 ABC,并且可以自定义写入属性信息。
public class SQLFilterProducer {
public static final int MESSAGE_COUNT = 20;
public static final String PRODUCER_GROUP = "Study1";
public static final String DEFAULT_NAMESRVADDR = "192.168.109.134:9876";
public static final String TOPIC = "SQLTopic";
public static final List<String> TAGS = List.of("TagA", "TagB", "TagC");
public static void main(String[] args) throws MQClientException, InterruptedException {
// 创建消费者,指定消费组
DefaultMQProducer producer = new DefaultMQProducer(PRODUCER_GROUP);
// 设置Nameserver地址
producer.setNamesrvAddr(DEFAULT_NAMESRVADDR);
// 启动生产者
producer.start();
for (int i = 0; i < MESSAGE_COUNT; i++) {
try {
// 创建消息体:订阅主题、Tag、消息体
Message msg = new Message(TOPIC,
TAGS.get(i % TAGS.size()),
("Hello RocketMQ " + i).getBytes(RemotingHelper.DEFAULT_CHARSET)
);
// 生产者发送带自定义属性的消息
msg.putUserProperty("orderId", "123456");
msg.putUserProperty("qty", String.valueOf(10 + i));
// 发送消息
SendResult sendResult = producer.send(msg, 20 * 1000);
System.out.printf("%s%n", sendResult);
} catch (Exception e) {
e.printStackTrace();
Thread.sleep(1000);
}
}
// 关闭生产者,释放资源
producer.shutdown();
}
}
消费者通过 SQL 进行过滤
public class SQLFilterConsumer {
public static final String CONSUMER_GROUP = "Study1";
public static final String DEFAULT_NAMESRVADDR = "192.168.109.134:9876";
public static final String TOPIC = "SQLTopic";
public static void main(String[] args) throws MQClientException {
// 创建消费者,指定消费组
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer(CONSUMER_GROUP);
// 设置Nameserver地址
consumer.setNamesrvAddr(DEFAULT_NAMESRVADDR);
// 如果特定的消费群体是全新的,请指定从哪里开始
consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_FIRST_OFFSET);
// 配置订阅主题Topic、SQL
consumer.subscribe(TOPIC,
MessageSelector.bySql("(TAGS IS NOT NULL AND TAGS IN ('TagA', 'TagB)) " +
"AND (orderId IS NOT NULL AND qty > 15)"));
// 设置回调函数,处理消息
consumer.registerMessageListener((MessageListenerConcurrently) (msg, context) -> {
System.out.printf("%s Receive New Messages: %s %n", Thread.currentThread().getName(), msg);
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
});
// 启动消费者,启动后会一直挂着,持续处理消息
consumer.start();
System.out.printf("Consumer Started.%n");
}
}
过滤出标签是 TagA 和 TagB 以及 qty 大于 15 并且 orderId 不能为空。
适用场景:复杂业务过滤,如根据订单金额、时间范围、用户等级等多维度筛选消息。
注:通过 TAGS 过滤是不用担心性能问题,但是通过自定义属性就会造成性能问题。 RocketMQ 中被过滤掉的消息不会消失,过滤只是限制这些消息投递给当前匹配过滤条件的消费者,消息本身的生命周期完全由 Broker 的存储策略决定,和是否被过滤没有关系。
由于模糊匹配(特别是 %xxx 这种前置通配符)会导致 Broker 无法利用索引,必须逐条扫描消息属性,在消息量巨大时会严重影响性能。如果可能,建议在发送消息时将需要模糊匹配的维度拆分为独立的消息属性(例如将 orderType 拆分为 prefix 和 suffix 两个属性),或者直接使用 Tag 进行粗粒度过滤,再在消费端进行细粒度的模糊匹配。
5 顺序消息
RocketMQ 的顺序消息(FIFO 消息)是一种**高级消息类型,支持消费者按照发送消息的先后顺序获取消息,从而实现业务场景中的顺序处理。它通过消息组 (MessageGroup)** 机制保证局部顺序,在满足业务顺序需求的同时兼顾系统吞吐能力。
生产端:保证发送顺序
- 队列选择器:通过
MessageQueueSelector将同一业务标识(如订单 ID)的消息路由到同一个队列,避免跨队列乱序 - 同步发送:必须使用同步发送(
send()),禁止异步发送,确保消息发送成功后再发送下一条 - 消息组机制:新版 RocketMQ 通过
MessageGroup属性标记消息归属,服务端自动路由到对应队列。
⽣产者只有将⼀批有顺序要求的消息,放到同⼀个MesasgeQueue上,通过MessageQueue的FIFO特性保证这⼀批消息的顺序。如果不指定MessageSelector对象,那么⽣产者会采⽤轮询的⽅式将多条消息依次发送到不同的MessageQueue上。
生产者案例代码
// 生产者发送顺序消息
producer.send(message, new MessageQueueSelector() {
@Override
public MessageQueue select(List<MessageQueue> mqs, Message msg, Object arg) {
// 按订单ID哈希选择队列,确保同一订单消息进入同一队列
Integer orderId = (Integer) arg;
int index = orderId % mqs.size();
return mqs.get(index);
}
}, orderId); // 传递订单ID作为路由参数
消费端:保证处理顺序
- 顺序消费模式:使用
MessageListenerOrderly(而非并发模式MessageListenerConcurrently) - 队列独占:同一队列同一时间仅分配给一个消费线程,线程按消息顺序串行处理
- 队列锁机制:消费端通过
ConsumeOrderlyContext自动加锁,防止同一队列被多线程并发消费。
消费者需要实现MessageListenerOrderly接⼝,实际上在服务端,处理MessageListenerOrderly时,会给⼀个MessageQueue加锁,拿到 MessageQueue上所有的消息,然后再去读取下⼀个MessageQueue的消息。
消费者案例代码
consumer.registerMessageListener(new MessageListenerOrderly() {
AtomicLong consumeTimes = new AtomicLong(0);
@Override
public ConsumeOrderlyStatus consumeMessage(List<MessageExt> msgs, ConsumeOrderlyContext context) {
context.setAutoCommit(true);
System.out.printf("%s Receive New Messages: %s %n", Thread.currentThread().getName(), msgs);
this.consumeTimes.incrementAndGet();
if ((this.consumeTimes.get() % 2) == 0) {
return ConsumeOrderlyStatus.SUCCESS;
} else if ((this.consumeTimes.get() % 5) == 0) {
context.setSuspendCurrentQueueTimeMillis(3000);
return ConsumeOrderlyStatus.SUSPEND_CURRENT_QUEUE_A_MOMENT;
}
return ConsumeOrderlyStatus.SUCCESS;
}
});
原理就是将消息都发到同一个MessageQueue。具体可以看官网:顺序消息 | RocketMQ
注:
1、一般场景下是局部有序,如果需要全局有序,那就将消息统一放到一个 MessageQueue 。
2、⽣产者端尽可能将同一组有序消息打散到不同的 MessageQueue 上,避免过于集中导致数据热点竞争。
3、消费者端只进⾏有限次数的重试。如果⼀条消息处理失败,RocketMQ会将后续消息阻塞住,让消费者进⾏重试。但是,如果消费者⼀直处理失败,超过最⼤重试次数,那么RocketMQ就会跳过这⼀条消息,处理后⾯的消息,这会造成消息乱序。
4、消费者端如果确实处理逻辑中出现问题,不建议抛出异常,可以返回
ConsumeOrderlyStatus.SUSPEND_CURRENT_QUEUE_A_MOMENT作为替代。
6 延迟消息
RocketMQ 的延迟消息是一种高级消息特性,指消息发送到 Broker 后,不会立即被消费者消费,而是在指定时间(或延迟级别对应的时间)后才投递到目标 Topic 供消费者处理。该机制广泛应用于订单超时处理、定时任务调度、通知提醒等场景。
6.1 原理
消息发送后,在固定的时间后才被消费,官方有两个版本,两者本质相同,均通过服务端定时调度实现,5.x 版本统一称为定时消息。
- RocketMQ 4.x 默认支持18 个固定延迟级别,不支持任意时间延迟。
Broker 配置可自定义延迟级别:messageDelayLevel=1s 5s 10s 30s 1m ... 1h 2h
- RocketMQ 5.x 引入精确定时消息,支持毫秒级任意时间延迟,通过设置绝对时间戳实现。
6.2 代码案例
生产者的核心代码
Message message = new Message(TOPIC, ("Hello scheduled message " + i).getBytes(StandardCharsets.UTF_8));
// This message will be delivered to consumer 10 seconds later.
message.setDelayTimeLevel(3);
// Send the message
SendResult result = producer.send(message);
System.out.print(result);
通过设置延迟时间级别 message.setDelayTimeLevel(3)代表 10 秒后发送。Broker 接收消息后,判断为延迟消息,将其暂存到内部主题 SCHEDULE_TOPIC_XXXX。Broker 启动定时任务线程池,每个延迟级别对应一个定时任务,定时任务周期性扫描对应队列,判断消息是否到期(存储时间 + 延迟时间≤当前时间),到期消息被重新投递到原始目标 Topic 的对应队列,消费者此时可正常消费该消息。
Message message = new Message(TOPIC, ("Hello scheduled message " + i).getBytes(StandardCharsets.UTF_8));
// This message will be delivered to consumer 10 seconds later.
//message.setDelayTimeSec(10);
// The effect is the same as the above
// message.setDelayTimeMs(10_000L);
// Set the specific delivery time, and the effect is the same as the above
// long deliverTime = LocalDateTime.of(2026, 1, 20, 10, 0, 0)
// .atZone(ZoneId.systemDefault())
// .toInstant()
// .toEpochMilli();
// message.setDeliverTimeMs(deliverTime);
message.setDeliverTimeMs(System.currentTimeMillis() + 10_000L);
// Send the message
SendResult result = producer.send(message);
System.out.printf(result + "\n");
时间轮将时间划分为固定槽位,每个槽位对应一个时间间隔。消息根据到期时间放入对应槽位,通过指针循环扫描处理到期消息。具体可以看官网:延迟消息发送 | RocketMQ
7 批量消息
在对吞吐率有一定要求的情况下,Apache RocketMQ可以将一些消息聚成一批以后进行发送,可以增加吞吐率,并减少API和网络调用次数。
案例代码
// 添加批量消息
List<Message> messages = new ArrayList<>(MESSAGE_COUNT);
for (int i = 0; i < MESSAGE_COUNT; i++) {
messages.add(new Message(TOPIC, TAG, "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);
System.out.printf("%s", sendResult);
}
注:
批量消息的使用非常简单,但是要注意RocketMQ做了限制。同一批消息的Topic必须相同,另外,不支持延迟消息。
还有批量消息的大小不要超过1M,如果太大就需要自行分割。
8 事务消息
事务消息为 Apache RocketMQ 中的高级特性消息,本文为您介绍事务消息的应用场景、功能原理、使用限制、使用方法和使用建议。
事务消息是 Apache RocketMQ 提供的一种高级消息类型,支持在分布式场景下保障消息生产和本地事务的最终一致性。
8.1 工作流程
简单的描述以上内容(后续会有文章来单独介绍,具体内容请看原文:事务消息 | RocketMQ)
首先,生产者将消息发送至Apache RocketMQ服务端,实际上是向 Broker(Server)发送一条 “半消息”(Half Message),Broker 确认接收成功后,这条消息暂时不会投递给消费者。然后生产者开始执行本地业务事务(比如数据库操作),确保业务逻辑和消息发送的一致性。接着本地事务执行完成后,生产者向 Broker 发送 Commit(事务成功)或 Rollback(事务失败)指令,告知 Broker 最终处理结果。事务状态回查有补偿机制,如果 Broker 长时间未收到生产者的指令,会主动触发事务回查,向生产者询问本地事务的最终状态;生产者重新检查本地事务并反馈结果。最后 Broker 收到 Commit 指令时,会将半消息正式投递给消费者(Subscriber);收到 Rollback 指令时,则直接丢弃半消息,不进行投递。
8.2 生命周期
- 初始化:半事务消息被生产者构建并完成初始化,待发送到服务端的状态。
- 事务待提交:半事务消息被发送到服务端,和普通消息不同,并不会直接被服务端持久化,而是会被单独存储到事务存储系统中,等待第二阶段本地事务返回执行结果后再提交。此时消息对下游消费者不可见。
- 消息回滚:第二阶段如果事务执行结果明确为回滚,服务端会将半事务消息回滚,该事务消息流程终止。
- 提交待消费:第二阶段如果事务执行结果明确为提交,服务端会将半事务消息重新存储到普通存储系统中,此时消息对下游消费者可见,等待被消费者获取并消费。
- 消费中:消息被消费者获取,并按照消费者本地的业务逻辑进行处理的过程。 此时服务端会等待消费者完成消费并提交消费结果,如果一定时间后没有收到消费者的响应,Apache RocketMQ会对消息进行重试处理。具体信息。
- 消费提交:消费者完成消费处理,并向服务端提交消费结果,服务端标记当前消息已经被处理(包括消费成功和失败)。 Apache RocketMQ默认支持保留所有消息,此时消息数据并不会立即被删除,只是逻辑标记已消费。消息在保存时间到期或存储空间不足被删除前,消费者仍然可以回溯消息重新消费。
- 消息删除:Apache RocketMQ按照消息保存机制滚动清理最早的消息数据,将消息从物理文件中删除。
8.3 官方实例代码
① 实现 TransactionListener 接口
public class TransactionListenerImpl implements TransactionListener {
// 原子整型:生成自增数值,模拟不同的本地事务结果(线程安全,适配多线程发送场景)
private AtomicInteger transactionIndex = new AtomicInteger(0);
// 并发哈希表:存储「事务消息ID-事务状态」的映射,供回查时使用(线程安全)
private ConcurrentHashMap<String, Integer> localTrans = new ConcurrentHashMap<>();
// 执行本地事务
@Override
public LocalTransactionState executeLocalTransaction(Message msg, Object arg) {
int value = transactionIndex.getAndIncrement();
int status = value % 3;
localTrans.put(msg.getTransactionId(), status);
return LocalTransactionState.UNKNOW;
}
// 回查事务状态
@Override
public LocalTransactionState checkLocalTransaction(MessageExt msg) {
Integer status = localTrans.get(msg.getTransactionId());
if (null != status) {
switch (status) {
case 0:
return LocalTransactionState.UNKNOW;
case 1:
return LocalTransactionState.COMMIT_MESSAGE;
case 2:
return LocalTransactionState.ROLLBACK_MESSAGE;
default:
return LocalTransactionState.COMMIT_MESSAGE;
}
}
return LocalTransactionState.COMMIT_MESSAGE;
}
}
② 生产者
public class TransactionProducer {
public static final String PRODUCER_GROUP = "please_rename_unique_group_name";
public static final String DEFAULT_NAMESRVADDR = "127.0.0.1:9876";
public static final String TOPIC = "TopicTest1234";
public static final int MESSAGE_COUNT = 10;
public static void main(String[] args) throws MQClientException, InterruptedException {
TransactionListener transactionListener = new TransactionListenerImpl();
TransactionMQProducer producer = new TransactionMQProducer(PRODUCER_GROUP);
producer.setNamesrvAddr(DEFAULT_NAMESRVADDR);
ExecutorService executorService = new ThreadPoolExecutor(2, 5, 100, TimeUnit.SECONDS, new ArrayBlockingQueue<>(2000), r -> {
Thread thread = new Thread(r);
thread.setName("client-transaction-msg-check-thread");
return thread;
});
producer.setExecutorService(executorService);
producer.setTransactionListener(transactionListener);
producer.start();
String[] tags = new String[] {"TagA", "TagB", "TagC", "TagD", "TagE"};
for (int i = 0; i < MESSAGE_COUNT; i++) {
try {
Message msg =
new Message(TOPIC, tags[i % tags.length], "KEY" + i,
("Hello RocketMQ " + i).getBytes(RemotingHelper.DEFAULT_CHARSET));
SendResult sendResult = producer.sendMessageInTransaction(msg, null);
System.out.printf("%s%n", sendResult);
Thread.sleep(10);
} catch (MQClientException | UnsupportedEncodingException e) {
e.printStackTrace();
}
}
// 保持程序运行(关键!否则生产者关闭后无法接收回查回调)
for (int i = 0; i < 100000; i++) {
Thread.sleep(1000);
}
producer.shutdown();
}
}
这两段代码,其中 TransactionListenerImpl 是事务监听器的实现类(负责处理本地事务和事务回查),TransactionProducer 是事务消息生产者(负责发送事务消息并驱动整个流程)。
public enum LocalTransactionState {
COMMIT_MESSAGE, // “提交”,消息对消费者可见;
ROLLBACK_MESSAGE, // “回滚”,消息被丢弃;
UNKNOW; // “未知”,RocketMQ 会继续回查(直到超时);
}
当 executeLocalTransaction 返回 UNKNOW,或生产者与 RocketMQ 通信异常时,RocketMQ会定时回调该方法,确认本地事务最终状态;
必须使用 TransactionMQProducer来发送消息。
9 ACL 权限控制机制
访问控制 2.0(ACL 2.0)是Apache RocketMQ的访问控制列表(Access Control List)升级版本,提供了完善的身份认证(Authentication)和权限授权(Authorization)机制,用于保护RocketMQ集群的数据安全。
我们可以在控制界面给 topic 设置权限
如图中的 perm 表示 topic 的权限,有三个可选项。 2:禁写禁订阅,4:可订阅,不能写,6:可写可订阅。
在Broker端还提供了更详细的权限控制机制。主要是在broker.conf中打开acl的标志:aclEnable=true。就可以用plain_acl.yml来进⾏权限配置了,并且是热加载,无需重启。
但是我这个是 2.0,在 ACL2.0 这种方法已经被移除了(后续文章将详细描写)。
ACL 2.0 可以通过配置 broker.conf 来设置认证授权等。
总结
RocketMQ 客户端消息机制具备清晰的使用逻辑与完善的可靠性保障:生产者、消费者均有固定编程步骤;消息确认机制分生产端(同步 / 异步 / 单向发送的差异化确认,关联 Broker 刷盘 / 复制策略)和消费端(基于 Offset 的自动 / 手动确认,含重试、死信队列等兜底);消费模式支持集群(负载均衡,Broker 管理 Offset)与广播(全量消费,本地维护 Offset);消息过滤可通过 Tag(简单高效)或 SQL92(复杂灵活)在 Broker 端实现,减少无效传输。
转发请携带作者信息 @怒放吧德德 @一个有梦有戏的人
持续创作很不容易,作者将以尽可能的详细把所学知识分享各位开发者,一起进步一起学习。转载请携带链接,转载到微信公众号请勿选择原创,谢谢!
👍创作不易,如有错误请指正,感谢观看!记得点赞哦!👍
谢谢支持!